Skip to content

Commit 78a215d

Browse files
authored
chore: refactor avm tx execution to generate "empty phase" events (#16676)
This simplifies tracegen so that it doesn't need to know about setup snapshots and rollbacks anymore, doesn't need special handling for situations when a phase is empty, and omits rows for phases skipped due to reverts. Note that there are two different relevant scenarios: 1. A phase is just "empty". The phase is encountered normally, but has no contents, like when there are no revertible nullifiers to insert, or when app logic has no enqueued calls to execute. **In this case, the phase gets a padding row**. 2. A phase is "skipped" entirely because of a revert. Like if revertible nullifier insertions fail, app-logic won't ever be encountered. **In this case, app-logic doesn't even get a row.** This PR specifically generates events for scenario 1, when a phase is encountered, but "empty". This "empty" event drives tracegen of a "padding" row. Creation of an event that drives padding rows means that tracegen no longer needs to know about setup snapshots and rollbacks to ensure that a padding row after a revert gets the post-revert state! Because now the post-revert state will be in the `EmptyPhaseEvent`, so tracegen can just generate a row from the phase event as it does for other rows. Also note that this now lets us easily differentiate between "empty" phases versus skipped phases. Now, in tracegen, if a phase has no events, it means that it was _skipped_ because if it was "empty" in simulation, it would've generated an `EmptyPhaseEvent`. So, this PR also stops generating padding rows for _skipped_ phases, which was the originally intended mechanism proposed by @IlyasRidhuan, but it was not implemented that way.
1 parent 614ee22 commit 78a215d

File tree

7 files changed

+165
-130
lines changed

7 files changed

+165
-130
lines changed

barretenberg/cpp/src/barretenberg/vm2/common/aztec_types.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ enum TransactionPhase {
2929
COLLECT_GAS_FEES = 10,
3030
TREE_PADDING = 11,
3131
CLEANUP = 12,
32+
LAST = CLEANUP,
3233
};
3334

3435
using InternalCallId = uint32_t;

barretenberg/cpp/src/barretenberg/vm2/simulation/events/tx_events.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,15 @@ struct PadTreesEvent {};
4848

4949
struct CleanupEvent {};
5050

51+
struct EmptyPhaseEvent {};
52+
5153
using TxPhaseEventType = std::variant<EnqueuedCallEvent,
5254
PrivateAppendTreeEvent,
5355
PrivateEmitL2L1MessageEvent,
5456
CollectGasFeeEvent,
5557
PadTreesEvent,
56-
CleanupEvent>;
58+
CleanupEvent,
59+
EmptyPhaseEvent>;
5760

5861
struct TxPhaseEvent {
5962
TransactionPhase phase;

barretenberg/cpp/src/barretenberg/vm2/simulation/tx_execution.cpp

Lines changed: 105 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -83,50 +83,11 @@ void TxExecution::simulate(const Tx& tx)
8383
insert_non_revertibles(tx);
8484

8585
// Setup.
86-
for (const auto& call : tx.setupEnqueuedCalls) {
87-
info("[SETUP] Executing enqueued call to ", call.request.contractAddress);
88-
TxContextEvent state_before = tx_context.serialize_tx_context_event();
89-
Gas start_gas = tx_context.gas_used;
90-
auto context = context_provider.make_enqueued_context(call.request.contractAddress,
91-
call.request.msgSender,
92-
/*transaction_fee=*/FF(0),
93-
call.calldata,
94-
call.request.isStaticCall,
95-
gas_limit,
96-
start_gas,
97-
tx_context.side_effect_states,
98-
TransactionPhase::SETUP);
99-
// This call should not throw unless it's an unexpected unrecoverable failure.
100-
ExecutionResult result = call_execution.execute(std::move(context));
101-
tx_context.side_effect_states = result.side_effect_states;
102-
tx_context.gas_used = result.gas_used;
103-
emit_public_call_request(call,
104-
TransactionPhase::SETUP,
105-
/*transaction_fee=*/FF(0),
106-
result.success,
107-
start_gas,
108-
tx_context.gas_used,
109-
state_before,
110-
tx_context.serialize_tx_context_event());
111-
if (!result.success) {
112-
// This will result in an unprovable tx.
113-
throw TxExecutionException(
114-
format("[SETUP] UNRECOVERABLE ERROR! Enqueued call to ", call.request.contractAddress, " failed"));
115-
}
116-
}
117-
SideEffectStates end_setup_side_effect_states = tx_context.side_effect_states;
118-
119-
// The checkpoint we should go back to if anything from now on reverts.
120-
merkle_db.create_checkpoint();
121-
122-
try {
123-
// Insert revertibles. This can throw if there is a nullifier collision.
124-
// Such an exception should be handled and the tx be provable.
125-
insert_revertibles(tx);
126-
127-
// App logic.
128-
for (const auto& call : tx.appLogicEnqueuedCalls) {
129-
info("[APP_LOGIC] Executing enqueued call to ", call.request.contractAddress);
86+
if (tx.setupEnqueuedCalls.empty()) {
87+
emit_empty_phase(TransactionPhase::SETUP);
88+
} else {
89+
for (const auto& call : tx.setupEnqueuedCalls) {
90+
info("[SETUP] Executing enqueued call to ", call.request.contractAddress);
13091
TxContextEvent state_before = tx_context.serialize_tx_context_event();
13192
Gas start_gas = tx_context.gas_used;
13293
auto context = context_provider.make_enqueued_context(call.request.contractAddress,
@@ -137,23 +98,70 @@ void TxExecution::simulate(const Tx& tx)
13798
gas_limit,
13899
start_gas,
139100
tx_context.side_effect_states,
140-
TransactionPhase::APP_LOGIC);
101+
TransactionPhase::SETUP);
141102
// This call should not throw unless it's an unexpected unrecoverable failure.
142103
ExecutionResult result = call_execution.execute(std::move(context));
143104
tx_context.side_effect_states = result.side_effect_states;
144105
tx_context.gas_used = result.gas_used;
145106
emit_public_call_request(call,
146-
TransactionPhase::APP_LOGIC,
107+
TransactionPhase::SETUP,
147108
/*transaction_fee=*/FF(0),
148109
result.success,
149110
start_gas,
150111
tx_context.gas_used,
151112
state_before,
152113
tx_context.serialize_tx_context_event());
153114
if (!result.success) {
154-
// This exception should be handled and the tx be provable.
115+
// This will result in an unprovable tx.
155116
throw TxExecutionException(
156-
format("[APP_LOGIC] Enqueued call to ", call.request.contractAddress, " failed"));
117+
format("[SETUP] UNRECOVERABLE ERROR! Enqueued call to ", call.request.contractAddress, " failed"));
118+
}
119+
}
120+
}
121+
SideEffectStates end_setup_side_effect_states = tx_context.side_effect_states;
122+
123+
// The checkpoint we should go back to if anything from now on reverts.
124+
merkle_db.create_checkpoint();
125+
126+
try {
127+
// Insert revertibles. This can throw if there is a nullifier collision.
128+
// Such an exception should be handled and the tx be provable.
129+
insert_revertibles(tx);
130+
131+
// App logic.
132+
if (tx.appLogicEnqueuedCalls.empty()) {
133+
emit_empty_phase(TransactionPhase::APP_LOGIC);
134+
} else {
135+
for (const auto& call : tx.appLogicEnqueuedCalls) {
136+
info("[APP_LOGIC] Executing enqueued call to ", call.request.contractAddress);
137+
TxContextEvent state_before = tx_context.serialize_tx_context_event();
138+
Gas start_gas = tx_context.gas_used;
139+
auto context = context_provider.make_enqueued_context(call.request.contractAddress,
140+
call.request.msgSender,
141+
/*transaction_fee=*/FF(0),
142+
call.calldata,
143+
call.request.isStaticCall,
144+
gas_limit,
145+
start_gas,
146+
tx_context.side_effect_states,
147+
TransactionPhase::APP_LOGIC);
148+
// This call should not throw unless it's an unexpected unrecoverable failure.
149+
ExecutionResult result = call_execution.execute(std::move(context));
150+
tx_context.side_effect_states = result.side_effect_states;
151+
tx_context.gas_used = result.gas_used;
152+
emit_public_call_request(call,
153+
TransactionPhase::APP_LOGIC,
154+
/*transaction_fee=*/FF(0),
155+
result.success,
156+
start_gas,
157+
tx_context.gas_used,
158+
state_before,
159+
tx_context.serialize_tx_context_event());
160+
if (!result.success) {
161+
// This exception should be handled and the tx be provable.
162+
throw TxExecutionException(
163+
format("[APP_LOGIC] Enqueued call to ", call.request.contractAddress, " failed"));
164+
}
157165
}
158166
}
159167
} catch (const TxExecutionException& e) {
@@ -175,7 +183,9 @@ void TxExecution::simulate(const Tx& tx)
175183

176184
// Teardown.
177185
try {
178-
if (tx.teardownEnqueuedCall) {
186+
if (!tx.teardownEnqueuedCall) {
187+
emit_empty_phase(TransactionPhase::TEARDOWN);
188+
} else {
179189
info("[TEARDOWN] Executing enqueued call to ", tx.teardownEnqueuedCall->request.contractAddress);
180190
// Teardown has its own gas limit and usage.
181191
Gas start_gas = { 0, 0 };
@@ -334,18 +344,30 @@ void TxExecution::insert_non_revertibles(const Tx& tx)
334344
tx.hash);
335345

336346
// 1. Write the already siloed nullifiers.
337-
for (const auto& nullifier : tx.nonRevertibleAccumulatedData.nullifiers) {
338-
emit_nullifier(false, nullifier);
347+
if (tx.nonRevertibleAccumulatedData.nullifiers.empty()) {
348+
emit_empty_phase(TransactionPhase::NR_NULLIFIER_INSERTION);
349+
} else {
350+
for (const auto& nullifier : tx.nonRevertibleAccumulatedData.nullifiers) {
351+
emit_nullifier(false, nullifier);
352+
}
339353
}
340354

341355
// 2. Write already unique note hashes.
342-
for (const auto& unique_note_hash : tx.nonRevertibleAccumulatedData.noteHashes) {
343-
emit_note_hash(false, unique_note_hash);
356+
if (tx.nonRevertibleAccumulatedData.noteHashes.empty()) {
357+
emit_empty_phase(TransactionPhase::NR_NOTE_INSERTION);
358+
} else {
359+
for (const auto& unique_note_hash : tx.nonRevertibleAccumulatedData.noteHashes) {
360+
emit_note_hash(false, unique_note_hash);
361+
}
344362
}
345363

346364
// 3. Write l2_l1 messages
347-
for (const auto& l2_to_l1_msg : tx.nonRevertibleAccumulatedData.l2ToL1Messages) {
348-
emit_l2_to_l1_message(false, l2_to_l1_msg);
365+
if (tx.nonRevertibleAccumulatedData.l2ToL1Messages.empty()) {
366+
emit_empty_phase(TransactionPhase::NR_L2_TO_L1_MESSAGE);
367+
} else {
368+
for (const auto& l2_to_l1_msg : tx.nonRevertibleAccumulatedData.l2ToL1Messages) {
369+
emit_l2_to_l1_message(false, l2_to_l1_msg);
370+
}
349371
}
350372
}
351373

@@ -362,18 +384,30 @@ void TxExecution::insert_revertibles(const Tx& tx)
362384
tx.hash);
363385

364386
// 1. Write the already siloed nullifiers.
365-
for (const auto& siloed_nullifier : tx.revertibleAccumulatedData.nullifiers) {
366-
emit_nullifier(true, siloed_nullifier);
387+
if (tx.revertibleAccumulatedData.nullifiers.empty()) {
388+
emit_empty_phase(TransactionPhase::R_NULLIFIER_INSERTION);
389+
} else {
390+
for (const auto& siloed_nullifier : tx.revertibleAccumulatedData.nullifiers) {
391+
emit_nullifier(true, siloed_nullifier);
392+
}
367393
}
368394

369395
// 2. Write the siloed non uniqued note hashes
370-
for (const auto& siloed_note_hash : tx.revertibleAccumulatedData.noteHashes) {
371-
emit_note_hash(true, siloed_note_hash);
396+
if (tx.revertibleAccumulatedData.noteHashes.empty()) {
397+
emit_empty_phase(TransactionPhase::R_NOTE_INSERTION);
398+
} else {
399+
for (const auto& siloed_note_hash : tx.revertibleAccumulatedData.noteHashes) {
400+
emit_note_hash(true, siloed_note_hash);
401+
}
372402
}
373403

374404
// 3. Write L2 to L1 messages.
375-
for (const auto& l2_to_l1_msg : tx.revertibleAccumulatedData.l2ToL1Messages) {
376-
emit_l2_to_l1_message(true, l2_to_l1_msg);
405+
if (tx.revertibleAccumulatedData.l2ToL1Messages.empty()) {
406+
emit_empty_phase(TransactionPhase::R_L2_TO_L1_MESSAGE);
407+
} else {
408+
for (const auto& l2_to_l1_msg : tx.revertibleAccumulatedData.l2ToL1Messages) {
409+
emit_l2_to_l1_message(true, l2_to_l1_msg);
410+
}
377411
}
378412
}
379413

@@ -426,4 +460,14 @@ void TxExecution::cleanup()
426460
.event = CleanupEvent{} });
427461
}
428462

463+
void TxExecution::emit_empty_phase(TransactionPhase phase)
464+
{
465+
TxContextEvent current_state = tx_context.serialize_tx_context_event();
466+
events.emit(TxPhaseEvent{ .phase = phase,
467+
.state_before = current_state,
468+
.state_after = current_state,
469+
.reverted = false,
470+
.event = EmptyPhaseEvent{} });
471+
}
472+
429473
} // namespace bb::avm2::simulation

barretenberg/cpp/src/barretenberg/vm2/simulation/tx_execution.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ class TxExecution final {
6666
void pad_trees();
6767

6868
void cleanup();
69+
70+
void emit_empty_phase(TransactionPhase phase);
6971
};
7072

7173
} // namespace bb::avm2::simulation

barretenberg/cpp/src/barretenberg/vm2/tracegen/tx_trace.cpp

Lines changed: 15 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ template <class... Ts> struct overloaded : Ts... {
2828
// explicit deduction guide (not needed as of C++20)
2929
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
3030

31-
constexpr size_t NUM_PHASES = 12; // See TransactionPhase enum
31+
constexpr size_t NUM_PHASES = static_cast<size_t>(TransactionPhase::LAST);
3232

3333
bool is_revertible(TransactionPhase phase)
3434
{
@@ -529,7 +529,6 @@ void TxTraceBuilder::process(const simulation::EventEmitterInterface<simulation:
529529

530530
// This is the tree state we will use during the "skipped" phases
531531
TxContextEvent propagated_state = startup_event_data.state;
532-
TxContextEvent end_setup_snapshot = startup_event_data.state;
533532
// Used to track the gas limit for the "padded" phases.
534533
Gas current_gas_limit = startup_event_data.gas_limit;
535534
Gas teardown_gas_limit = startup_event_data.teardown_gas_limit;
@@ -538,6 +537,12 @@ void TxTraceBuilder::process(const simulation::EventEmitterInterface<simulation:
538537
// Go through each phase except startup and process the events in the phase
539538
for (uint32_t i = 0; i < NUM_PHASES; i++) {
540539
const auto& phase_events = phase_buckets[i];
540+
if (phase_events.empty()) {
541+
// There will be no events for a phase if it is skipped (jumped over) due to a revert.
542+
// This is different from a phase that has an EmptyPhaseEvent, which is a phase that has no contents to
543+
// process, like when app logic starts but has no enqueued calls.
544+
continue;
545+
}
541546

542547
TransactionPhase phase = phase_array[i];
543548

@@ -555,24 +560,6 @@ void TxTraceBuilder::process(const simulation::EventEmitterInterface<simulation:
555560
current_gas_limit = teardown_gas_limit;
556561
}
557562

558-
if (phase_events.empty()) {
559-
trace.set(row, insert_state(propagated_state, propagated_state));
560-
trace.set(row, handle_padded_row(phase, gas_used, discard));
561-
trace.set(row, handle_pi_read(phase, /*phase_length=*/0, /*read_counter*/ 0));
562-
trace.set(row, handle_prev_gas_used(gas_used));
563-
trace.set(row, handle_next_gas_used(gas_used));
564-
trace.set(row, handle_gas_limit(current_gas_limit));
565-
trace.set(row, handle_state_change_selectors(phase));
566-
if (row == 1) {
567-
trace.set(row, handle_first_row());
568-
}
569-
if (phase == TransactionPhase::SETUP) {
570-
// If setup is empty, the end-setup-snapshot should just be current/propagated state
571-
end_setup_snapshot = propagated_state;
572-
}
573-
row++;
574-
continue;
575-
}
576563
// Count the number of steps in this phase
577564
uint32_t phase_counter = 0;
578565
uint32_t phase_length = static_cast<uint32_t>(phase_events.size());
@@ -591,7 +578,7 @@ void TxTraceBuilder::process(const simulation::EventEmitterInterface<simulation:
591578
{ C::tx_discard, discard ? 1 : 0 },
592579
{ C::tx_phase_value, static_cast<uint8_t>(tx_phase_event->phase) },
593580
{ Column::tx_setup_phase_value, static_cast<uint8_t>(TransactionPhase::SETUP) },
594-
{ C::tx_is_padded, 0 },
581+
{ C::tx_is_padded, 0 }, // overidden below if this is a skipped phase event
595582
{ C::tx_start_phase, phase_counter == 0 ? 1 : 0 },
596583
{ C::tx_sel_read_phase_length, phase_counter == 0 && !is_one_shot_phase(tx_phase_event->phase) },
597584
{ C::tx_is_revertible, is_revertible(tx_phase_event->phase) ? 1 : 0 },
@@ -640,40 +627,23 @@ void TxTraceBuilder::process(const simulation::EventEmitterInterface<simulation:
640627
[&](const simulation::CleanupEvent&) {
641628
trace.set(row, handle_pi_read(tx_phase_event->phase, 1, 0));
642629
trace.set(row, handle_cleanup());
630+
},
631+
[&](const simulation::EmptyPhaseEvent&) {
632+
// EmptyPhaseEvent represents a phase that is not explicitly skipped because of a
633+
// revert, but just has no contents to process, like when app logic starts but has no
634+
// enqueued calls.
635+
trace.set(row, handle_pi_read(tx_phase_event->phase, 0, 0));
636+
trace.set(row, handle_padded_row(tx_phase_event->phase, gas_used, discard));
643637
} },
644638
tx_phase_event->event);
645639
trace.set(row, handle_next_gas_used(gas_used));
646640
trace.set(row, handle_gas_limit(current_gas_limit));
647641

648-
// Handle a potential phase jump due to a revert, we dont need to check if we are in a revertible phase
649-
// since our witgen will have exited for any reverts in a non-revertible phase.
650-
// If we revert in a phase that isnt TEARDOWN, we jump to TEARDOWN
651-
if (tx_phase_event->reverted && tx_phase_event->phase != TransactionPhase::TEARDOWN) {
652-
// Jump to the TEARDOWN phase
653-
// we need to -2 because of the loop increment and because the enum is 1-indexed
654-
i = static_cast<uint8_t>(TransactionPhase::TEARDOWN) - 2;
655-
}
656642
phase_counter++;
657643
row++;
658644
}
659645
// In case we encounter another skip row
660646
propagated_state = phase_events.back()->state_after;
661-
662-
if (phase == TransactionPhase::SETUP) {
663-
// Store off the state at the end of setup to rollback to later on revert
664-
end_setup_snapshot = phase_events.back()->state_after;
665-
}
666-
if (phase_events.back()->reverted) {
667-
// On revert, roll back to end-setup-snapshot
668-
// Even though tx-execution events should already do this,
669-
// we need to update propagated state here so that any padded rows
670-
// get the correct rolled-back state rather then the pre-rollback state.
671-
propagated_state.tree_states = end_setup_snapshot.tree_states;
672-
propagated_state.written_public_data_slots_tree_snapshot =
673-
end_setup_snapshot.written_public_data_slots_tree_snapshot;
674-
propagated_state.side_effect_states = end_setup_snapshot.side_effect_states;
675-
// Note: we only rollback tree/side-effect states, not gas used or next_context_id.
676-
}
677647
}
678648
}
679649

0 commit comments

Comments
 (0)