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 BB_BENCH_NAME("TxExecution::simulate");
65
66 const Gas& gas_limit = tx.gas_settings.gas_limits;
67 const Gas& teardown_gas_limit = tx.gas_settings.teardown_gas_limits;
68 tx_context.gas_used = tx.gas_used_by_private;
69
71 .gas_used = tx_context.gas_used,
72 .gas_limit = gas_limit,
73 .teardown_gas_limit = teardown_gas_limit,
74 .phase_lengths = PhaseLengths::from_tx(tx), // Extract lengths of each phase at start.
75 });
76
77 vinfo("Simulating tx ",
78 tx.hash,
79 " with ",
80 tx.setup_enqueued_calls.size(),
81 " setup enqueued calls, ",
82 tx.app_logic_enqueued_calls.size(),
83 " app logic enqueued calls, and ",
84 tx.teardown_enqueued_call.has_value() ? "1 teardown enqueued call" : "no teardown enqueued call");
85
86 // Let the metadata collector know that we are entering the SETUP phase.
88
89 // Insert non-revertibles. This can throw if there is a nullifier collision or the maximum number of
90 // nullifiers, note hashes, or L2 to L1 messages is reached.
91 // That would result in an unprovable tx.
93
94 // Setup.
95 if (tx.setup_enqueued_calls.empty()) {
97 } else {
98 for (const auto& call : tx.setup_enqueued_calls) {
99 vinfo("[SETUP] Executing enqueued call to ",
100 call.request.contract_address,
101 "::",
102 get_debug_function_name(call.request.contract_address, call.calldata));
103 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
104 const Gas start_gas =
105 tx_context.gas_used; // Do not use a const reference as tx_context.gas_used will be modified.
106 auto context = context_provider.make_enqueued_context(call.request.contract_address,
107 call.request.msg_sender,
108 /*transaction_fee=*/FF(0),
109 call.calldata,
110 call.request.calldata_hash,
111 call.request.is_static_call,
112 gas_limit,
113 start_gas,
115 // This call should not throw unless it's an unexpected unrecoverable failure.
117 tx_context.gas_used = result.gas_used;
120 /*transaction_fee=*/FF(0),
121 result.success,
122 start_gas,
123 tx_context.gas_used,
124 state_before,
125 tx_context.serialize_tx_context_event());
126 if (!result.success) {
127 // This will result in an unprovable tx.
128 throw TxExecutionException(
129 format("[SETUP] UNRECOVERABLE ERROR! Enqueued call to ", call.request.contract_address, " failed"));
130 }
131 }
132 }
133
134 // The checkpoint we should go back to if anything from now on reverts.
137
138 // Let the metadata collector know that we are entering the APP_LOGIC phase.
140
141 try {
142 // Insert revertibles. This can throw if there is a nullifier collision.
143 // Such an exception should be handled and the tx be provable.
144 // We catch separately here to record the revert reason in call stack metadata,
145 // since no calls have populated the metadata yet at this point.
146 try {
148 } catch (const TxExecutionException& e) {
150 throw;
151 }
152
153 // App Logic.
154 if (tx.app_logic_enqueued_calls.empty()) {
156 } else {
157 for (const auto& call : tx.app_logic_enqueued_calls) {
158 vinfo("[APP_LOGIC] Executing enqueued call to ",
159 call.request.contract_address,
160 "::",
161 get_debug_function_name(call.request.contract_address, call.calldata));
162 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
163 const Gas start_gas =
164 tx_context.gas_used; // Do not use a const reference as tx_context.gas_used will be modified.
165
166 auto context = context_provider.make_enqueued_context(call.request.contract_address,
167 call.request.msg_sender,
168 /*transaction_fee=*/FF(0),
169 call.calldata,
170 call.request.calldata_hash,
171 call.request.is_static_call,
172 gas_limit,
173 start_gas,
175 // This call should not throw unless it's an unexpected unrecoverable failure.
177 tx_context.gas_used = result.gas_used;
178
181 /*transaction_fee=*/FF(0),
182 result.success,
183 start_gas,
184 tx_context.gas_used,
185 state_before,
186 tx_context.serialize_tx_context_event());
187 if (!result.success) {
188 // This exception should be handled, and the tx should be provable.
189 throw TxExecutionException(
190 format("[APP_LOGIC] Enqueued call to ", call.request.contract_address, " failed"));
191 }
192 }
193 }
194 } catch (const TxExecutionException& e) {
195 vinfo("Revertible failure while simulating tx ", tx.hash, ": ", e.what());
197 // We revert to the post-setup state.
200 // But we also create a new fork so that the teardown phase can transparently
201 // commit or rollback to the end of teardown.
204 }
205
206 // Let the metadata collector know that we are entering the teardown phase.
208
209 // Compute the transaction fee here so it can be passed to teardown.
210 const uint128_t& fee_per_da_gas = tx.effective_gas_fees.fee_per_da_gas;
211 const uint128_t& fee_per_l2_gas = tx.effective_gas_fees.fee_per_l2_gas;
212 const FF fee =
213 FF(fee_per_da_gas) * FF(tx_context.gas_used.da_gas) + FF(fee_per_l2_gas) * FF(tx_context.gas_used.l2_gas);
214 Gas gas_used_by_teardown = { 0, 0 };
215
216 // Teardown.
217 try {
218 if (!tx.teardown_enqueued_call.has_value()) {
220 } else {
221 const auto& teardown_enqueued_call = tx.teardown_enqueued_call.value();
222 vinfo("[TEARDOWN] Executing enqueued call to ",
223 teardown_enqueued_call.request.contract_address,
224 "::",
225 get_debug_function_name(teardown_enqueued_call.request.contract_address,
226 teardown_enqueued_call.calldata));
227 // Teardown has its own gas limit and usage.
228 constexpr Gas start_gas = { 0, 0 };
229 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
230 auto context = context_provider.make_enqueued_context(teardown_enqueued_call.request.contract_address,
231 teardown_enqueued_call.request.msg_sender,
232 fee,
233 teardown_enqueued_call.calldata,
234 teardown_enqueued_call.request.calldata_hash,
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 // TODO(fcarreiro): move these back to important log once/if we have log levels properly set up.
262 vinfo("Teardown failure while simulating tx ", tx.hash, ": ", e.what());
263 tx_context.revert_code = tx_context.revert_code == RevertCode::APP_LOGIC_REVERTED
266 // We rollback to the post-setup state.
269 }
270
271 // Fee payment
272 pay_fee(tx.fee_payer, fee, fee_per_da_gas, fee_per_l2_gas);
273
274 pad_trees();
275
276 cleanup();
277
278 return {
279 .gas_used = {
280 // Follows PublicTxContext.getActualGasUsed()
281 .total_gas = tx_context.gas_used + (tx.teardown_enqueued_call ? (gas_used_by_teardown - teardown_gas_limit) : Gas {0, 0}),
282 .teardown_gas = gas_used_by_teardown,
283 // Follows PublicTxContext.getActualPublicGasUsed()
284 .public_gas = tx_context.gas_used + gas_used_by_teardown - tx.gas_used_by_private,
285 // Follows PublicTxContext.getTotalGasUsed()
286 .billed_gas = tx_context.gas_used,
287 },
288 .revert_code = tx_context.revert_code,
289 .transaction_fee = fee,
290 };
291}
292
307 TransactionPhase phase,
308 const FF& transaction_fee,
309 bool success,
310 const Gas& start_gas,
311 const Gas& end_gas,
312 const TxContextEvent& state_before,
313 const TxContextEvent& state_after)
314{
315 events.emit(TxPhaseEvent{ .phase = phase,
316 .state_before = state_before,
317 .state_after = state_after,
318 .reverted = !success,
319 .event = EnqueuedCallEvent{
321 .contract_address = call.request.contract_address,
322 .transaction_fee = transaction_fee,
323 .is_static = call.request.is_static_call,
324 .calldata_size = static_cast<uint32_t>(call.calldata.size()),
325 .calldata_hash = call.request.calldata_hash,
326 .start_gas = start_gas,
327 .end_gas = end_gas,
328 .success = success,
329 } });
330}
331
340void TxExecution::emit_nullifier(bool revertible, const FF& nullifier)
341{
342 const TransactionPhase phase =
344 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
345 try {
346 uint32_t prev_nullifier_count = merkle_db.get_tree_state().nullifier_tree.counter;
347
348 if (prev_nullifier_count == MAX_NULLIFIERS_PER_TX) {
349 throw TxExecutionException("Maximum number of nullifiers reached");
350 }
351
352 try {
354 } catch (const NullifierCollisionException& e) {
355 throw TxExecutionException(e.what());
356 }
357
358 events.emit(TxPhaseEvent{ .phase = phase,
359 .state_before = state_before,
360 .state_after = tx_context.serialize_tx_context_event(),
361 .reverted = false,
363
364 } catch (const TxExecutionException& e) {
366 .phase = phase,
367 .state_before = state_before,
368 .state_after = tx_context.serialize_tx_context_event(),
369 .reverted = true,
371 });
372 // Rethrow the error.
373 throw e;
374 }
375}
376
385void TxExecution::emit_note_hash(bool revertible, const FF& note_hash)
386{
387 const TransactionPhase phase =
389 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
390
391 try {
392 uint32_t prev_note_hash_count = merkle_db.get_tree_state().note_hash_tree.counter;
393
394 if (prev_note_hash_count == MAX_NOTE_HASHES_PER_TX) {
395 throw TxExecutionException("Maximum number of note hashes reached");
396 }
397
398 if (revertible) {
400 } else {
402 }
403
404 events.emit(TxPhaseEvent{ .phase = phase,
405 .state_before = state_before,
406 .state_after = tx_context.serialize_tx_context_event(),
407 .reverted = false,
409 } catch (const TxExecutionException& e) {
410 events.emit(TxPhaseEvent{ .phase = phase,
411 .state_before = state_before,
412 .state_after = tx_context.serialize_tx_context_event(),
413 .reverted = true,
415 // Rethrow the error.
416 throw e;
417 }
418}
419
428void TxExecution::emit_l2_to_l1_message(bool revertible, const ScopedL2ToL1Message& l2_to_l1_message)
429{
430 const TransactionPhase phase =
432 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
433 auto& side_effect_tracker = tx_context.side_effect_tracker;
434 const auto& side_effects = side_effect_tracker.get_side_effects();
435
436 try {
437 if (side_effects.l2_to_l1_messages.size() == MAX_L2_TO_L1_MSGS_PER_TX) {
438 throw TxExecutionException("Maximum number of L2 to L1 messages reached");
439 }
440 side_effect_tracker.add_l2_to_l1_message(
441 l2_to_l1_message.contract_address, l2_to_l1_message.message.recipient, l2_to_l1_message.message.content);
442 events.emit(TxPhaseEvent{ .phase = phase,
443 .state_before = state_before,
444 .state_after = tx_context.serialize_tx_context_event(),
445 .reverted = false,
446 .event = PrivateEmitL2L1MessageEvent{ .scoped_msg = l2_to_l1_message } });
447 } catch (const TxExecutionException& e) {
448 events.emit(TxPhaseEvent{ .phase = phase,
449 .state_before = state_before,
450 .state_after = tx_context.serialize_tx_context_event(),
451 .reverted = true,
452 .event = PrivateEmitL2L1MessageEvent{ .scoped_msg = l2_to_l1_message } });
453 // Rethrow the error.
454 throw e;
455 }
456}
457
468{
469 BB_BENCH_NAME("TxExecution::insert_non_revertibles");
470
471 vinfo("[NON_REVERTIBLE] Inserting ",
472 tx.non_revertible_accumulated_data.nullifiers.size(),
473 " nullifiers, ",
474 tx.non_revertible_accumulated_data.note_hashes.size(),
475 " note hashes, and ",
476 tx.non_revertible_accumulated_data.l2_to_l1_messages.size(),
477 " L2 to L1 messages for tx ",
478 tx.hash);
479
480 // 1. Write the already siloed nullifiers.
481 if (tx.non_revertible_accumulated_data.nullifiers.empty()) {
483 } else {
484 for (const auto& nullifier : tx.non_revertible_accumulated_data.nullifiers) {
486 }
487 }
488
489 // 2. Write already unique note hashes.
490 if (tx.non_revertible_accumulated_data.note_hashes.empty()) {
492 } else {
493 for (const auto& unique_note_hash : tx.non_revertible_accumulated_data.note_hashes) {
494 emit_note_hash(false, unique_note_hash);
495 }
496 }
497
498 // 3. Write L2 to L1 messages.
499 if (tx.non_revertible_accumulated_data.l2_to_l1_messages.empty()) {
501 } else {
502 for (const auto& l2_to_l1_msg : tx.non_revertible_accumulated_data.l2_to_l1_messages) {
503 emit_l2_to_l1_message(false, l2_to_l1_msg);
504 }
505 }
506
507 // Add new contracts to the contracts DB so that their code may be found and called.
508 contract_db.add_contracts(tx.non_revertible_contract_deployment_data);
509}
510
520{
521 BB_BENCH_NAME("TxExecution::insert_revertibles");
522
523 vinfo("[REVERTIBLE] Inserting ",
524 tx.revertible_accumulated_data.nullifiers.size(),
525 " nullifiers, ",
526 tx.revertible_accumulated_data.note_hashes.size(),
527 " note hashes, and ",
528 tx.revertible_accumulated_data.l2_to_l1_messages.size(),
529 " L2 to L1 messages for tx ",
530 tx.hash);
531
532 // 1. Write the already siloed nullifiers.
533 if (tx.revertible_accumulated_data.nullifiers.empty()) {
535 } else {
536 for (const auto& siloed_nullifier : tx.revertible_accumulated_data.nullifiers) {
537 emit_nullifier(true, siloed_nullifier);
538 }
539 }
540
541 // 2. Write the siloed non-unique note hashes.
542 if (tx.revertible_accumulated_data.note_hashes.empty()) {
544 } else {
545 for (const auto& siloed_note_hash : tx.revertible_accumulated_data.note_hashes) {
546 emit_note_hash(true, siloed_note_hash);
547 }
548 }
549
550 // 3. Write L2 to L1 messages.
551 if (tx.revertible_accumulated_data.l2_to_l1_messages.empty()) {
553 } else {
554 for (const auto& l2_to_l1_msg : tx.revertible_accumulated_data.l2_to_l1_messages) {
555 emit_l2_to_l1_message(true, l2_to_l1_msg);
556 }
557 }
558
559 // Add new contracts to the contracts DB so that their functions may be found and called.
560 contract_db.add_contracts(tx.revertible_contract_deployment_data);
561}
562
573void TxExecution::pay_fee(const AztecAddress& fee_payer,
574 const FF& fee,
575 const uint128_t& fee_per_da_gas,
576 const uint128_t& fee_per_l2_gas)
577{
578 BB_BENCH_NAME("TxExecution::pay_fee");
579
580 if (fee_payer == 0) {
582 vinfo("Fee payer is 0. Skipping fee enforcement. No one is paying the fee of ", fee);
583 return;
584 }
585 // Real transactions are enforced by the private kernel to have a non-zero fee payer.
586 // Real transactions cannot skip fee enforcement (skipping fee enforcement makes them unprovable).
587 // Unrecoverable error.
588 throw TxExecutionException("Fee payer cannot be 0 unless skipping fee enforcement for simulation");
589 }
590
591 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
592 const FF fee_juice_balance_slot = poseidon2.hash({ FEE_JUICE_BALANCES_SLOT, fee_payer });
593 FF fee_payer_balance = merkle_db.storage_read(FEE_JUICE_ADDRESS, fee_juice_balance_slot);
594
595 if (field_gt.ff_gt(fee, fee_payer_balance)) {
597 vinfo("Fee payer balance insufficient, but we're skipping fee enforcement");
598 // We still proceed and perform the storage write to minimize deviation from normal execution.
599 fee_payer_balance = fee;
600 } else {
601 // Without "skipFeeEnforcement", such transactions should be filtered by GasTxValidator.
602 // Unrecoverable error.
603 throw TxExecutionException("Not enough balance for fee payer to pay for transaction");
604 }
605 }
606
607 merkle_db.storage_write(FEE_JUICE_ADDRESS, fee_juice_balance_slot, fee_payer_balance - fee, true);
608
610 .state_before = state_before,
611 .state_after = tx_context.serialize_tx_context_event(),
612 .reverted = false,
614 .effective_fee_per_da_gas = fee_per_da_gas,
615 .effective_fee_per_l2_gas = fee_per_l2_gas,
616 .fee_payer = fee_payer,
617 .fee_payer_balance = fee_payer_balance,
618 .fee_juice_balance_slot = fee_juice_balance_slot,
619 .fee = fee,
620 } });
621}
622
628{
629 BB_BENCH_NAME("TxExecution::pad_trees");
630
631 const TxContextEvent state_before = tx_context.serialize_tx_context_event();
634 .state_before = state_before,
635 .state_after = tx_context.serialize_tx_context_event(),
636 .reverted = false,
637 .event = PadTreesEvent{} });
638}
639
645{
646 const TxContextEvent current_state = tx_context.serialize_tx_context_event();
648 .state_before = current_state,
649 .state_after = current_state,
650 .reverted = false,
651 .event = CleanupEvent{} });
652}
653
662{
663 const TxContextEvent current_state = tx_context.serialize_tx_context_event();
664 events.emit(TxPhaseEvent{ .phase = phase,
665 .state_before = current_state,
666 .state_after = current_state,
667 .reverted = false,
668 .event = EmptyPhaseEvent{} });
669}
670
679{
680 // Public function is dispatched, and therefore the target function is passed in the first argument.
681 if (calldata.empty()) {
682 return format("<calldata[0] undefined> (Contract Address: ", contract_address, ")");
683 }
684
685 const FF& selector = calldata[0];
687
688 if (debug_name.has_value()) {
689 return debug_name.value();
690 }
691
692 // Return selector as hex string if debug name is not found.
693 return format("<selector: ", selector, ">");
694}
695
696} // 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
#define BB_BENCH_NAME(name)
Definition bb_bench.hpp:219
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(const AztecAddress &address, const AztecAddress &msg_sender, const FF &transaction_fee, std::span< const FF > calldata, const FF &calldata_hash, bool is_static, const Gas &gas_limit, const 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:24
#define vinfo(...)
Definition log.hpp:94
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