From cae7279be8df3e12004757f1ede721452719e5ad Mon Sep 17 00:00:00 2001 From: David Banks <47112877+dbanks12@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:10:27 -0500 Subject: [PATCH] refactor: enqueued calls processor -> public tx simulator (#9919) - Rename EnqueuedCallsProcessor -> PublicTxSimulator - Introduce PublicTxContext - Refactor EnqueuedCallsProcessor with functions per phase that use PublicTxContext - Split up tracing of enqueued calls and the merging of their state & traces (too confusing if they're coupled) - Lots of prep for removal of public kernels - Misc cleanup Peek at the other PRs in this stack to see how things will simplify further! I am open to all feedback when it comes to the refactor of EnqueuedCallsProcessor, this new PublicTxContext, state forking, etc. --- .../src/tx_validator/gas_validator.ts | 4 +- .../src/tx_validator/phases_validator.ts | 4 +- .../simulator/src/avm/avm_simulator.ts | 4 +- .../simulator/src/avm/journal/journal.ts | 63 +- .../src/avm/opcodes/external_calls.ts | 5 +- .../src/public/dual_side_effect_trace.ts | 31 +- .../enqueued_call_side_effect_trace.test.ts | 33 +- .../public/enqueued_call_side_effect_trace.ts | 175 +++--- .../src/public/enqueued_calls_processor.ts | 554 ------------------ yarn-project/simulator/src/public/index.ts | 3 +- .../src/public/public_processor.test.ts | 14 +- .../simulator/src/public/public_processor.ts | 6 +- .../simulator/src/public/public_tx_context.ts | 290 +++++++++ ...or.test.ts => public_tx_simulator.test.ts} | 13 +- .../src/public/public_tx_simulator.ts | 263 +++++++++ .../simulator/src/public/side_effect_trace.ts | 17 +- .../src/public/side_effect_trace_interface.ts | 14 +- yarn-project/simulator/src/public/utils.ts | 182 ++++++ 18 files changed, 908 insertions(+), 767 deletions(-) delete mode 100644 yarn-project/simulator/src/public/enqueued_calls_processor.ts create mode 100644 yarn-project/simulator/src/public/public_tx_context.ts rename yarn-project/simulator/src/public/{enqueued_calls_processor.test.ts => public_tx_simulator.test.ts} (98%) create mode 100644 yarn-project/simulator/src/public/public_tx_simulator.ts create mode 100644 yarn-project/simulator/src/public/utils.ts diff --git a/yarn-project/sequencer-client/src/tx_validator/gas_validator.ts b/yarn-project/sequencer-client/src/tx_validator/gas_validator.ts index 7fc5647904e..e7a47a7eced 100644 --- a/yarn-project/sequencer-client/src/tx_validator/gas_validator.ts +++ b/yarn-project/sequencer-client/src/tx_validator/gas_validator.ts @@ -1,7 +1,7 @@ import { type Tx, TxExecutionPhase, type TxValidator } from '@aztec/circuit-types'; import { type AztecAddress, type Fr, FunctionSelector } from '@aztec/circuits.js'; import { createDebugLogger } from '@aztec/foundation/log'; -import { EnqueuedCallsProcessor, computeFeePayerBalanceStorageSlot } from '@aztec/simulator'; +import { computeFeePayerBalanceStorageSlot, getExecutionRequestsByPhase } from '@aztec/simulator'; /** Provides a view into public contract state */ export interface PublicStateSource { @@ -58,7 +58,7 @@ export class GasTxValidator implements TxValidator { ); // If there is a claim in this tx that increases the fee payer balance in Fee Juice, add it to balance - const setupFns = EnqueuedCallsProcessor.getExecutionRequestsByPhase(tx, TxExecutionPhase.SETUP); + const setupFns = getExecutionRequestsByPhase(tx, TxExecutionPhase.SETUP); const claimFunctionCall = setupFns.find( fn => fn.callContext.contractAddress.equals(this.#feeJuiceAddress) && diff --git a/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts b/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts index 813c382a6a6..4474b198afa 100644 --- a/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts +++ b/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts @@ -7,7 +7,7 @@ import { } from '@aztec/circuit-types'; import { type ContractDataSource } from '@aztec/circuits.js'; import { createDebugLogger } from '@aztec/foundation/log'; -import { ContractsDataSourcePublicDB, EnqueuedCallsProcessor } from '@aztec/simulator'; +import { ContractsDataSourcePublicDB, getExecutionRequestsByPhase } from '@aztec/simulator'; export class PhasesTxValidator implements TxValidator { #log = createDebugLogger('aztec:sequencer:tx_validator:tx_phases'); @@ -45,7 +45,7 @@ export class PhasesTxValidator implements TxValidator { return true; } - const setupFns = EnqueuedCallsProcessor.getExecutionRequestsByPhase(tx, TxExecutionPhase.SETUP); + const setupFns = getExecutionRequestsByPhase(tx, TxExecutionPhase.SETUP); for (const setupFn of setupFns) { if (!(await this.isOnAllowList(setupFn, this.setupAllowList))) { this.#log.warn( diff --git a/yarn-project/simulator/src/avm/avm_simulator.ts b/yarn-project/simulator/src/avm/avm_simulator.ts index 7b6f7e26aef..dd97bf194dd 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.ts @@ -30,8 +30,8 @@ type PcTally = { export class AvmSimulator { private log: DebugLogger; private bytecode: Buffer | undefined; - public opcodeTallies: Map = new Map(); - public pcTallies: Map = new Map(); + private opcodeTallies: Map = new Map(); + private pcTallies: Map = new Map(); constructor(private context: AvmContext) { assert( diff --git a/yarn-project/simulator/src/avm/journal/journal.ts b/yarn-project/simulator/src/avm/journal/journal.ts index bbaa5633160..ac5e8810474 100644 --- a/yarn-project/simulator/src/avm/journal/journal.ts +++ b/yarn-project/simulator/src/avm/journal/journal.ts @@ -430,9 +430,16 @@ export class AvmPersistableStateManager { /** * Accept nested world state modifications */ - public acceptForkedState(forkedState: AvmPersistableStateManager) { + public mergeForkedState(forkedState: AvmPersistableStateManager) { this.publicStorage.acceptAndMerge(forkedState.publicStorage); this.nullifiers.acceptAndMerge(forkedState.nullifiers); + this.trace.mergeSuccessfulForkedTrace(forkedState.trace); + } + + public rejectForkedState(forkedState: AvmPersistableStateManager) { + this.publicStorage.acceptAndMerge(forkedState.publicStorage); + this.nullifiers.acceptAndMerge(forkedState.nullifiers); + this.trace.mergeRevertedForkedTrace(forkedState.trace); } /** @@ -474,10 +481,8 @@ export class AvmPersistableStateManager { return undefined; } } - /** - * Accept the nested call's state and trace the nested call - */ - public async processNestedCall( + + public async traceNestedCall( forkedState: AvmPersistableStateManager, nestedEnvironment: AvmExecutionEnvironment, startGasLeft: Gas, @@ -485,9 +490,6 @@ export class AvmPersistableStateManager { bytecode: Buffer, avmCallResults: AvmContractCallResult, ) { - if (!avmCallResults.reverted) { - this.acceptForkedState(forkedState); - } const functionName = await getPublicFunctionDebugName( this.worldStateDB, nestedEnvironment.address, @@ -495,7 +497,7 @@ export class AvmPersistableStateManager { nestedEnvironment.calldata, ); - this.log.verbose(`[AVM] Calling nested function ${functionName}`); + this.log.verbose(`[AVM] Tracing nested external contract call ${functionName}`); this.trace.traceNestedCall( forkedState.trace, @@ -508,47 +510,8 @@ export class AvmPersistableStateManager { ); } - public async mergeStateForEnqueuedCall( - forkedState: AvmPersistableStateManager, - /** The call request from private that enqueued this call. */ - publicCallRequest: PublicCallRequest, - /** The call's calldata */ - calldata: Fr[], - /** Did the call revert? */ - reverted: boolean, - ) { - if (!reverted) { - this.acceptForkedState(forkedState); - } - const functionName = await getPublicFunctionDebugName( - this.worldStateDB, - publicCallRequest.contractAddress, - publicCallRequest.functionSelector, - calldata, - ); - - this.log.verbose(`[AVM] Encountered enqueued public call starting with function ${functionName}`); - - this.trace.traceEnqueuedCall(forkedState.trace, publicCallRequest, calldata, reverted); - } - - public mergeStateForPhase( - /** The forked state manager used by app logic */ - forkedState: AvmPersistableStateManager, - /** The call requests for each enqueued call in app logic. */ - publicCallRequests: PublicCallRequest[], - /** The calldatas for each enqueued call in app logic */ - calldatas: Fr[][], - /** Did the any enqueued call in app logic revert? */ - reverted: boolean, - ) { - if (!reverted) { - this.acceptForkedState(forkedState); - } - - this.log.verbose(`[AVM] Encountered app logic phase`); - - this.trace.traceExecutionPhase(forkedState.trace, publicCallRequests, calldatas, reverted); + public traceEnqueuedCall(publicCallRequest: PublicCallRequest, calldata: Fr[], reverted: boolean) { + this.trace.traceEnqueuedCall(publicCallRequest, calldata, reverted); } } diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.ts index 9b52a8ff160..7bfbe5e8c71 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.ts @@ -98,7 +98,10 @@ abstract class ExternalCall extends Instruction { context.machineState.refundGas(gasLeftToGas(nestedContext.machineState)); // Accept the nested call's state and trace the nested call - await context.persistableState.processNestedCall( + if (success) { + context.persistableState.mergeForkedState(nestedContext.persistableState); + } + await context.persistableState.traceNestedCall( /*nestedState=*/ nestedContext.persistableState, /*nestedEnvironment=*/ nestedContext.environment, /*startGasLeft=*/ Gas.from(allocatedGas), diff --git a/yarn-project/simulator/src/public/dual_side_effect_trace.ts b/yarn-project/simulator/src/public/dual_side_effect_trace.ts index 3c72c06c7ce..15fa4b76456 100644 --- a/yarn-project/simulator/src/public/dual_side_effect_trace.ts +++ b/yarn-project/simulator/src/public/dual_side_effect_trace.ts @@ -222,8 +222,6 @@ export class DualSideEffectTrace implements PublicSideEffectTraceInterface { } public traceEnqueuedCall( - /** The trace of the enqueued call. */ - enqueuedCallTrace: this, /** The call request from private that enqueued this call. */ publicCallRequest: PublicCallRequest, /** The call's calldata */ @@ -231,30 +229,15 @@ export class DualSideEffectTrace implements PublicSideEffectTraceInterface { /** Did the call revert? */ reverted: boolean, ) { - this.enqueuedCallTrace.traceEnqueuedCall( - enqueuedCallTrace.enqueuedCallTrace, - publicCallRequest, - calldata, - reverted, - ); + this.enqueuedCallTrace.traceEnqueuedCall(publicCallRequest, calldata, reverted); } - public traceExecutionPhase( - /** The trace of the enqueued call. */ - appLogicTrace: this, - /** The call request from private that enqueued this call. */ - publicCallRequests: PublicCallRequest[], - /** The call's calldata */ - calldatas: Fr[][], - /** Did the any enqueued call in app logic revert? */ - reverted: boolean, - ) { - this.enqueuedCallTrace.traceExecutionPhase( - appLogicTrace.enqueuedCallTrace, - publicCallRequests, - calldatas, - reverted, - ); + public mergeSuccessfulForkedTrace(nestedTrace: this) { + this.enqueuedCallTrace.mergeSuccessfulForkedTrace(nestedTrace.enqueuedCallTrace); + } + + public mergeRevertedForkedTrace(nestedTrace: this) { + this.enqueuedCallTrace.mergeRevertedForkedTrace(nestedTrace.enqueuedCallTrace); } /** diff --git a/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.test.ts b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.test.ts index 421a861fbe0..dc1ccb6bc45 100644 --- a/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.test.ts +++ b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.test.ts @@ -34,7 +34,7 @@ import { import { computePublicDataTreeLeafSlot, computeVarArgsHash, siloNullifier } from '@aztec/circuits.js/hash'; import { Fr } from '@aztec/foundation/fields'; -import { randomBytes, randomInt } from 'crypto'; +import { randomInt } from 'crypto'; import { AvmContractCallResult } from '../avm/avm_contract_call_result.js'; import { type AvmExecutionEnvironment } from '../avm/avm_execution_environment.js'; @@ -70,7 +70,6 @@ describe('Enqueued-call Side Effect Trace', () => { const endGasLeft = Gas.fromFields([new Fr(randomInt(10000)), new Fr(randomInt(10000))]); const transactionFee = Fr.random(); const calldata = [Fr.random(), Fr.random(), Fr.random(), Fr.random()]; - const bytecode = randomBytes(100); const returnValues = [Fr.random(), Fr.random()]; const constants = CombinedConstantData.empty(); @@ -80,7 +79,6 @@ describe('Enqueued-call Side Effect Trace', () => { transactionFee, }); const avmCallResults = new AvmContractCallResult(/*reverted=*/ false, returnValues); - const avmCallRevertedResults = new AvmContractCallResult(/*reverted=*/ true, returnValues); const emptyValidationRequests = PublicValidationRequests.empty(); @@ -477,8 +475,8 @@ describe('Enqueued-call Side Effect Trace', () => { }); }); - describe.each([avmCallResults, avmCallRevertedResults])('Should trace & absorb nested calls', callResults => { - it(`${callResults.reverted ? 'Reverted' : 'Successful'} calls should be traced and absorbed properly`, () => { + describe.each([false, true])('Should merge forked traces', reverted => { + it(`${reverted ? 'Reverted' : 'Successful'} forked trace should be merged properly`, () => { const existsDefault = true; const nestedTrace = new PublicEnqueuedCallSideEffectTrace(startCounter); @@ -510,7 +508,11 @@ describe('Enqueued-call Side Effect Trace', () => { nestedTrace.traceGetContractInstance(address, /*exists=*/ false, contractInstance); testCounter++; - trace.traceNestedCall(nestedTrace, avmEnvironment, startGasLeft, endGasLeft, bytecode, callResults); + if (reverted) { + trace.mergeRevertedForkedTrace(nestedTrace); + } else { + trace.mergeSuccessfulForkedTrace(nestedTrace); + } // parent trace adopts nested call's counter expect(trace.getCounter()).toBe(testCounter); @@ -518,17 +520,16 @@ describe('Enqueued-call Side Effect Trace', () => { // parent absorbs child's side effects const parentSideEffects = trace.getSideEffects(); const childSideEffects = nestedTrace.getSideEffects(); - if (callResults.reverted) { - expect(parentSideEffects.publicDataReads).toEqual(childSideEffects.publicDataReads); - expect(parentSideEffects.publicDataWrites).toEqual(childSideEffects.publicDataWrites); - expect(parentSideEffects.noteHashReadRequests).toEqual(childSideEffects.noteHashReadRequests); + // TODO(dbanks12): confirm that all hints were merged from child + if (reverted) { + expect(parentSideEffects.publicDataReads).toEqual([]); + expect(parentSideEffects.publicDataWrites).toEqual([]); + expect(parentSideEffects.noteHashReadRequests).toEqual([]); expect(parentSideEffects.noteHashes).toEqual([]); - expect(parentSideEffects.nullifierReadRequests).toEqual(childSideEffects.nullifierReadRequests); - expect(parentSideEffects.nullifierNonExistentReadRequests).toEqual( - childSideEffects.nullifierNonExistentReadRequests, - ); - expect(parentSideEffects.nullifiers).toEqual(childSideEffects.nullifiers); - expect(parentSideEffects.l1ToL2MsgReadRequests).toEqual(childSideEffects.l1ToL2MsgReadRequests); + expect(parentSideEffects.nullifierReadRequests).toEqual([]); + expect(parentSideEffects.nullifierNonExistentReadRequests).toEqual([]); + expect(parentSideEffects.nullifiers).toEqual([]); + expect(parentSideEffects.l1ToL2MsgReadRequests).toEqual([]); expect(parentSideEffects.l2ToL1Msgs).toEqual([]); expect(parentSideEffects.unencryptedLogs).toEqual([]); expect(parentSideEffects.unencryptedLogsHashes).toEqual([]); diff --git a/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.ts b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.ts index 038a6183f1b..b5f87c521b9 100644 --- a/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.ts +++ b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.ts @@ -1,6 +1,8 @@ import { UnencryptedFunctionL2Logs, UnencryptedL2Log } from '@aztec/circuit-types'; import { + AvmAccumulatedData, AvmAppendTreeHint, + AvmCircuitPublicInputs, AvmContractBytecodeHints, AvmContractInstanceHint, AvmEnqueuedCallHint, @@ -16,6 +18,8 @@ import { type ContractClassIdPreimage, EthAddress, Gas, + type GasSettings, + type GlobalVariables, L1_TO_L2_MSG_TREE_HEIGHT, L2ToL1Message, LogHash, @@ -38,12 +42,15 @@ import { Nullifier, NullifierLeafPreimage, PUBLIC_DATA_TREE_HEIGHT, + PrivateToAvmAccumulatedData, + PrivateToAvmAccumulatedDataArrayLengths, PublicAccumulatedData, PublicAccumulatedDataArrayLengths, PublicCallRequest, PublicDataRead, PublicDataTreeLeafPreimage, PublicDataUpdateRequest, + PublicDataWrite, PublicInnerCallRequest, PublicValidationRequestArrayLengths, PublicValidationRequests, @@ -55,6 +62,7 @@ import { ScopedReadRequest, SerializableContractInstance, TreeLeafReadRequest, + type TreeSnapshots, VMCircuitPublicInputs, } from '@aztec/circuits.js'; import { computePublicDataTreeLeafSlot, siloNullifier } from '@aztec/circuits.js/hash'; @@ -62,6 +70,7 @@ import { makeTuple } from '@aztec/foundation/array'; import { padArrayEnd } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; +import { type Tuple } from '@aztec/foundation/serialize'; import { assert } from 'console'; @@ -241,7 +250,10 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI this.avmCircuitHints.storageUpdateRequest.items.push( new AvmPublicDataWriteTreeHint(readHint, newLeafPreimage, insertionPath), ); - this.log.debug(`SSTORE cnt: ${this.sideEffectCounter} val: ${value} slot: ${slot}`); + + this.log.debug( + `Traced public data write (address=${contractAddress}, slot=${slot}, leafSlot=${leafSlot}): value=${value} (counter=${this.sideEffectCounter})`, + ); this.incrementSideEffectCounter(); } @@ -475,20 +487,11 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI /** Function name for logging */ _functionName: string = 'unknown', ) { + // TODO(4805): check if some threshold is reached for max nested calls (to unique contracts?) + // // Store end side effect counter before it gets updated by absorbing nested call trace const endSideEffectCounter = new Fr(this.sideEffectCounter); - // TODO(4805): check if some threshold is reached for max nested calls (to unique contracts?) - // TODO(dbanks12): should emit a nullifier read request. There should be two thresholds. - // one for max unique contract calls, and another based on max nullifier reads. - // Since this trace function happens _after_ a nested call, such threshold limits must take - // place in another trace function that occurs _before_ a nested call. - if (avmCallResults.reverted) { - this.mergeRevertedForkedTrace(nestedCallTrace); - } else { - this.mergeSuccessfulForkedTrace(nestedCallTrace); - } - const gasUsed = new Gas(startGasLeft.daGas - endGasLeft.daGas, startGasLeft.l2Gas - endGasLeft.l2Gas); this.avmCircuitHints.externalCalls.items.push( @@ -507,97 +510,49 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI * Accept some results from a finished call's trace into this one. */ public traceEnqueuedCall( - /** The trace of the enqueued call. */ - enqueuedCallTrace: this, /** The call request from private that enqueued this call. */ publicCallRequest: PublicCallRequest, /** The call's calldata */ calldata: Fr[], /** Did the call revert? */ - reverted: boolean, + _reverted: boolean, ) { + this.log.debug(`Tracing enqueued call`); // TODO(4805): check if some threshold is reached for max enqueued or nested calls (to unique contracts?) - // TODO(dbanks12): should emit a nullifier read request. There should be two thresholds. - // one for max unique contract calls, and another based on max nullifier reads. - // Since this trace function happens _after_ a nested call, such threshold limits must take - // place in another trace function that occurs _before_ a nested call. - if (reverted) { - this.mergeRevertedForkedTrace(enqueuedCallTrace); - } else { - this.mergeSuccessfulForkedTrace(enqueuedCallTrace); - } - this.enqueuedCalls.push(publicCallRequest); - this.avmCircuitHints.enqueuedCalls.items.push(new AvmEnqueuedCallHint(publicCallRequest.contractAddress, calldata)); } - /** - * Trace an enqueued call. - * Accept some results from a finished call's trace into this one. - */ - public traceExecutionPhase( - /** The trace of the enqueued call. */ - phaseTrace: this, - /** The call request from private that enqueued this call. */ - publicCallRequests: PublicCallRequest[], - /** The call's calldata */ - calldatas: Fr[][], - /** Did the any enqueued call in app logic revert? */ - reverted: boolean, - ) { - // We only merge in enqueued calls here at the top-level - // because enqueued calls cannot enqueue others. - this.enqueuedCalls.push(...phaseTrace.enqueuedCalls); - if (reverted) { - this.mergeRevertedForkedTrace(phaseTrace); - } else { - this.mergeSuccessfulForkedTrace(phaseTrace); - } - - for (let i = 0; i < publicCallRequests.length; i++) { - this.enqueuedCalls.push(publicCallRequests[i]); - - this.avmCircuitHints.enqueuedCalls.items.push( - new AvmEnqueuedCallHint(publicCallRequests[i].contractAddress, calldatas[i]), - ); - } - } - - private mergeSuccessfulForkedTrace(nestedTrace: this) { + public mergeSuccessfulForkedTrace(nestedTrace: this) { // TODO(dbanks12): accept & merge nested trace's hints! this.sideEffectCounter = nestedTrace.sideEffectCounter; + + this.enqueuedCalls.push(...nestedTrace.enqueuedCalls); + this.publicDataReads.push(...nestedTrace.publicDataReads); this.publicDataWrites.push(...nestedTrace.publicDataWrites); this.noteHashReadRequests.push(...nestedTrace.noteHashReadRequests); this.noteHashes.push(...nestedTrace.noteHashes); this.nullifierReadRequests.push(...nestedTrace.nullifierReadRequests); this.nullifierNonExistentReadRequests.push(...nestedTrace.nullifierNonExistentReadRequests); + this.log.debug(`Merging nullifiers: ${nestedTrace.nullifiers.length}`); + this.log.debug(`Into parent nullifiers: ${this.nullifiers.length}`); this.nullifiers.push(...nestedTrace.nullifiers); + this.log.debug(`After merge: ${JSON.stringify(this.nullifiers)}`); this.l1ToL2MsgReadRequests.push(...nestedTrace.l1ToL2MsgReadRequests); this.l2ToL1Messages.push(...nestedTrace.l2ToL1Messages); this.unencryptedLogs.push(...nestedTrace.unencryptedLogs); this.unencryptedLogsHashes.push(...nestedTrace.unencryptedLogsHashes); } - private mergeRevertedForkedTrace(nestedTrace: this) { - // All read requests, and any writes (storage & nullifiers) that - // require complex validation in public kernel (with end lifetimes) - // must be absorbed even on revert. - + /** + * Discard accumulated side effects, but keep hints. + */ + public mergeRevertedForkedTrace(nestedTrace: this) { // TODO(dbanks12): accept & merge nested trace's hints! - // TODO(dbanks12): What should happen to side effect counter on revert? this.sideEffectCounter = nestedTrace.sideEffectCounter; - this.publicDataReads.push(...nestedTrace.publicDataReads); - this.publicDataWrites.push(...nestedTrace.publicDataWrites); - this.noteHashReadRequests.push(...nestedTrace.noteHashReadRequests); - // new noteHashes are tossed on revert - this.nullifierReadRequests.push(...nestedTrace.nullifierReadRequests); - this.nullifierNonExistentReadRequests.push(...nestedTrace.nullifierNonExistentReadRequests); - this.nullifiers.push(...nestedTrace.nullifiers); - this.l1ToL2MsgReadRequests.push(...nestedTrace.l1ToL2MsgReadRequests); - // new l2-to-l1 messages are tossed on revert - // new unencrypted logs are tossed on revert + + this.enqueuedCalls.push(...nestedTrace.enqueuedCalls); } public getSideEffects(): SideEffects { @@ -662,6 +617,7 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI /** The call's results */ avmCallResults: AvmContractCallResult, ): VMCircuitPublicInputs { + this.log.debug(`Creating public inputs with call result: ${avmCallResults.reverted}`); return new VMCircuitPublicInputs( /*constants=*/ constants, /*callRequest=*/ callRequest, @@ -678,6 +634,53 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI ); } + public toAvmCircuitPublicInputs( + /** Globals. */ + globalVariables: GlobalVariables, + /** Start tree snapshots. */ + startTreeSnapshots: TreeSnapshots, + /** Gas used at start of TX. */ + startGasUsed: Gas, + /** How much gas was available for this public execution. */ + gasLimits: GasSettings, + /** Call requests for setup phase. */ + publicSetupCallRequests: Tuple, + /** Call requests for app logic phase. */ + publicAppLogicCallRequests: Tuple, + /** Call request for teardown phase. */ + publicTeardownCallRequest: PublicCallRequest, + /** End tree snapshots. */ + endTreeSnapshots: TreeSnapshots, + /** + * Gas used by the whole transaction, assuming entire teardown limit is used. + * This is the gas used when computing transaction fee. + */ + endGasUsed: Gas, + /** Transaction fee. */ + transactionFee: Fr, + /** The call's results */ + reverted: boolean, + ): AvmCircuitPublicInputs { + return new AvmCircuitPublicInputs( + globalVariables, + startTreeSnapshots, + startGasUsed, + gasLimits, + publicSetupCallRequests, + publicAppLogicCallRequests, + publicTeardownCallRequest, + /*previousNonRevertibleAccumulatedDataArrayLengths=*/ PrivateToAvmAccumulatedDataArrayLengths.empty(), + /*previousRevertibleAccumulatedDataArrayLengths=*/ PrivateToAvmAccumulatedDataArrayLengths.empty(), + /*previousNonRevertibleAccumulatedDataArray=*/ PrivateToAvmAccumulatedData.empty(), + /*previousRevertibleAccumulatedDataArray=*/ PrivateToAvmAccumulatedData.empty(), + endTreeSnapshots, + endGasUsed, + /*accumulatedData=*/ this.getAvmAccumulatedData(), + transactionFee, + reverted, + ); + } + public toPublicFunctionCallResult( /** The execution environment of the nested call. */ _avmEnvironment: AvmExecutionEnvironment, @@ -718,6 +721,28 @@ export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceI ); } + private getAvmAccumulatedData() { + return new AvmAccumulatedData( + padArrayEnd( + this.noteHashes.map(n => n.value), + Fr.zero(), + MAX_NOTE_HASHES_PER_TX, + ), + padArrayEnd( + this.nullifiers.map(n => n.value), + Fr.zero(), + MAX_NULLIFIERS_PER_TX, + ), + padArrayEnd(this.l2ToL1Messages, ScopedL2ToL1Message.empty(), MAX_L2_TO_L1_MSGS_PER_TX), + padArrayEnd(this.unencryptedLogsHashes, ScopedLogHash.empty(), MAX_UNENCRYPTED_LOGS_PER_TX), + padArrayEnd( + this.publicDataWrites.map(w => new PublicDataWrite(w.leafSlot, w.newValue)), + PublicDataWrite.empty(), + MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, + ), + ); + } + private getAccumulatedData(gasUsed: Gas) { return new PublicAccumulatedData( padArrayEnd(this.noteHashes, ScopedNoteHash.empty(), MAX_NOTE_HASHES_PER_TX), diff --git a/yarn-project/simulator/src/public/enqueued_calls_processor.ts b/yarn-project/simulator/src/public/enqueued_calls_processor.ts deleted file mode 100644 index f29123ba001..00000000000 --- a/yarn-project/simulator/src/public/enqueued_calls_processor.ts +++ /dev/null @@ -1,554 +0,0 @@ -import { - type AvmProvingRequest, - type GasUsed, - type MerkleTreeReadOperations, - type NestedProcessReturnValues, - type PublicExecutionRequest, - type SimulationError, - type Tx, - TxExecutionPhase, -} from '@aztec/circuit-types'; -import { - AvmAccumulatedData, - AvmCircuitPublicInputs, - type CombinedAccumulatedData, - CombinedConstantData, - EnqueuedCallData, - Fr, - Gas, - type GlobalVariables, - type Header, - type KernelCircuitPublicInputs, - NESTED_RECURSIVE_PROOF_LENGTH, - type PrivateKernelTailCircuitPublicInputs, - PrivateToAvmAccumulatedData, - PrivateToAvmAccumulatedDataArrayLengths, - type PrivateToPublicAccumulatedData, - PublicAccumulatedData, - PublicAccumulatedDataArrayLengths, - type PublicCallRequest, - PublicKernelCircuitPrivateInputs, - PublicKernelCircuitPublicInputs, - PublicKernelData, - PublicValidationRequestArrayLengths, - PublicValidationRequests, - RevertCode, - TreeSnapshots, - type VMCircuitPublicInputs, - VerificationKeyData, - countAccumulatedItems, - makeEmptyProof, - makeEmptyRecursiveProof, -} from '@aztec/circuits.js'; -import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; -import { Timer } from '@aztec/foundation/timer'; -import { getVKSiblingPath } from '@aztec/noir-protocol-circuits-types'; - -import { inspect } from 'util'; - -import { AvmPersistableStateManager } from '../avm/journal/journal.js'; -import { DualSideEffectTrace } from './dual_side_effect_trace.js'; -import { PublicEnqueuedCallSideEffectTrace } from './enqueued_call_side_effect_trace.js'; -import { EnqueuedCallSimulator } from './enqueued_call_simulator.js'; -import { type PublicExecutor } from './executor.js'; -import { type WorldStateDB } from './public_db_sources.js'; -import { type PublicKernelCircuitSimulator } from './public_kernel_circuit_simulator.js'; -import { PublicKernelTailSimulator } from './public_kernel_tail_simulator.js'; -import { PublicSideEffectTrace } from './side_effect_trace.js'; - -const PhaseIsRevertible: Record = { - [TxExecutionPhase.SETUP]: false, - [TxExecutionPhase.APP_LOGIC]: true, - [TxExecutionPhase.TEARDOWN]: true, -}; - -type PublicPhaseResult = { - avmProvingRequest: AvmProvingRequest; - /** The output of the public kernel circuit simulation for this phase */ - publicKernelOutput: PublicKernelCircuitPublicInputs; - /** Return values of simulating complete callstack */ - returnValues: NestedProcessReturnValues[]; - /** Gas used during the execution of this phase */ - gasUsed: Gas; - /** Time spent for the execution this phase */ - durationMs: number; - /** Reverted */ - reverted: boolean; - /** Revert reason, if any */ - revertReason?: SimulationError; -}; - -type PhaseGasUsed = Record; - -export type ProcessedPhase = { - phase: TxExecutionPhase; - durationMs: number; - returnValues: NestedProcessReturnValues[]; - revertReason?: SimulationError; -}; - -export type TxPublicCallsResult = { - avmProvingRequest: AvmProvingRequest; - /** Gas used during the execution of this tx */ - gasUsed: GasUsed; - revertCode: RevertCode; - /** Revert reason, if any */ - revertReason?: SimulationError; - processedPhases: ProcessedPhase[]; -}; - -export class EnqueuedCallsProcessor { - private log: DebugLogger; - - constructor( - private publicKernelSimulator: PublicKernelCircuitSimulator, - private globalVariables: GlobalVariables, - private worldStateDB: WorldStateDB, - private enqueuedCallSimulator: EnqueuedCallSimulator, - private publicKernelTailSimulator: PublicKernelTailSimulator, - ) { - this.log = createDebugLogger(`aztec:sequencer`); - } - - static create( - db: MerkleTreeReadOperations, - publicExecutor: PublicExecutor, - publicKernelSimulator: PublicKernelCircuitSimulator, - globalVariables: GlobalVariables, - historicalHeader: Header, - worldStateDB: WorldStateDB, - realAvmProvingRequests: boolean = true, - ) { - const enqueuedCallSimulator = new EnqueuedCallSimulator( - db, - worldStateDB, - publicExecutor, - globalVariables, - historicalHeader, - realAvmProvingRequests, - ); - - const publicKernelTailSimulator = PublicKernelTailSimulator.create(db, publicKernelSimulator); - - return new EnqueuedCallsProcessor( - publicKernelSimulator, - globalVariables, - worldStateDB, - enqueuedCallSimulator, - publicKernelTailSimulator, - ); - } - - static getExecutionRequestsByPhase(tx: Tx, phase: TxExecutionPhase): PublicExecutionRequest[] { - switch (phase) { - case TxExecutionPhase.SETUP: - return tx.getNonRevertiblePublicExecutionRequests(); - case TxExecutionPhase.APP_LOGIC: - return tx.getRevertiblePublicExecutionRequests(); - case TxExecutionPhase.TEARDOWN: { - const request = tx.getPublicTeardownExecutionRequest(); - return request ? [request] : []; - } - default: - throw new Error(`Unknown phase: ${phase}`); - } - } - - static getCallRequestsByPhase(tx: Tx, phase: TxExecutionPhase): PublicCallRequest[] { - switch (phase) { - case TxExecutionPhase.SETUP: - return tx.data.getNonRevertiblePublicCallRequests(); - case TxExecutionPhase.APP_LOGIC: - return tx.data.getRevertiblePublicCallRequests(); - case TxExecutionPhase.TEARDOWN: { - const request = tx.data.getTeardownPublicCallRequest(); - return request ? [request] : []; - } - default: - throw new Error(`Unknown phase: ${phase}`); - } - } - - async process(tx: Tx): Promise { - this.log.verbose(`Processing tx ${tx.getTxHash()}`); - - const constants = CombinedConstantData.combine(tx.data.constants, this.globalVariables); - const phases: TxExecutionPhase[] = [TxExecutionPhase.SETUP, TxExecutionPhase.APP_LOGIC, TxExecutionPhase.TEARDOWN]; - const processedPhases: ProcessedPhase[] = []; - let phaseGasUsed: PhaseGasUsed = { - [TxExecutionPhase.SETUP]: Gas.empty(), - [TxExecutionPhase.APP_LOGIC]: Gas.empty(), - [TxExecutionPhase.TEARDOWN]: Gas.empty(), - }; - let avmProvingRequest: AvmProvingRequest; - let publicKernelOutput = this.getPublicKernelCircuitPublicInputs(tx.data); - let revertReason: SimulationError | undefined; - - const nonRevertibleNullifiersFromPrivate = publicKernelOutput.endNonRevertibleData.nullifiers - .filter(n => !n.isEmpty()) - .map(n => n.value); - const _revertibleNullifiersFromPrivate = publicKernelOutput.end.nullifiers - .filter(n => !n.isEmpty()) - .map(n => n.value); - - // During SETUP, non revertible side effects from private are our "previous data" - const prevAccumulatedData = publicKernelOutput.endNonRevertibleData; - const previousValidationRequestArrayLengths = PublicValidationRequestArrayLengths.new( - publicKernelOutput.validationRequests, - ); - - const previousAccumulatedDataArrayLengths = PublicAccumulatedDataArrayLengths.new(prevAccumulatedData); - const innerCallTrace = new PublicSideEffectTrace(); - const enqueuedCallTrace = new PublicEnqueuedCallSideEffectTrace( - /*startSideEffectCounter=*/ 0, - previousValidationRequestArrayLengths, - previousAccumulatedDataArrayLengths, - ); - const trace = new DualSideEffectTrace(innerCallTrace, enqueuedCallTrace); - - // Transaction level state manager that will be forked for revertible phases. - const txStateManager = AvmPersistableStateManager.newWithPendingSiloedNullifiers( - this.worldStateDB, - trace, - nonRevertibleNullifiersFromPrivate, - ); - // TODO(dbanks12): insert all non-revertible side effects from private here. - - for (let i = 0; i < phases.length; i++) { - const phase = phases[i]; - let stateManagerForPhase: AvmPersistableStateManager; - if (phase === TxExecutionPhase.SETUP) { - // don't need to fork for setup since it's non-revertible - // (if setup fails, transaction is thrown out) - stateManagerForPhase = txStateManager; - } else { - // Fork the state manager so that we can rollback state if a revertible phase reverts. - stateManagerForPhase = txStateManager.fork(); - // NOTE: Teardown is revertible, but will run even if app logic reverts! - } - const callRequests = EnqueuedCallsProcessor.getCallRequestsByPhase(tx, phase); - if (callRequests.length) { - const allocatedGas = this.getAllocatedGasForPhase(phase, tx, phaseGasUsed); - const transactionFee = phase !== TxExecutionPhase.TEARDOWN ? Fr.ZERO : this.getTransactionFee(tx, phaseGasUsed); - - const executionRequests = EnqueuedCallsProcessor.getExecutionRequestsByPhase(tx, phase); - const result = await this.processPhase( - phase, - tx, - constants, - callRequests, - executionRequests, - publicKernelOutput, - allocatedGas, - transactionFee, - stateManagerForPhase, - ).catch(async err => { - await this.worldStateDB.rollbackToCommit(); - throw err; - }); - - publicKernelOutput = result.publicKernelOutput; - - // Propagate only one avmProvingRequest of a function call for now, so that we know it's still provable. - // Eventually this will be the proof for the entire public call stack. - avmProvingRequest = result.avmProvingRequest; - - if (phase !== TxExecutionPhase.SETUP) { - txStateManager.mergeStateForPhase( - stateManagerForPhase, - callRequests, - executionRequests.map(req => req.args), - /*reverted=*/ result.revertReason ? true : false, - ); - } - - processedPhases.push({ - phase, - durationMs: result.durationMs, - returnValues: result.returnValues, - revertReason: result.revertReason, - }); - - phaseGasUsed = { - ...phaseGasUsed, - [phase]: result.gasUsed, - }; - - revertReason ??= result.revertReason; - } - } - - const tailKernelOutput = await this.publicKernelTailSimulator.simulate(publicKernelOutput).catch( - // the abstract phase manager throws if simulation gives error in non-revertible phase - async err => { - await this.worldStateDB.rollbackToCommit(); - throw err; - }, - ); - - const gasUsedForFee = this.getGasUsedForFee(tx, phaseGasUsed); - const transactionFee = this.getTransactionFee(tx, phaseGasUsed); - avmProvingRequest!.inputs.output = this.generateAvmCircuitPublicInputs( - tx, - tailKernelOutput, - gasUsedForFee, - transactionFee, - ); - - const gasUsed = { - totalGas: this.getActualGasUsed(tx, phaseGasUsed), - teardownGas: phaseGasUsed[TxExecutionPhase.TEARDOWN], - }; - - return { - avmProvingRequest: avmProvingRequest!, - gasUsed, - processedPhases, - revertCode: tailKernelOutput.revertCode, - revertReason, - }; - } - - private async processPhase( - phase: TxExecutionPhase, - tx: Tx, - constants: CombinedConstantData, - callRequests: PublicCallRequest[], - executionRequests: PublicExecutionRequest[], - previousPublicKernelOutput: PublicKernelCircuitPublicInputs, - allocatedGas: Gas, - transactionFee: Fr, - txStateManager: AvmPersistableStateManager, - ): Promise { - this.log.debug(`Beginning processing in phase ${TxExecutionPhase[phase]} for tx ${tx.getTxHash()}`); - - const phaseTimer = new Timer(); - const returnValues: NestedProcessReturnValues[] = []; - let availableGas = allocatedGas; - let avmProvingRequest: AvmProvingRequest; - let publicKernelOutput = previousPublicKernelOutput; - let reverted: boolean = false; - let revertReason: SimulationError | undefined; - for (let i = callRequests.length - 1; i >= 0; i--) { - if (reverted) { - break; - } - - const callRequest = callRequests[i]; - const executionRequest = executionRequests[i]; - - // add new contracts to the contracts db so that their functions may be found and called - // TODO(#4073): This is catching only private deployments, when we add public ones, we'll - // have to capture contracts emitted in that phase as well. - // TODO(@spalladino): Should we allow emitting contracts in the fee preparation phase? - // TODO(#6464): Should we allow emitting contracts in the private setup phase? - // if so, this should only add contracts that were deployed during private app logic. - await this.worldStateDB.addNewContracts(tx); - - // each enqueued call starts with an incremented side effect counter - const enqueuedCallStateManager = txStateManager.fork(/*incrementSideEffectCounter=*/ true); - const enqueuedCallResult = await this.enqueuedCallSimulator.simulate( - callRequest, - executionRequest, - constants, - availableGas, - transactionFee, - enqueuedCallStateManager, - ); - - if (enqueuedCallResult.revertReason && !PhaseIsRevertible[phase]) { - this.log.debug( - `Simulation error on ${executionRequest.callContext.contractAddress}:${executionRequest.callContext.functionSelector} with reason: ${enqueuedCallResult.revertReason}`, - ); - throw enqueuedCallResult.revertReason; - } - await txStateManager.mergeStateForEnqueuedCall( - enqueuedCallStateManager, - callRequest, - executionRequest.args, - enqueuedCallResult.reverted!, - ); - - availableGas = availableGas.sub(enqueuedCallResult.gasUsed); - avmProvingRequest = enqueuedCallResult.avmProvingRequest; - returnValues.push(enqueuedCallResult.returnValues); - reverted = enqueuedCallResult.reverted; - revertReason = enqueuedCallResult.revertReason; - - // Instead of operating on worldStateDB here, do we do AvmPersistableStateManager.revert() or return()? - if (reverted) { - // TODO(#6464): Should we allow emitting contracts in the private setup phase? - // if so, this is removing contracts deployed in private setup - // You can't submit contracts in public, so this is only relevant for private-created - // side effects - // Are we reverting here back to end of non-revertible insertions? - // What are we reverting back to? - await this.worldStateDB.removeNewContracts(tx); - tx.filterRevertedLogs(publicKernelOutput); - } else { - tx.unencryptedLogs.addFunctionLogs([enqueuedCallResult.newUnencryptedLogs]); - } - - const output = await this.runMergeKernelCircuit(publicKernelOutput, enqueuedCallResult.kernelOutput); - publicKernelOutput = output; - } - - return { - avmProvingRequest: avmProvingRequest!, - publicKernelOutput, - durationMs: phaseTimer.ms(), - gasUsed: allocatedGas.sub(availableGas), - returnValues, - reverted, - revertReason, - }; - } - - private getAllocatedGasForPhase(phase: TxExecutionPhase, tx: Tx, phaseGasUsed: PhaseGasUsed) { - const gasSettings = tx.data.constants.txContext.gasSettings; - if (phase === TxExecutionPhase.TEARDOWN) { - return gasSettings.teardownGasLimits; - } else { - return gasSettings.gasLimits - .sub(tx.data.gasUsed) - .sub(phaseGasUsed[TxExecutionPhase.SETUP]) - .sub(phaseGasUsed[TxExecutionPhase.APP_LOGIC]); - } - } - - private getTransactionFee(tx: Tx, phaseGasUsed: PhaseGasUsed): Fr { - const gasFees = this.globalVariables.gasFees; - const txFee = this.getGasUsedForFee(tx, phaseGasUsed).computeFee(gasFees); - - this.log.debug(`Computed tx fee`, { txFee, gasUsed: inspect(phaseGasUsed), gasFees: inspect(gasFees) }); - - return txFee; - } - - private getGasUsedForFee(tx: Tx, phaseGasUsed: PhaseGasUsed) { - return tx.data.gasUsed // This should've included teardown gas limits. - .add(phaseGasUsed[TxExecutionPhase.SETUP]) - .add(phaseGasUsed[TxExecutionPhase.APP_LOGIC]); - } - - private getActualGasUsed(tx: Tx, phaseGasUsed: PhaseGasUsed) { - const requireTeardown = tx.data.hasTeardownPublicCallRequest(); - const teardownGasLimits = tx.data.constants.txContext.gasSettings.teardownGasLimits; - const privateGasUsed = tx.data.gasUsed.sub(requireTeardown ? teardownGasLimits : Gas.empty()); - const publicGasUsed = Object.values(phaseGasUsed).reduce((accum, gasUsed) => accum.add(gasUsed), Gas.empty()); - return privateGasUsed.add(publicGasUsed); - } - - private async runMergeKernelCircuit( - previousOutput: PublicKernelCircuitPublicInputs, - enqueuedCallData: VMCircuitPublicInputs, - ): Promise { - const previousKernel = this.getPreviousKernelData(previousOutput); - - // The proof is not used in simulation. - const vmProof = makeEmptyProof(); - const callData = new EnqueuedCallData(enqueuedCallData, vmProof); - - const inputs = new PublicKernelCircuitPrivateInputs(previousKernel, callData); - - return await this.publicKernelSimulator.publicKernelCircuitMerge(inputs); - } - - private getPreviousKernelData(previousOutput: PublicKernelCircuitPublicInputs): PublicKernelData { - // The proof is not used in simulation. - const proof = makeEmptyRecursiveProof(NESTED_RECURSIVE_PROOF_LENGTH); - - const vk = VerificationKeyData.makeFakeHonk(); - const vkIndex = 0; - const siblingPath = getVKSiblingPath(vkIndex); - - return new PublicKernelData(previousOutput, proof, vk, vkIndex, siblingPath); - } - - // Temporary hack to create PublicKernelCircuitPublicInputs from PrivateKernelTailCircuitPublicInputs. - private getPublicKernelCircuitPublicInputs(data: PrivateKernelTailCircuitPublicInputs) { - const constants = CombinedConstantData.combine(data.constants, this.globalVariables); - - const validationRequest = PublicValidationRequests.empty(); - validationRequest.forRollup = data.rollupValidationRequests; - - const convertAccumulatedData = (from: PrivateToPublicAccumulatedData) => { - const to = PublicAccumulatedData.empty(); - to.noteHashes.forEach((_, i) => (to.noteHashes[i].noteHash.value = from.noteHashes[i])); - to.nullifiers.forEach((_, i) => (to.nullifiers[i].value = from.nullifiers[i])); - to.l2ToL1Msgs.forEach((_, i) => (to.l2ToL1Msgs[i] = from.l2ToL1Msgs[i])); - to.noteEncryptedLogsHashes.forEach((_, i) => (to.noteEncryptedLogsHashes[i] = from.noteEncryptedLogsHashes[i])); - to.encryptedLogsHashes.forEach((_, i) => (to.encryptedLogsHashes[i] = from.encryptedLogsHashes[i])); - to.publicCallStack.forEach((_, i) => (to.publicCallStack[i] = from.publicCallRequests[i])); - return to; - }; - - return new PublicKernelCircuitPublicInputs( - constants, - validationRequest, - convertAccumulatedData(data.forPublic!.nonRevertibleAccumulatedData), - convertAccumulatedData(data.forPublic!.revertibleAccumulatedData), - 0, - data.forPublic!.publicTeardownCallRequest, - data.feePayer, - RevertCode.OK, - ); - } - - // Temporary hack to create the AvmCircuitPublicInputs from public tail's public inputs. - private generateAvmCircuitPublicInputs( - tx: Tx, - tailOutput: KernelCircuitPublicInputs, - gasUsedForFee: Gas, - transactionFee: Fr, - ) { - const startTreeSnapshots = new TreeSnapshots( - tailOutput.constants.historicalHeader.state.l1ToL2MessageTree, - tailOutput.startState.noteHashTree, - tailOutput.startState.nullifierTree, - tailOutput.startState.publicDataTree, - ); - - const getArrayLengths = (from: PrivateToPublicAccumulatedData) => - new PrivateToAvmAccumulatedDataArrayLengths( - countAccumulatedItems(from.noteHashes), - countAccumulatedItems(from.nullifiers), - countAccumulatedItems(from.l2ToL1Msgs), - ); - - const convertAccumulatedData = (from: PrivateToPublicAccumulatedData) => - new PrivateToAvmAccumulatedData(from.noteHashes, from.nullifiers, from.l2ToL1Msgs); - - const convertAvmAccumulatedData = (from: CombinedAccumulatedData) => - new AvmAccumulatedData( - from.noteHashes, - from.nullifiers, - from.l2ToL1Msgs, - from.unencryptedLogsHashes, - from.publicDataWrites, - ); - - // This is wrong. But this is not used or checked in the rollup at the moment. - // Should fetch the updated roots from db. - const endTreeSnapshots = startTreeSnapshots; - - return new AvmCircuitPublicInputs( - tailOutput.constants.globalVariables, - startTreeSnapshots, - tx.data.gasUsed, - tx.data.constants.txContext.gasSettings, - tx.data.forPublic!.nonRevertibleAccumulatedData.publicCallRequests, - tx.data.forPublic!.revertibleAccumulatedData.publicCallRequests, - tx.data.forPublic!.publicTeardownCallRequest, - getArrayLengths(tx.data.forPublic!.nonRevertibleAccumulatedData), - getArrayLengths(tx.data.forPublic!.revertibleAccumulatedData), - convertAccumulatedData(tx.data.forPublic!.nonRevertibleAccumulatedData), - convertAccumulatedData(tx.data.forPublic!.revertibleAccumulatedData), - endTreeSnapshots, - gasUsedForFee, - convertAvmAccumulatedData(tailOutput.end), - transactionFee, - !tailOutput.revertCode.equals(RevertCode.OK), - ); - } -} diff --git a/yarn-project/simulator/src/public/index.ts b/yarn-project/simulator/src/public/index.ts index 1145e23e7b0..1735b437ef1 100644 --- a/yarn-project/simulator/src/public/index.ts +++ b/yarn-project/simulator/src/public/index.ts @@ -1,6 +1,6 @@ export * from './db_interfaces.js'; export { EnqueuedCallSimulator } from './enqueued_call_simulator.js'; -export * from './enqueued_calls_processor.js'; +export * from './public_tx_simulator.js'; export { type EnqueuedPublicCallExecutionResult as PublicExecutionResult, type PublicFunctionCallResult, @@ -15,3 +15,4 @@ export { PublicProcessor, PublicProcessorFactory } from './public_processor.js'; export { PublicSideEffectTrace } from './side_effect_trace.js'; export { PublicEnqueuedCallSideEffectTrace } from './enqueued_call_side_effect_trace.js'; export { DualSideEffectTrace } from './dual_side_effect_trace.js'; +export { getExecutionRequestsByPhase } from './utils.js'; diff --git a/yarn-project/simulator/src/public/public_processor.test.ts b/yarn-project/simulator/src/public/public_processor.test.ts index b15932abf08..621a833af3b 100644 --- a/yarn-project/simulator/src/public/public_processor.test.ts +++ b/yarn-project/simulator/src/public/public_processor.test.ts @@ -26,19 +26,19 @@ import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { type EnqueuedCallsProcessor, type TxPublicCallsResult } from './enqueued_calls_processor.js'; import { computeFeePayerBalanceLeafSlot } from './fee_payment.js'; import { type WorldStateDB } from './public_db_sources.js'; import { PublicProcessor } from './public_processor.js'; +import { type PublicTxResult, type PublicTxSimulator } from './public_tx_simulator.js'; describe('public_processor', () => { let db: MockProxy; let worldStateDB: MockProxy; - let enqueuedCallsProcessor: MockProxy; + let publicTxProcessor: MockProxy; let handler: MockProxy; let root: Buffer; - let mockedEnqueuedCallsResult: TxPublicCallsResult; + let mockedEnqueuedCallsResult: PublicTxResult; let mockedAvmOutput: AvmCircuitPublicInputs; let processor: PublicProcessor; @@ -55,7 +55,7 @@ describe('public_processor', () => { beforeEach(() => { db = mock(); worldStateDB = mock(); - enqueuedCallsProcessor = mock(); + publicTxProcessor = mock(); handler = mock(); root = Buffer.alloc(32, 5); @@ -79,7 +79,7 @@ describe('public_processor', () => { worldStateDB.storageRead.mockResolvedValue(Fr.ZERO); - enqueuedCallsProcessor.process.mockImplementation(() => { + publicTxProcessor.process.mockImplementation(() => { return Promise.resolve(mockedEnqueuedCallsResult); }); @@ -88,7 +88,7 @@ describe('public_processor', () => { globalVariables, Header.empty(), worldStateDB, - enqueuedCallsProcessor, + publicTxProcessor, new NoopTelemetryClient(), ); }); @@ -136,7 +136,7 @@ describe('public_processor', () => { }); it('returns failed txs without aborting entire operation', async function () { - enqueuedCallsProcessor.process.mockRejectedValue(new SimulationError(`Failed`, [])); + publicTxProcessor.process.mockRejectedValue(new SimulationError(`Failed`, [])); const tx = mockTxWithPublicCalls(); const [processed, failed] = await processor.process([tx], 1, handler); diff --git a/yarn-project/simulator/src/public/public_processor.ts b/yarn-project/simulator/src/public/public_processor.ts index 947f1fda5b1..55fce164cfb 100644 --- a/yarn-project/simulator/src/public/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor.ts @@ -32,13 +32,13 @@ import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec/telemetry-client'; import { type SimulationProvider } from '../providers/index.js'; -import { EnqueuedCallsProcessor } from './enqueued_calls_processor.js'; import { PublicExecutor } from './executor.js'; import { computeFeePayerBalanceLeafSlot, computeFeePayerBalanceStorageSlot } from './fee_payment.js'; import { WorldStateDB } from './public_db_sources.js'; import { RealPublicKernelCircuitSimulator } from './public_kernel.js'; import { type PublicKernelCircuitSimulator } from './public_kernel_circuit_simulator.js'; import { PublicProcessorMetrics } from './public_processor_metrics.js'; +import { PublicTxSimulator } from './public_tx_simulator.js'; /** * Creates new instances of PublicProcessor given the provided merkle tree db and contract data source. @@ -91,7 +91,7 @@ export class PublicProcessor { protected globalVariables: GlobalVariables, protected historicalHeader: Header, protected worldStateDB: WorldStateDB, - protected enqueuedCallsProcessor: EnqueuedCallsProcessor, + protected enqueuedCallsProcessor: PublicTxSimulator, telemetryClient: TelemetryClient, private log = createDebugLogger('aztec:sequencer:public-processor'), ) { @@ -107,7 +107,7 @@ export class PublicProcessor { worldStateDB: WorldStateDB, telemetryClient: TelemetryClient, ) { - const enqueuedCallsProcessor = EnqueuedCallsProcessor.create( + const enqueuedCallsProcessor = PublicTxSimulator.create( db, publicExecutor, publicKernelSimulator, diff --git a/yarn-project/simulator/src/public/public_tx_context.ts b/yarn-project/simulator/src/public/public_tx_context.ts new file mode 100644 index 00000000000..8e2a898ffbb --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_context.ts @@ -0,0 +1,290 @@ +import { + type AvmProvingRequest, + type MerkleTreeReadOperations, + type PublicExecutionRequest, + type SimulationError, + type Tx, + TxExecutionPhase, +} from '@aztec/circuit-types'; +import { + CombinedConstantData, + Fr, + Gas, + type GasSettings, + type GlobalVariables, + PublicAccumulatedDataArrayLengths, + type PublicCallRequest, + type PublicKernelCircuitPublicInputs, + PublicValidationRequestArrayLengths, + RevertCode, + type StateReference, +} from '@aztec/circuits.js'; +import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; + +import { assert } from 'console'; +import { inspect } from 'util'; + +import { AvmPersistableStateManager } from '../avm/index.js'; +import { DualSideEffectTrace } from './dual_side_effect_trace.js'; +import { PublicEnqueuedCallSideEffectTrace } from './enqueued_call_side_effect_trace.js'; +import { type WorldStateDB } from './public_db_sources.js'; +import { PublicSideEffectTrace } from './side_effect_trace.js'; +import { getCallRequestsByPhase, getExecutionRequestsByPhase, getPublicKernelCircuitPublicInputs } from './utils.js'; + +class PhaseStateManager { + private currentlyActiveStateManager: AvmPersistableStateManager | undefined; + + constructor(private readonly txStateManager: AvmPersistableStateManager) {} + + fork() { + assert(!this.currentlyActiveStateManager, 'Cannot fork when already forked'); + this.currentlyActiveStateManager = this.txStateManager.fork(); + } + + getActiveStateManager() { + return this.currentlyActiveStateManager || this.txStateManager; + } + + isForked() { + return !!this.currentlyActiveStateManager; + } + + mergeForkedState() { + assert(this.currentlyActiveStateManager, 'No forked state to merge'); + this.txStateManager.mergeForkedState(this.currentlyActiveStateManager!); + // Drop the forked state manager now that it is merged + this.currentlyActiveStateManager = undefined; + } + + discardForkedState() { + assert(this.currentlyActiveStateManager, 'No forked state to discard'); + this.txStateManager.rejectForkedState(this.currentlyActiveStateManager!); + // Drop the forked state manager. We don't want it! + this.currentlyActiveStateManager = undefined; + } +} + +export class PublicTxContext { + private log: DebugLogger; + + private currentPhase: TxExecutionPhase = TxExecutionPhase.SETUP; + + /* Gas used including private, teardown gas _limit_, setup and app logic */ + private gasUsed: Gas; + /* Gas actually used during teardown (different from limit) */ + public teardownGasUsed: Gas = Gas.empty(); + + public revertCode: RevertCode = RevertCode.OK; + public revertReason: SimulationError | undefined; + + public avmProvingRequest: AvmProvingRequest | undefined; // tmp hack + + constructor( + public readonly state: PhaseStateManager, + public readonly tx: Tx, // tmp hack + public readonly globalVariables: GlobalVariables, + public readonly constants: CombinedConstantData, // tmp hack + public readonly startStateReference: StateReference, + startGasUsed: Gas, + private readonly gasSettings: GasSettings, + private readonly setupCallRequests: PublicCallRequest[], + private readonly appLogicCallRequests: PublicCallRequest[], + private readonly teardownCallRequests: PublicCallRequest[], + private readonly setupExecutionRequests: PublicExecutionRequest[], + private readonly appLogicExecutionRequests: PublicExecutionRequest[], + private readonly teardownExecutionRequests: PublicExecutionRequest[], + public latestPublicKernelOutput: PublicKernelCircuitPublicInputs, + public trace: PublicEnqueuedCallSideEffectTrace, + ) { + this.log = createDebugLogger(`aztec:public_tx_context`); + this.gasUsed = startGasUsed; + } + + public static async create( + db: MerkleTreeReadOperations, + worldStateDB: WorldStateDB, + tx: Tx, + globalVariables: GlobalVariables, + ) { + const privateKernelOutput = tx.data; + const latestPublicKernelOutput = getPublicKernelCircuitPublicInputs(privateKernelOutput, globalVariables); + + const nonRevertibleNullifiersFromPrivate = latestPublicKernelOutput.endNonRevertibleData.nullifiers + .filter(n => !n.isEmpty()) + .map(n => n.value); + const _revertibleNullifiersFromPrivate = latestPublicKernelOutput.end.nullifiers + .filter(n => !n.isEmpty()) + .map(n => n.value); + + // During SETUP, non revertible side effects from private are our "previous data" + const prevAccumulatedData = latestPublicKernelOutput.endNonRevertibleData; + const previousValidationRequestArrayLengths = PublicValidationRequestArrayLengths.new( + latestPublicKernelOutput.validationRequests, + ); + + const previousAccumulatedDataArrayLengths = PublicAccumulatedDataArrayLengths.new(prevAccumulatedData); + + const innerCallTrace = new PublicSideEffectTrace(); + const enqueuedCallTrace = new PublicEnqueuedCallSideEffectTrace( + /*startSideEffectCounter=*/ 0, + previousValidationRequestArrayLengths, + previousAccumulatedDataArrayLengths, + ); + const trace = new DualSideEffectTrace(innerCallTrace, enqueuedCallTrace); + + // Transaction level state manager that will be forked for revertible phases. + const txStateManager = AvmPersistableStateManager.newWithPendingSiloedNullifiers( + worldStateDB, + trace, + nonRevertibleNullifiersFromPrivate, + ); + + return new PublicTxContext( + new PhaseStateManager(txStateManager), + tx, + globalVariables, + CombinedConstantData.combine(tx.data.constants, globalVariables), + await db.getStateReference(), + tx.data.gasUsed, + tx.data.constants.txContext.gasSettings, + getCallRequestsByPhase(tx, TxExecutionPhase.SETUP), + getCallRequestsByPhase(tx, TxExecutionPhase.APP_LOGIC), + getCallRequestsByPhase(tx, TxExecutionPhase.TEARDOWN), + getExecutionRequestsByPhase(tx, TxExecutionPhase.SETUP), + getExecutionRequestsByPhase(tx, TxExecutionPhase.APP_LOGIC), + getExecutionRequestsByPhase(tx, TxExecutionPhase.TEARDOWN), + latestPublicKernelOutput, + enqueuedCallTrace, + ); + } + + getCurrentPhase(): TxExecutionPhase { + return this.currentPhase; + } + + hasPhase(phase: TxExecutionPhase = this.currentPhase): boolean { + if (phase === TxExecutionPhase.SETUP) { + return this.setupCallRequests.length > 0; + } else if (phase === TxExecutionPhase.APP_LOGIC) { + return this.appLogicCallRequests.length > 0; + } else { + // phase === TxExecutionPhase.TEARDOWN + return this.teardownCallRequests.length > 0; + } + } + + progressToNextPhase() { + assert(this.currentPhase !== TxExecutionPhase.TEARDOWN, 'Cannot progress past teardown'); + if (this.currentPhase === TxExecutionPhase.SETUP) { + this.currentPhase = TxExecutionPhase.APP_LOGIC; + } else { + this.currentPhase = TxExecutionPhase.TEARDOWN; + } + } + + revert(revertReason: SimulationError | undefined = undefined, culprit = '') { + this.log.debug( + `${TxExecutionPhase[this.currentPhase]} phase reverted! ${culprit} failed with reason: ${revertReason}`, + ); + if (revertReason && !this.revertReason) { + // don't override revertReason + // (if app logic and teardown both revert, we want app logic's reason) + this.revertReason = revertReason; + } + if (this.currentPhase === TxExecutionPhase.SETUP) { + this.log.debug(`Setup phase reverted! The transaction will be thrown out.`); + if (revertReason) { + throw revertReason; + } else { + throw new Error(`Setup phase reverted! The transaction will be thrown out. ${culprit} failed`); + } + } else if (this.currentPhase === TxExecutionPhase.APP_LOGIC) { + this.revertCode = RevertCode.APP_LOGIC_REVERTED; + } else if (this.currentPhase === TxExecutionPhase.TEARDOWN) { + if (this.revertCode.equals(RevertCode.APP_LOGIC_REVERTED)) { + this.revertCode = RevertCode.BOTH_REVERTED; + } else { + this.revertCode = RevertCode.TEARDOWN_REVERTED; + } + } + } + + getCallRequestsForCurrentPhase(): PublicCallRequest[] { + switch (this.currentPhase) { + case TxExecutionPhase.SETUP: + return this.setupCallRequests; + case TxExecutionPhase.APP_LOGIC: + return this.appLogicCallRequests; + case TxExecutionPhase.TEARDOWN: + return this.teardownCallRequests; + } + } + + getExecutionRequestsForCurrentPhase(): PublicExecutionRequest[] { + switch (this.currentPhase) { + case TxExecutionPhase.SETUP: + return this.setupExecutionRequests; + case TxExecutionPhase.APP_LOGIC: + return this.appLogicExecutionRequests; + case TxExecutionPhase.TEARDOWN: + return this.teardownExecutionRequests; + } + } + + getGasLeftForCurrentPhase(): Gas { + if (this.currentPhase === TxExecutionPhase.TEARDOWN) { + return this.gasSettings.teardownGasLimits; + } else { + return this.gasSettings.gasLimits.sub(this.gasUsed); + } + } + + consumeGas(gas: Gas) { + if (this.currentPhase === TxExecutionPhase.TEARDOWN) { + // track teardown gas used separately + this.teardownGasUsed = this.teardownGasUsed.add(gas); + } else { + this.gasUsed = this.gasUsed.add(gas); + } + } + + /** + * Compute the gas used using the actual gas used during teardown instead + * of the teardown gas limit. + * Note that this.startGasUsed comes from private and private includes + * teardown gas limit in its output gasUsed. + */ + getActualGasUsed(): Gas { + assert(this.currentPhase === TxExecutionPhase.TEARDOWN, 'Can only compute actual gas used after app logic'); + const requireTeardown = this.teardownCallRequests.length > 0; + const teardownGasLimits = requireTeardown ? this.gasSettings.teardownGasLimits : Gas.empty(); + return this.gasUsed.sub(teardownGasLimits).add(this.teardownGasUsed); + } + + getGasUsedForFee(): Gas { + return this.gasUsed; + } + + getTransactionFeeAtCurrentPhase(): Fr { + if (this.currentPhase === TxExecutionPhase.TEARDOWN) { + return this.getTransactionFeeUnsafe(); + } else { + return Fr.zero(); + } + } + + getTransactionFee(): Fr { + assert(this.currentPhase === TxExecutionPhase.TEARDOWN, 'Transaction fee is only known during/after teardown'); + return this.getTransactionFeeUnsafe(); + } + + private getTransactionFeeUnsafe(): Fr { + const txFee = this.gasUsed.computeFee(this.globalVariables.gasFees); + this.log.debug(`Computed tx fee`, { + txFee, + gasUsed: inspect(this.gasUsed), + gasFees: inspect(this.globalVariables.gasFees), + }); + return txFee; + } +} diff --git a/yarn-project/simulator/src/public/enqueued_calls_processor.test.ts b/yarn-project/simulator/src/public/public_tx_simulator.test.ts similarity index 98% rename from yarn-project/simulator/src/public/enqueued_calls_processor.test.ts rename to yarn-project/simulator/src/public/public_tx_simulator.test.ts index f3502980876..08780d7008c 100644 --- a/yarn-project/simulator/src/public/enqueued_calls_processor.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator.test.ts @@ -32,13 +32,13 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import { type AvmPersistableStateManager } from '../avm/journal/journal.js'; import { PublicExecutionResultBuilder } from '../mocks/fixtures.js'; import { WASMSimulator } from '../providers/acvm_wasm.js'; -import { EnqueuedCallsProcessor } from './enqueued_calls_processor.js'; import { type PublicExecutor } from './executor.js'; import { type WorldStateDB } from './public_db_sources.js'; import { RealPublicKernelCircuitSimulator } from './public_kernel.js'; import { type PublicKernelCircuitSimulator } from './public_kernel_circuit_simulator.js'; +import { PublicTxSimulator } from './public_tx_simulator.js'; -describe('enqueued_calls_processor', () => { +describe('public_tx_simulator', () => { // Gas settings. const gasFees = GasFees.from({ feePerDaGas: new Fr(2), feePerL2Gas: new Fr(3) }); const gasLimits = Gas.from({ daGas: 100, l2Gas: 150 }); @@ -59,7 +59,7 @@ describe('enqueued_calls_processor', () => { let root: Buffer; let publicDataTree: AppendOnlyTree; - let processor: EnqueuedCallsProcessor; + let processor: PublicTxSimulator; const mockTxWithPublicCalls = ({ numberOfSetupCalls = 0, @@ -165,7 +165,7 @@ describe('enqueued_calls_processor', () => { publicKernel = new RealPublicKernelCircuitSimulator(new WASMSimulator()); - processor = EnqueuedCallsProcessor.create( + processor = PublicTxSimulator.create( db, publicExecutor, publicKernel, @@ -376,6 +376,7 @@ describe('enqueued_calls_processor', () => { // squashed // new PublicDataWrite(computePublicDataTreeLeafSlot(contractAddress, contractSlotA), fr(0x101)), new PublicDataWrite(computePublicDataTreeLeafSlot(contractAddress, contractSlotB), fr(0x151)), + new PublicDataWrite(computePublicDataTreeLeafSlot(contractAddress, contractSlotA), fr(0x103)), // squashed // new PublicDataWrite(computePublicDataTreeLeafSlot(contractAddress, contractSlotC), fr(0x201)), @@ -402,7 +403,7 @@ describe('enqueued_calls_processor', () => { expect(publicExecutor.simulate).toHaveBeenCalledTimes(1); }); - it('includes a transaction that reverts in app logic', async function () { + it('includes a transaction that reverts in app logic only', async function () { const tx = mockTxWithPublicCalls({ numberOfSetupCalls: 1, numberOfAppLogicCalls: 2, @@ -480,7 +481,7 @@ describe('enqueued_calls_processor', () => { ]); }); - it('includes a transaction that reverts in teardown', async function () { + it('includes a transaction that reverts in teardown only', async function () { const tx = mockTxWithPublicCalls({ numberOfSetupCalls: 1, numberOfAppLogicCalls: 2, diff --git a/yarn-project/simulator/src/public/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator.ts new file mode 100644 index 00000000000..e694b45dc9c --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_simulator.ts @@ -0,0 +1,263 @@ +import { + type AvmProvingRequest, + type GasUsed, + type MerkleTreeReadOperations, + type NestedProcessReturnValues, + type SimulationError, + type Tx, + TxExecutionPhase, +} from '@aztec/circuit-types'; +import { type GlobalVariables, type Header, type RevertCode } from '@aztec/circuits.js'; +import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; +import { Timer } from '@aztec/foundation/timer'; + +import { EnqueuedCallSimulator } from './enqueued_call_simulator.js'; +import { type PublicExecutor } from './executor.js'; +import { type WorldStateDB } from './public_db_sources.js'; +import { type PublicKernelCircuitSimulator } from './public_kernel_circuit_simulator.js'; +import { PublicKernelTailSimulator } from './public_kernel_tail_simulator.js'; +import { PublicTxContext } from './public_tx_context.js'; +import { generateAvmCircuitPublicInputs, runMergeKernelCircuit } from './utils.js'; + +export type ProcessedPhase = { + phase: TxExecutionPhase; + durationMs: number; + returnValues: NestedProcessReturnValues[]; + reverted: boolean; + revertReason?: SimulationError; +}; + +export type PublicTxResult = { + avmProvingRequest: AvmProvingRequest; + /** Gas used during the execution of this tx */ + gasUsed: GasUsed; + revertCode: RevertCode; + /** Revert reason, if any */ + revertReason?: SimulationError; + processedPhases: ProcessedPhase[]; +}; + +export class PublicTxSimulator { + private log: DebugLogger; + + constructor( + private db: MerkleTreeReadOperations, + private publicKernelSimulator: PublicKernelCircuitSimulator, + private globalVariables: GlobalVariables, + private worldStateDB: WorldStateDB, + private enqueuedCallSimulator: EnqueuedCallSimulator, + private publicKernelTailSimulator: PublicKernelTailSimulator, + ) { + this.log = createDebugLogger(`aztec:sequencer`); + } + + static create( + db: MerkleTreeReadOperations, + publicExecutor: PublicExecutor, + publicKernelSimulator: PublicKernelCircuitSimulator, + globalVariables: GlobalVariables, + historicalHeader: Header, + worldStateDB: WorldStateDB, + realAvmProvingRequests: boolean = true, + ) { + const enqueuedCallSimulator = new EnqueuedCallSimulator( + db, + worldStateDB, + publicExecutor, + globalVariables, + historicalHeader, + realAvmProvingRequests, + ); + + const publicKernelTailSimulator = PublicKernelTailSimulator.create(db, publicKernelSimulator); + + return new PublicTxSimulator( + db, + publicKernelSimulator, + globalVariables, + worldStateDB, + enqueuedCallSimulator, + publicKernelTailSimulator, + ); + } + + async process(tx: Tx): Promise { + this.log.verbose(`Processing tx ${tx.getTxHash()}`); + + const context = await PublicTxContext.create(this.db, this.worldStateDB, tx, this.globalVariables); + + const setupResult = await this.processSetupPhase(context); + const appLogicResult = await this.processAppLogicPhase(context); + const teardownResult = await this.processTeardownPhase(context); + + const processedPhases = [setupResult, appLogicResult, teardownResult].filter( + result => result !== undefined, + ) as ProcessedPhase[]; + + const _endStateReference = await this.db.getStateReference(); + const transactionFee = context.getTransactionFee(); + + const tailKernelOutput = await this.publicKernelTailSimulator.simulate(context.latestPublicKernelOutput); + + context.avmProvingRequest!.inputs.output = generateAvmCircuitPublicInputs( + tx, + tailKernelOutput, + context.getGasUsedForFee(), + transactionFee, + ); + + const gasUsed = { + totalGas: context.getActualGasUsed(), + teardownGas: context.teardownGasUsed, + }; + return { + avmProvingRequest: context.avmProvingRequest!, + gasUsed, + revertCode: context.revertCode, + revertReason: context.revertReason, + processedPhases: processedPhases, + }; + } + + private async processSetupPhase(context: PublicTxContext): Promise { + // Start in phase TxExecutionPhase.SETUP; + if (context.hasPhase()) { + return await this.processPhase(context); + } + } + + private async processAppLogicPhase(context: PublicTxContext): Promise { + context.progressToNextPhase(); // to app logic + if (context.hasPhase()) { + // Fork the state manager so that we can rollback state if app logic or teardown reverts. + // Don't need to fork for setup since it's non-revertible (if setup fails, transaction is thrown out). + context.state.fork(); + + const result = await this.processPhase(context); + + if (result.reverted) { + // Drop the currently active forked state manager and rollback to end of setup. + // Fork again for teardown so that if teardown fails we can again rollback to end of setup. + context.state.discardForkedState(); + } else { + if (!context.hasPhase(TxExecutionPhase.TEARDOWN)) { + // Nothing to do after this (no teardown), so merge state in now instead of letting teardown handle it. + context.state.mergeForkedState(); + } + } + + return result; + } + } + + private async processTeardownPhase(context: PublicTxContext): Promise { + context.progressToNextPhase(); // to teardown + if (context.hasPhase()) { + if (!context.state.isForked()) { + // if state isn't forked (app logic was empty or reverted), fork now + // so we can rollback to the end of setup on teardown revert + context.state.fork(); + } + + const result = await this.processPhase(context); + + if (result.reverted) { + // Drop the currently active forked state manager and rollback to end of setup. + context.state.discardForkedState(); + } else { + context.state.mergeForkedState(); + } + + return result; + } + } + + private async processPhase(context: PublicTxContext): Promise { + const tx = context.tx; + const callRequests = context.getCallRequestsForCurrentPhase(); + const executionRequests = context.getExecutionRequestsForCurrentPhase(); + const txStateManager = context.state.getActiveStateManager(); + + this.log.debug( + `Beginning processing in phase ${TxExecutionPhase[context.getCurrentPhase()]} for tx ${tx.getTxHash()}`, + ); + + const returnValues: NestedProcessReturnValues[] = []; + let reverted = false; + let revertReason: SimulationError | undefined; + const phaseTimer = new Timer(); + for (let i = callRequests.length - 1; i >= 0; i--) { + if (reverted) { + break; + } + + const callRequest = callRequests[i]; + const executionRequest = executionRequests[i]; + + // add new contracts to the contracts db so that their functions may be found and called + // TODO(#4073): This is catching only private deployments, when we add public ones, we'll + // have to capture contracts emitted in that phase as well. + // TODO(@spalladino): Should we allow emitting contracts in the fee preparation phase? + // TODO(#6464): Should we allow emitting contracts in the private setup phase? + // if so, this should only add contracts that were deployed during private app logic. + // FIXME: we shouldn't need to directly modify worldStateDb here! + await this.worldStateDB.addNewContracts(tx); + + // each enqueued call starts with an incremented side effect counter + // FIXME: should be able to stop forking here and just trace the enqueued call (for hinting) + // and proceed with the same state manager for the entire phase + const enqueuedCallStateManager = txStateManager.fork(/*incrementSideEffectCounter=*/ true); + const enqueuedCallResult = await this.enqueuedCallSimulator.simulate( + callRequest, + executionRequest, + context.constants, + /*availableGas=*/ context.getGasLeftForCurrentPhase(), + /*transactionFee=*/ context.getTransactionFeeAtCurrentPhase(), + enqueuedCallStateManager, + ); + + txStateManager.traceEnqueuedCall(callRequest, executionRequest.args, enqueuedCallResult.reverted!); + + context.consumeGas(enqueuedCallResult.gasUsed); + returnValues.push(enqueuedCallResult.returnValues); + // Propagate only one avmProvingRequest of a function call for now, so that we know it's still provable. + // Eventually this will be the proof for the entire public portion of the transaction. + context.avmProvingRequest = enqueuedCallResult.avmProvingRequest; + if (enqueuedCallResult.reverted) { + reverted = true; + const culprit = `${executionRequest.callContext.contractAddress}:${executionRequest.callContext.functionSelector}`; + revertReason = enqueuedCallResult.revertReason; + context.revert(enqueuedCallResult.revertReason, culprit); // throws if in setup (non-revertible) phase + + // TODO(#6464): Should we allow emitting contracts in the private setup phase? + // if so, this is removing contracts deployed in private setup + // You can't submit contracts in public, so this is only relevant for private-created side effects + // FIXME: we shouldn't need to directly modify worldStateDb here! + await this.worldStateDB.removeNewContracts(tx); + // FIXME: we shouldn't be modifying the transaction here! + tx.filterRevertedLogs(context.latestPublicKernelOutput); + // Enqueeud call reverted. Discard state updates and accumulated side effects, but keep hints traced for the circuit. + txStateManager.rejectForkedState(enqueuedCallStateManager); + } else { + // FIXME: we shouldn't be modifying the transaction here! + tx.unencryptedLogs.addFunctionLogs([enqueuedCallResult.newUnencryptedLogs]); + // Enqueued call succeeded! Merge in any state updates made in the forked state manager. + txStateManager.mergeForkedState(enqueuedCallStateManager); + } + + context.latestPublicKernelOutput = await runMergeKernelCircuit( + context.latestPublicKernelOutput, + enqueuedCallResult.kernelOutput, + this.publicKernelSimulator, + ); + } + + return { + phase: context.getCurrentPhase(), + durationMs: phaseTimer.ms(), + returnValues, + reverted, + revertReason, + }; + } +} diff --git a/yarn-project/simulator/src/public/side_effect_trace.ts b/yarn-project/simulator/src/public/side_effect_trace.ts index 19f89f9b117..efe7a3afa6b 100644 --- a/yarn-project/simulator/src/public/side_effect_trace.ts +++ b/yarn-project/simulator/src/public/side_effect_trace.ts @@ -430,8 +430,6 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { } public traceEnqueuedCall( - /** The trace of the enqueued call. */ - _enqueuedCallTrace: this, /** The call request from private that enqueued this call. */ _publicCallRequest: PublicCallRequest, /** The call's calldata */ @@ -442,16 +440,11 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { throw new Error('Not implemented'); } - public traceExecutionPhase( - /** The trace of the enqueued call. */ - _appLogicTrace: this, - /** The call request from private that enqueued this call. */ - _publicCallRequests: PublicCallRequest[], - /** The call's calldata */ - _calldatas: Fr[][], - /** Did the any enqueued call in app logic revert? */ - _reverted: boolean, - ) { + public mergeSuccessfulForkedTrace(_nestedTrace: this) { + throw new Error('Not implemented'); + } + + public mergeRevertedForkedTrace(_nestedTrace: this) { throw new Error('Not implemented'); } diff --git a/yarn-project/simulator/src/public/side_effect_trace_interface.ts b/yarn-project/simulator/src/public/side_effect_trace_interface.ts index bad67b3b553..02179157071 100644 --- a/yarn-project/simulator/src/public/side_effect_trace_interface.ts +++ b/yarn-project/simulator/src/public/side_effect_trace_interface.ts @@ -94,8 +94,6 @@ export interface PublicSideEffectTraceInterface { functionName: string, ): void; traceEnqueuedCall( - /** The trace of the enqueued call. */ - enqueuedCallTrace: this, /** The call request from private that enqueued this call. */ publicCallRequest: PublicCallRequest, /** The call's calldata */ @@ -103,16 +101,8 @@ export interface PublicSideEffectTraceInterface { /** Did the call revert? */ reverted: boolean, ): void; - traceExecutionPhase( - /** The trace of the enqueued call. */ - appLogicTrace: this, - /** The call request from private that enqueued this call. */ - publicCallRequests: PublicCallRequest[], - /** The call's calldata */ - calldatas: Fr[][], - /** Did the any enqueued call in app logic revert? */ - reverted: boolean, - ): void; + mergeSuccessfulForkedTrace(nestedTrace: PublicSideEffectTraceInterface): void; + mergeRevertedForkedTrace(nestedTrace: PublicSideEffectTraceInterface): void; toPublicEnqueuedCallExecutionResult( /** How much gas was left after this public execution. */ endGasLeft: Gas, diff --git a/yarn-project/simulator/src/public/utils.ts b/yarn-project/simulator/src/public/utils.ts new file mode 100644 index 00000000000..8d3f6093f75 --- /dev/null +++ b/yarn-project/simulator/src/public/utils.ts @@ -0,0 +1,182 @@ +import { type PublicExecutionRequest, type Tx, TxExecutionPhase } from '@aztec/circuit-types'; +import { + AvmAccumulatedData, + AvmCircuitPublicInputs, + type CombinedAccumulatedData, + CombinedConstantData, + EnqueuedCallData, + type Fr, + type Gas, + type GlobalVariables, + type KernelCircuitPublicInputs, + NESTED_RECURSIVE_PROOF_LENGTH, + type PrivateKernelTailCircuitPublicInputs, + PrivateToAvmAccumulatedData, + PrivateToAvmAccumulatedDataArrayLengths, + type PrivateToPublicAccumulatedData, + PublicAccumulatedData, + type PublicCallRequest, + PublicKernelCircuitPrivateInputs, + PublicKernelCircuitPublicInputs, + PublicKernelData, + PublicValidationRequests, + RevertCode, + TreeSnapshots, + type VMCircuitPublicInputs, + VerificationKeyData, + countAccumulatedItems, + makeEmptyProof, + makeEmptyRecursiveProof, +} from '@aztec/circuits.js'; +import { getVKSiblingPath } from '@aztec/noir-protocol-circuits-types'; + +import { type PublicKernelCircuitSimulator } from './public_kernel_circuit_simulator.js'; + +export function getExecutionRequestsByPhase(tx: Tx, phase: TxExecutionPhase): PublicExecutionRequest[] { + switch (phase) { + case TxExecutionPhase.SETUP: + return tx.getNonRevertiblePublicExecutionRequests(); + case TxExecutionPhase.APP_LOGIC: + return tx.getRevertiblePublicExecutionRequests(); + case TxExecutionPhase.TEARDOWN: { + const request = tx.getPublicTeardownExecutionRequest(); + return request ? [request] : []; + } + default: + throw new Error(`Unknown phase: ${phase}`); + } +} + +export function getCallRequestsByPhase(tx: Tx, phase: TxExecutionPhase): PublicCallRequest[] { + switch (phase) { + case TxExecutionPhase.SETUP: + return tx.data.getNonRevertiblePublicCallRequests(); + case TxExecutionPhase.APP_LOGIC: + return tx.data.getRevertiblePublicCallRequests(); + case TxExecutionPhase.TEARDOWN: { + const request = tx.data.getTeardownPublicCallRequest(); + return request ? [request] : []; + } + default: + throw new Error(`Unknown phase: ${phase}`); + } +} + +// Temporary hack to create PublicKernelCircuitPublicInputs from PrivateKernelTailCircuitPublicInputs. +export function getPublicKernelCircuitPublicInputs( + data: PrivateKernelTailCircuitPublicInputs, + globalVariables: GlobalVariables, +) { + const constants = CombinedConstantData.combine(data.constants, globalVariables); + + const validationRequest = PublicValidationRequests.empty(); + validationRequest.forRollup = data.rollupValidationRequests; + + const convertAccumulatedData = (from: PrivateToPublicAccumulatedData) => { + const to = PublicAccumulatedData.empty(); + to.noteHashes.forEach((_, i) => (to.noteHashes[i].noteHash.value = from.noteHashes[i])); + to.nullifiers.forEach((_, i) => (to.nullifiers[i].value = from.nullifiers[i])); + to.l2ToL1Msgs.forEach((_, i) => (to.l2ToL1Msgs[i] = from.l2ToL1Msgs[i])); + to.noteEncryptedLogsHashes.forEach((_, i) => (to.noteEncryptedLogsHashes[i] = from.noteEncryptedLogsHashes[i])); + to.encryptedLogsHashes.forEach((_, i) => (to.encryptedLogsHashes[i] = from.encryptedLogsHashes[i])); + to.publicCallStack.forEach((_, i) => (to.publicCallStack[i] = from.publicCallRequests[i])); + return to; + }; + + return new PublicKernelCircuitPublicInputs( + constants, + validationRequest, + convertAccumulatedData(data.forPublic!.nonRevertibleAccumulatedData), + convertAccumulatedData(data.forPublic!.revertibleAccumulatedData), + 0, + data.forPublic!.publicTeardownCallRequest, + data.feePayer, + RevertCode.OK, + ); +} + +// Temporary hack to create the AvmCircuitPublicInputs from public tail's public inputs. +export function generateAvmCircuitPublicInputs( + tx: Tx, + tailOutput: KernelCircuitPublicInputs, + gasUsedForFee: Gas, + transactionFee: Fr, +) { + const startTreeSnapshots = new TreeSnapshots( + tailOutput.constants.historicalHeader.state.l1ToL2MessageTree, + tailOutput.startState.noteHashTree, + tailOutput.startState.nullifierTree, + tailOutput.startState.publicDataTree, + ); + + const getArrayLengths = (from: PrivateToPublicAccumulatedData) => + new PrivateToAvmAccumulatedDataArrayLengths( + countAccumulatedItems(from.noteHashes), + countAccumulatedItems(from.nullifiers), + countAccumulatedItems(from.l2ToL1Msgs), + ); + + const convertAccumulatedData = (from: PrivateToPublicAccumulatedData) => + new PrivateToAvmAccumulatedData(from.noteHashes, from.nullifiers, from.l2ToL1Msgs); + + const convertAvmAccumulatedData = (from: CombinedAccumulatedData) => + new AvmAccumulatedData( + from.noteHashes, + from.nullifiers, + from.l2ToL1Msgs, + from.unencryptedLogsHashes, + from.publicDataWrites, + ); + + // This is wrong. But this is not used or checked in the rollup at the moment. + // Should fetch the updated roots from db. + const endTreeSnapshots = startTreeSnapshots; + + const avmCircuitpublicInputs = new AvmCircuitPublicInputs( + tailOutput.constants.globalVariables, + startTreeSnapshots, + tx.data.gasUsed, + tx.data.constants.txContext.gasSettings, + tx.data.forPublic!.nonRevertibleAccumulatedData.publicCallRequests, + tx.data.forPublic!.revertibleAccumulatedData.publicCallRequests, + tx.data.forPublic!.publicTeardownCallRequest, + getArrayLengths(tx.data.forPublic!.nonRevertibleAccumulatedData), + getArrayLengths(tx.data.forPublic!.revertibleAccumulatedData), + convertAccumulatedData(tx.data.forPublic!.nonRevertibleAccumulatedData), + convertAccumulatedData(tx.data.forPublic!.revertibleAccumulatedData), + endTreeSnapshots, + gasUsedForFee, + convertAvmAccumulatedData(tailOutput.end), + transactionFee, + !tailOutput.revertCode.equals(RevertCode.OK), + ); + //console.log(`[FROM TAIL] AVM: ${inspect(avmCircuitpublicInputs, { depth: 5 })}`); + return avmCircuitpublicInputs; +} + +function getPreviousKernelData(previousOutput: PublicKernelCircuitPublicInputs): PublicKernelData { + // The proof is not used in simulation. + const proof = makeEmptyRecursiveProof(NESTED_RECURSIVE_PROOF_LENGTH); + + const vk = VerificationKeyData.makeFakeHonk(); + const vkIndex = 0; + const siblingPath = getVKSiblingPath(vkIndex); + + return new PublicKernelData(previousOutput, proof, vk, vkIndex, siblingPath); +} + +export async function runMergeKernelCircuit( + previousOutput: PublicKernelCircuitPublicInputs, + enqueuedCallData: VMCircuitPublicInputs, + publicKernelSimulator: PublicKernelCircuitSimulator, +): Promise { + const previousKernel = getPreviousKernelData(previousOutput); + + // The proof is not used in simulation. + const vmProof = makeEmptyProof(); + const callData = new EnqueuedCallData(enqueuedCallData, vmProof); + + const inputs = new PublicKernelCircuitPrivateInputs(previousKernel, callData); + + return await publicKernelSimulator.publicKernelCircuitMerge(inputs); +}