Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
tx_execution.cpp
Go to the documentation of this file.
2
3#include <cstdint>
4#include <stdexcept>
5
7
8namespace bb::avm2::simulation {
9namespace {
10
11// A tx-level exception that is expected to be handled.
12// This is in contrast to other runtime exceptions that might occur and should be propagated.
13// Note, however, that we re-throw unrecoverable errors of this type (exceptions thrown in insert_non_revertibles()).
14class TxExecutionException : public std::runtime_error {
15 public:
16 TxExecutionException(const std::string& message)
17 : std::runtime_error(message)
18 {}
19};
20
21} // namespace
22
63{
64 const Gas& gas_limit = tx.gas_settings.gas_limits;
65 const Gas& teardown_gas_limit = tx.gas_settings.teardown_gas_limits;
66 tx_context.gas_used = tx.gas_used_by_private;
67
68 // NOTE: This vector will be populated with one CallStackMetadata per app logic enqueued call.
69 // IMPORTANT: The nesting will only be 1 level deep! You will get one result per enqueued call
70 // but no information about nested calls. This can be added later.
71 std::vector<CallStackMetadata> app_logic_return_values;
72
74 .gas_used = tx_context.gas_used,
75 .gas_limit = gas_limit,
76 .teardown_gas_limit = teardown_gas_limit,
77 .phase_lengths = PhaseLengths::from_tx(tx), // Extract lengths of each phase at start.
78 });
79
80 vinfo("Simulating tx ",
81 tx.hash,
82 " with ",
83 tx.setup_enqueued_calls.size(),
84 " setup enqueued calls, ",
85 tx.app_logic_enqueued_calls.size(),
86 " app logic enqueued calls, and ",
87 tx.teardown_enqueued_call.has_value() ? "1 teardown enqueued call" : "no teardown enqueued call");
88
89 // Let the metadata collector know that we are entering the SETUP phase.
91
92 // Insert non-revertibles. This can throw if there is a nullifier collision or the maximum number of
93 // nullifiers, note hashes, or L2 to L1 messages is reached.
94 // That would result in an unprovable tx.
96
97 // Setup.
98 if (tx.setup_enqueued_calls.empty()) {
100 } else {
101 for (const auto& call : tx.setup_enqueued_calls) {
102 vinfo("[SETUP] Executing enqueued call to ",
103 call.request.contract_address,
104 "::",
105 get_debug_function_name(call.request.contract_address, call.calldata));
106 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
107 const Gas start_gas =
108 tx_context.gas_used; // Do not use a const reference as tx_context.gas_used will be modified.
109 auto context = context_provider.make_enqueued_context(call.request.contract_address,
110 call.request.msg_sender,
111 /*transaction_fee=*/FF(0),
112 call.calldata,
113 call.request.is_static_call,
114 gas_limit,
115 start_gas,
117 // This call should not throw unless it's an unexpected unrecoverable failure.
119 tx_context.gas_used = result.gas_used;
122 /*transaction_fee=*/FF(0),
123 result.success,
124 start_gas,
125 tx_context.gas_used,
126 state_before,
127 tx_context.serialize_tx_context_event());
128 if (!result.success) {
129 // This will result in an unprovable tx.
130 throw TxExecutionException(
131 format("[SETUP] UNRECOVERABLE ERROR! Enqueued call to ", call.request.contract_address, " failed"));
132 }
133 }
134 }
135
136 // The checkpoint we should go back to if anything from now on reverts.
139
140 // Let the metadata collector know that we are entering the APP_LOGIC phase.
142
143 try {
144 // Insert revertibles. This can throw if there is a nullifier collision.
145 // Such an exception should be handled and the tx be provable.
146 // We catch separately here to record the revert reason in call stack metadata,
147 // since no calls have populated the metadata yet at this point.
148 try {
150 } catch (const TxExecutionException& e) {
152 throw;
153 }
154
155 // App Logic.
156 if (tx.app_logic_enqueued_calls.empty()) {
158 } else {
159 for (const auto& call : tx.app_logic_enqueued_calls) {
160 vinfo("[APP_LOGIC] Executing enqueued call to ",
161 call.request.contract_address,
162 "::",
163 get_debug_function_name(call.request.contract_address, call.calldata));
164 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
165 const Gas start_gas =
166 tx_context.gas_used; // Do not use a const reference as tx_context.gas_used will be modified.
167
168 auto context = context_provider.make_enqueued_context(call.request.contract_address,
169 call.request.msg_sender,
170 /*transaction_fee=*/FF(0),
171 call.calldata,
172 call.request.is_static_call,
173 gas_limit,
174 start_gas,
176 // This call should not throw unless it's an unexpected unrecoverable failure.
178 tx_context.gas_used = result.gas_used;
179
182 /*transaction_fee=*/FF(0),
183 result.success,
184 start_gas,
185 tx_context.gas_used,
186 state_before,
187 tx_context.serialize_tx_context_event());
188 if (!result.success) {
189 // This exception should be handled, and the tx should be provable.
190 throw TxExecutionException(
191 format("[APP_LOGIC] Enqueued call to ", call.request.contract_address, " failed"));
192 }
193 }
194 }
195 } catch (const TxExecutionException& e) {
196 vinfo("Revertible failure while simulating tx ", tx.hash, ": ", e.what());
198 // We revert to the post-setup state.
201 // But we also create a new fork so that the teardown phase can transparently
202 // commit or rollback to the end of teardown.
205 }
206
207 // Let the metadata collector know that we are entering the teardown phase.
209
210 // Compute the transaction fee here so it can be passed to teardown.
211 const uint128_t& fee_per_da_gas = tx.effective_gas_fees.fee_per_da_gas;
212 const uint128_t& fee_per_l2_gas = tx.effective_gas_fees.fee_per_l2_gas;
213 const FF fee =
214 FF(fee_per_da_gas) * FF(tx_context.gas_used.da_gas) + FF(fee_per_l2_gas) * FF(tx_context.gas_used.l2_gas);
215 Gas gas_used_by_teardown = { 0, 0 };
216
217 // Teardown.
218 try {
219 if (!tx.teardown_enqueued_call.has_value()) {
221 } else {
222 const auto& teardown_enqueued_call = tx.teardown_enqueued_call.value();
223 vinfo("[TEARDOWN] Executing enqueued call to ",
224 teardown_enqueued_call.request.contract_address,
225 "::",
226 get_debug_function_name(teardown_enqueued_call.request.contract_address,
227 teardown_enqueued_call.calldata));
228 // Teardown has its own gas limit and usage.
229 constexpr Gas start_gas = { 0, 0 };
230 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
231 auto context = context_provider.make_enqueued_context(teardown_enqueued_call.request.contract_address,
232 teardown_enqueued_call.request.msg_sender,
233 fee,
234 teardown_enqueued_call.calldata,
235 teardown_enqueued_call.request.is_static_call,
236 teardown_gas_limit,
237 start_gas,
239 // This call should not throw unless it's an unexpected unrecoverable failure.
241 gas_used_by_teardown = result.gas_used;
242 emit_public_call_request(teardown_enqueued_call,
244 fee,
245 result.success,
246 start_gas,
247 result.gas_used,
248 state_before,
249 tx_context.serialize_tx_context_event());
250 if (!result.success) {
251 // This exception should be handled, and the tx should be provable.
252 throw TxExecutionException(
253 format("[TEARDOWN] Enqueued call to ", teardown_enqueued_call.request.contract_address, " failed"));
254 }
255 }
256
257 // We commit the forked state and we are done.
260 } catch (const TxExecutionException& e) {
261 info("Teardown failure while simulating tx ", tx.hash, ": ", e.what());
262 tx_context.revert_code = tx_context.revert_code == RevertCode::APP_LOGIC_REVERTED
265 // We rollback to the post-setup state.
268 }
269
270 // Fee payment
271 pay_fee(tx.fee_payer, fee, fee_per_da_gas, fee_per_l2_gas);
272
273 pad_trees();
274
275 cleanup();
276
277 return {
278 .gas_used = {
279 // Follows PublicTxContext.getActualGasUsed()
280 .total_gas = tx_context.gas_used + (tx.teardown_enqueued_call ? (gas_used_by_teardown - teardown_gas_limit) : Gas {0, 0}),
281 .teardown_gas = gas_used_by_teardown,
282 // Follows PublicTxContext.getActualPublicGasUsed()
283 .public_gas = tx_context.gas_used + gas_used_by_teardown - tx.gas_used_by_private,
284 // Follows PublicTxContext.getTotalGasUsed()
285 .billed_gas = tx_context.gas_used,
286 },
287 .revert_code = tx_context.revert_code,
288 .transaction_fee = fee,
289 };
290}
291
306 TransactionPhase phase,
307 const FF& transaction_fee,
308 bool success,
309 const Gas& start_gas,
310 const Gas& end_gas,
311 const TxContextEvent& state_before,
312 const TxContextEvent& state_after)
313{
314 events.emit(TxPhaseEvent{ .phase = phase,
315 .state_before = state_before,
316 .state_after = state_after,
317 .reverted = !success,
318 .event = EnqueuedCallEvent{
320 .contract_address = call.request.contract_address,
321 .transaction_fee = transaction_fee,
322 .is_static = call.request.is_static_call,
323 .calldata_size = static_cast<uint32_t>(call.calldata.size()),
324 .calldata_hash = call.request.calldata_hash,
325 .start_gas = start_gas,
326 .end_gas = end_gas,
327 .success = success,
328 } });
329}
330
339void TxExecution::emit_nullifier(bool revertible, const FF& nullifier)
340{
341 const TransactionPhase phase =
343 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
344 try {
345 uint32_t prev_nullifier_count = merkle_db.get_tree_state().nullifier_tree.counter;
346
347 if (prev_nullifier_count == MAX_NULLIFIERS_PER_TX) {
348 throw TxExecutionException("Maximum number of nullifiers reached");
349 }
350
351 try {
353 } catch (const NullifierCollisionException& e) {
354 throw TxExecutionException(e.what());
355 }
356
357 events.emit(TxPhaseEvent{ .phase = phase,
358 .state_before = state_before,
359 .state_after = tx_context.serialize_tx_context_event(),
360 .reverted = false,
362
363 } catch (const TxExecutionException& e) {
365 .phase = phase,
366 .state_before = state_before,
367 .state_after = tx_context.serialize_tx_context_event(),
368 .reverted = true,
370 });
371 // Rethrow the error.
372 throw e;
373 }
374}
375
384void TxExecution::emit_note_hash(bool revertible, const FF& note_hash)
385{
386 const TransactionPhase phase =
388 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
389
390 try {
391 uint32_t prev_note_hash_count = merkle_db.get_tree_state().note_hash_tree.counter;
392
393 if (prev_note_hash_count == MAX_NOTE_HASHES_PER_TX) {
394 throw TxExecutionException("Maximum number of note hashes reached");
395 }
396
397 if (revertible) {
399 } else {
401 }
402
403 events.emit(TxPhaseEvent{ .phase = phase,
404 .state_before = state_before,
405 .state_after = tx_context.serialize_tx_context_event(),
406 .reverted = false,
408 } catch (const TxExecutionException& e) {
409 events.emit(TxPhaseEvent{ .phase = phase,
410 .state_before = state_before,
411 .state_after = tx_context.serialize_tx_context_event(),
412 .reverted = true,
414 // Rethrow the error.
415 throw e;
416 }
417}
418
427void TxExecution::emit_l2_to_l1_message(bool revertible, const ScopedL2ToL1Message& l2_to_l1_message)
428{
429 const TransactionPhase phase =
431 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
432 auto& side_effect_tracker = tx_context.side_effect_tracker;
433 const auto& side_effects = side_effect_tracker.get_side_effects();
434
435 try {
436 if (side_effects.l2_to_l1_messages.size() == MAX_L2_TO_L1_MSGS_PER_TX) {
437 throw TxExecutionException("Maximum number of L2 to L1 messages reached");
438 }
439 side_effect_tracker.add_l2_to_l1_message(
440 l2_to_l1_message.contract_address, l2_to_l1_message.message.recipient, l2_to_l1_message.message.content);
441 events.emit(TxPhaseEvent{ .phase = phase,
442 .state_before = state_before,
443 .state_after = tx_context.serialize_tx_context_event(),
444 .reverted = false,
445 .event = PrivateEmitL2L1MessageEvent{ .scoped_msg = l2_to_l1_message } });
446 } catch (const TxExecutionException& e) {
447 events.emit(TxPhaseEvent{ .phase = phase,
448 .state_before = state_before,
449 .state_after = tx_context.serialize_tx_context_event(),
450 .reverted = true,
451 .event = PrivateEmitL2L1MessageEvent{ .scoped_msg = l2_to_l1_message } });
452 // Rethrow the error.
453 throw e;
454 }
455}
456
467{
468 vinfo("[NON_REVERTIBLE] Inserting ",
469 tx.non_revertible_accumulated_data.nullifiers.size(),
470 " nullifiers, ",
471 tx.non_revertible_accumulated_data.note_hashes.size(),
472 " note hashes, and ",
473 tx.non_revertible_accumulated_data.l2_to_l1_messages.size(),
474 " L2 to L1 messages for tx ",
475 tx.hash);
476
477 // 1. Write the already siloed nullifiers.
478 if (tx.non_revertible_accumulated_data.nullifiers.empty()) {
480 } else {
481 for (const auto& nullifier : tx.non_revertible_accumulated_data.nullifiers) {
483 }
484 }
485
486 // 2. Write already unique note hashes.
487 if (tx.non_revertible_accumulated_data.note_hashes.empty()) {
489 } else {
490 for (const auto& unique_note_hash : tx.non_revertible_accumulated_data.note_hashes) {
491 emit_note_hash(false, unique_note_hash);
492 }
493 }
494
495 // 3. Write L2 to L1 messages.
496 if (tx.non_revertible_accumulated_data.l2_to_l1_messages.empty()) {
498 } else {
499 for (const auto& l2_to_l1_msg : tx.non_revertible_accumulated_data.l2_to_l1_messages) {
500 emit_l2_to_l1_message(false, l2_to_l1_msg);
501 }
502 }
503
504 // Add new contracts to the contracts DB so that their code may be found and called.
505 contract_db.add_contracts(tx.non_revertible_contract_deployment_data);
506}
507
517{
518 vinfo("[REVERTIBLE] Inserting ",
519 tx.revertible_accumulated_data.nullifiers.size(),
520 " nullifiers, ",
521 tx.revertible_accumulated_data.note_hashes.size(),
522 " note hashes, and ",
523 tx.revertible_accumulated_data.l2_to_l1_messages.size(),
524 " L2 to L1 messages for tx ",
525 tx.hash);
526
527 // 1. Write the already siloed nullifiers.
528 if (tx.revertible_accumulated_data.nullifiers.empty()) {
530 } else {
531 for (const auto& siloed_nullifier : tx.revertible_accumulated_data.nullifiers) {
532 emit_nullifier(true, siloed_nullifier);
533 }
534 }
535
536 // 2. Write the siloed non-unique note hashes.
537 if (tx.revertible_accumulated_data.note_hashes.empty()) {
539 } else {
540 for (const auto& siloed_note_hash : tx.revertible_accumulated_data.note_hashes) {
541 emit_note_hash(true, siloed_note_hash);
542 }
543 }
544
545 // 3. Write L2 to L1 messages.
546 if (tx.revertible_accumulated_data.l2_to_l1_messages.empty()) {
548 } else {
549 for (const auto& l2_to_l1_msg : tx.revertible_accumulated_data.l2_to_l1_messages) {
550 emit_l2_to_l1_message(true, l2_to_l1_msg);
551 }
552 }
553
554 // Add new contracts to the contracts DB so that their functions may be found and called.
555 contract_db.add_contracts(tx.revertible_contract_deployment_data);
556}
557
568void TxExecution::pay_fee(const AztecAddress& fee_payer,
569 const FF& fee,
570 const uint128_t& fee_per_da_gas,
571 const uint128_t& fee_per_l2_gas)
572{
573 if (fee_payer == 0) {
575 vinfo("Fee payer is 0. Skipping fee enforcement. No one is paying the fee of ", fee);
576 return;
577 }
578 // Real transactions are enforced by the private kernel to have a non-zero fee payer.
579 // Real transactions cannot skip fee enforcement (skipping fee enforcement makes them unprovable).
580 // Unrecoverable error.
581 throw TxExecutionException("Fee payer cannot be 0 unless skipping fee enforcement for simulation");
582 }
583
584 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
585 const FF fee_juice_balance_slot = poseidon2.hash({ FEE_JUICE_BALANCES_SLOT, fee_payer });
586 FF fee_payer_balance = merkle_db.storage_read(FEE_JUICE_ADDRESS, fee_juice_balance_slot);
587
588 if (field_gt.ff_gt(fee, fee_payer_balance)) {
590 vinfo("Fee payer balance insufficient, but we're skipping fee enforcement");
591 // We still proceed and perform the storage write to minimize deviation from normal execution.
592 fee_payer_balance = fee;
593 } else {
594 // Without "skipFeeEnforcement", such transactions should be filtered by GasTxValidator.
595 // Unrecoverable error.
596 throw TxExecutionException("Not enough balance for fee payer to pay for transaction");
597 }
598 }
599
600 merkle_db.storage_write(FEE_JUICE_ADDRESS, fee_juice_balance_slot, fee_payer_balance - fee, true);
601
603 .state_before = state_before,
604 .state_after = tx_context.serialize_tx_context_event(),
605 .reverted = false,
607 .effective_fee_per_da_gas = fee_per_da_gas,
608 .effective_fee_per_l2_gas = fee_per_l2_gas,
609 .fee_payer = fee_payer,
610 .fee_payer_balance = fee_payer_balance,
611 .fee_juice_balance_slot = fee_juice_balance_slot,
612 .fee = fee,
613 } });
614}
615
621{
622 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
625 .state_before = state_before,
626 .state_after = tx_context.serialize_tx_context_event(),
627 .reverted = false,
628 .event = PadTreesEvent{} });
629}
630
636{
637 const TxContextEvent current_state = tx_context.serialize_tx_context_event();
639 .state_before = current_state,
640 .state_after = current_state,
641 .reverted = false,
642 .event = CleanupEvent{} });
643}
644
653{
654 const TxContextEvent current_state = tx_context.serialize_tx_context_event();
655 events.emit(TxPhaseEvent{ .phase = phase,
656 .state_before = current_state,
657 .state_after = current_state,
658 .reverted = false,
659 .event = EmptyPhaseEvent{} });
660}
661
670{
671 // Public function is dispatched, and therefore the target function is passed in the first argument.
672 if (calldata.empty()) {
673 return format("<calldata[0] undefined> (Contract Address: ", contract_address, ")");
674 }
675
676 const FF& selector = calldata[0];
678
679 if (debug_name.has_value()) {
680 return debug_name.value();
681 }
682
683 // Return selector as hex string if debug name is not found.
684 return format("<selector: ", selector, ">");
685}
686
687} // namespace bb::avm2::simulation
std::shared_ptr< Napi::ThreadSafeFunction > debug_name
#define FEE_JUICE_ADDRESS
#define MAX_L2_TO_L1_MSGS_PER_TX
#define MAX_NOTE_HASHES_PER_TX
#define FEE_JUICE_BALANCES_SLOT
#define MAX_NULLIFIERS_PER_TX
virtual void set_phase(CoarseTransactionPhase phase)=0
virtual void notify_tx_revert(const std::string &revert_message)=0
virtual std::unique_ptr< ContextInterface > make_enqueued_context(AztecAddress address, AztecAddress msg_sender, FF transaction_fee, std::span< const FF > calldata, bool is_static, Gas gas_limit, Gas gas_used, TransactionPhase phase)=0
virtual void add_contracts(const ContractDeploymentData &contract_deployment_data)=0
virtual std::optional< std::string > get_debug_function_name(const AztecAddress &address, const FunctionSelector &selector) const =0
virtual void emit(Event &&event)=0
virtual EnqueuedCallResult execute(std::unique_ptr< ContextInterface > context)=0
virtual bool ff_gt(const FF &a, const FF &b)=0
virtual void unique_note_hash_write(const FF &note_hash)=0
virtual FF storage_read(const AztecAddress &contract_address, const FF &slot) const =0
virtual void siloed_note_hash_write(const FF &note_hash)=0
virtual void storage_write(const AztecAddress &contract_address, const FF &slot, const FF &value, bool is_protocol_write)=0
virtual void siloed_nullifier_write(const FF &nullifier)=0
virtual TreeStates get_tree_state() const =0
void cleanup()
Emit a TxPhaseEvent event with the embedded event type CleanupEvent. This is used to finalize the acc...
void emit_public_call_request(const PublicCallRequestWithCalldata &call, TransactionPhase phase, const FF &transaction_fee, bool success, const Gas &start_gas, const Gas &end_gas, const TxContextEvent &state_before, const TxContextEvent &state_after)
Handle a public call request and emit a TxPhaseEvent event with the embedded event type EnqueuedCallE...
TxExecutionResult simulate(const Tx &tx)
Simulates the entire transaction execution phases.
void insert_revertibles(const Tx &tx)
Insert the revertible accumulated data into the Merkle DB and emit corresponding events....
FieldGreaterThanInterface & field_gt
void pad_trees()
Pad the note hash and nullifier trees and emit a TxPhaseEvent event with the embedded event type PadT...
std::string get_debug_function_name(const AztecAddress &contract_address, const std::vector< FF > &calldata)
Get the debug function name for a given contract address and calldata.
void emit_empty_phase(TransactionPhase phase)
Emit a TxPhaseEvent event with the embedded event type EmptyPhaseEvent. This is used to indicate that...
void insert_non_revertibles(const Tx &tx)
Insert the non-revertible accumulated data into the Merkle DB and emit corresponding events....
HighLevelMerkleDBInterface & merkle_db
EventEmitterInterface< TxEvent > & events
void pay_fee(const AztecAddress &fee_payer, const FF &fee, const uint128_t &fee_per_da_gas, const uint128_t &fee_per_l2_gas)
Pay the fee for the transaction and emit a TxPhaseEvent event with the embedded event type CollectGas...
void emit_nullifier(bool revertible, const FF &nullifier)
Handle a nullifier insertion and emit a TxPhaseEvent event with the embedded event type PrivateAppend...
ContractDBInterface & contract_db
void emit_note_hash(bool revertible, const FF &note_hash)
Handle a note hash insertion and emit a TxPhaseEvent event with the embedded event type PrivateAppend...
ContextProviderInterface & context_provider
void emit_l2_to_l1_message(bool revertible, const ScopedL2ToL1Message &l2_to_l1_message)
Handle an L2 to L1 message insertion and emit a TxPhaseEvent event with the embedded event type Priva...
CallStackMetadataCollectorInterface & call_stack_metadata_collector
ExecutionInterface & call_execution
static FF hash(const std::vector< FF > &input)
Hashes a vector of field elements.
std::string format(Args... args)
Definition log.hpp:22
#define vinfo(...)
Definition log.hpp:80
void info(Args... args)
Definition log.hpp:75
AvmFlavorSettings::FF FF
Definition field.hpp:10
STL namespace.
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
simulation::PublicDataTreeReadWriteEvent event
unsigned __int128 uint128_t
Definition serialize.hpp:44
static PhaseLengths from_tx(const Tx &tx)
Definition tx_events.hpp:24
SideEffectTracker side_effect_tracker