diff --git a/yarn-project/circuits.js/src/structs/l2_to_l1_message.ts b/yarn-project/circuits.js/src/structs/l2_to_l1_message.ts index b3343a2f0d3..37d1ae3c811 100644 --- a/yarn-project/circuits.js/src/structs/l2_to_l1_message.ts +++ b/yarn-project/circuits.js/src/structs/l2_to_l1_message.ts @@ -76,6 +76,10 @@ export class L2ToL1Message { isEmpty(): boolean { return this.recipient.isZero() && this.content.isZero() && !this.counter; } + + scope(contractAddress: AztecAddress) { + return new ScopedL2ToL1Message(this, contractAddress); + } } export class ScopedL2ToL1Message { diff --git a/yarn-project/circuits.js/src/structs/log_hash.ts b/yarn-project/circuits.js/src/structs/log_hash.ts index a51086e5d11..b0691826bbf 100644 --- a/yarn-project/circuits.js/src/structs/log_hash.ts +++ b/yarn-project/circuits.js/src/structs/log_hash.ts @@ -40,6 +40,10 @@ export class LogHash implements Ordered { return `value=${this.value} counter=${this.counter} length=${this.length}`; } + scope(contractAddress: AztecAddress) { + return new ScopedLogHash(this, contractAddress); + } + [inspect.custom](): string { return `LogHash { ${this.toString()} }`; } diff --git a/yarn-project/prover-client/src/mocks/test_context.ts b/yarn-project/prover-client/src/mocks/test_context.ts index 84732a4c12b..d73e304c3a0 100644 --- a/yarn-project/prover-client/src/mocks/test_context.ts +++ b/yarn-project/prover-client/src/mocks/test_context.ts @@ -8,7 +8,14 @@ import { type Tx, type TxValidator, } from '@aztec/circuit-types'; -import { type Gas, type GlobalVariables, Header, type Nullifier, type TxContext } from '@aztec/circuits.js'; +import { + type CombinedConstantData, + type Gas, + type GlobalVariables, + Header, + type Nullifier, + type TxContext, +} from '@aztec/circuits.js'; import { type Fr } from '@aztec/foundation/fields'; import { type DebugLogger } from '@aztec/foundation/log'; import { openTmpStore } from '@aztec/kv-store/utils'; @@ -155,7 +162,7 @@ export class TestContext { ) { const defaultExecutorImplementation = ( execution: PublicExecutionRequest, - _globalVariables: GlobalVariables, + _constants: CombinedConstantData, availableGas: Gas, _txContext: TxContext, _pendingNullifiers: Nullifier[], @@ -195,7 +202,7 @@ export class TestContext { txValidator?: TxValidator, executorMock?: ( execution: PublicExecutionRequest, - globalVariables: GlobalVariables, + constants: CombinedConstantData, availableGas: Gas, txContext: TxContext, pendingNullifiers: Nullifier[], diff --git a/yarn-project/simulator/src/public/dual_side_effect_trace.ts b/yarn-project/simulator/src/public/dual_side_effect_trace.ts new file mode 100644 index 00000000000..1146feb6e6a --- /dev/null +++ b/yarn-project/simulator/src/public/dual_side_effect_trace.ts @@ -0,0 +1,169 @@ +import { type CombinedConstantData, type Gas, type VMCircuitPublicInputs } from '@aztec/circuits.js'; +import { type Fr } from '@aztec/foundation/fields'; +import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; + +import { assert } from 'console'; + +import { type AvmContractCallResult } from '../avm/avm_contract_call_result.js'; +import { type AvmExecutionEnvironment } from '../avm/avm_execution_environment.js'; +import { type PublicEnqueuedCallSideEffectTrace } from './enqueued_call_side_effect_trace.js'; +import { type PublicExecutionResult } from './execution.js'; +import { type PublicSideEffectTrace } from './side_effect_trace.js'; +import { type PublicSideEffectTraceInterface } from './side_effect_trace_interface.js'; + +export type TracedContractInstance = { exists: boolean } & ContractInstanceWithAddress; + +export class DualSideEffectTrace implements PublicSideEffectTraceInterface { + constructor( + public readonly innerCallTrace: PublicSideEffectTrace, + public readonly enqueuedCallTrace: PublicEnqueuedCallSideEffectTrace, + ) {} + + public fork() { + return new DualSideEffectTrace(this.innerCallTrace.fork(), this.enqueuedCallTrace.fork()); + } + + public getCounter() { + assert(this.innerCallTrace.getCounter() == this.enqueuedCallTrace.getCounter()); + return this.innerCallTrace.getCounter(); + } + + public tracePublicStorageRead(storageAddress: Fr, slot: Fr, value: Fr, exists: boolean, cached: boolean) { + this.innerCallTrace.tracePublicStorageRead(storageAddress, slot, value, exists, cached); + this.enqueuedCallTrace.tracePublicStorageRead(storageAddress, slot, value, exists, cached); + } + + public tracePublicStorageWrite(storageAddress: Fr, slot: Fr, value: Fr) { + this.innerCallTrace.tracePublicStorageWrite(storageAddress, slot, value); + this.enqueuedCallTrace.tracePublicStorageWrite(storageAddress, slot, value); + } + + // TODO(8287): _exists can be removed once we have the vm properly handling the equality check + public traceNoteHashCheck(_storageAddress: Fr, noteHash: Fr, leafIndex: Fr, exists: boolean) { + this.innerCallTrace.traceNoteHashCheck(_storageAddress, noteHash, leafIndex, exists); + this.enqueuedCallTrace.traceNoteHashCheck(_storageAddress, noteHash, leafIndex, exists); + } + + public traceNewNoteHash(_storageAddress: Fr, noteHash: Fr) { + this.innerCallTrace.traceNewNoteHash(_storageAddress, noteHash); + this.enqueuedCallTrace.traceNewNoteHash(_storageAddress, noteHash); + } + + public traceNullifierCheck(storageAddress: Fr, nullifier: Fr, leafIndex: Fr, exists: boolean, isPending: boolean) { + this.innerCallTrace.traceNullifierCheck(storageAddress, nullifier, leafIndex, exists, isPending); + this.enqueuedCallTrace.traceNullifierCheck(storageAddress, nullifier, leafIndex, exists, isPending); + } + + public traceNewNullifier(storageAddress: Fr, nullifier: Fr) { + this.innerCallTrace.traceNewNullifier(storageAddress, nullifier); + this.enqueuedCallTrace.traceNewNullifier(storageAddress, nullifier); + } + + public traceL1ToL2MessageCheck(contractAddress: Fr, msgHash: Fr, msgLeafIndex: Fr, exists: boolean) { + this.innerCallTrace.traceL1ToL2MessageCheck(contractAddress, msgHash, msgLeafIndex, exists); + this.enqueuedCallTrace.traceL1ToL2MessageCheck(contractAddress, msgHash, msgLeafIndex, exists); + } + + public traceNewL2ToL1Message(contractAddress: Fr, recipient: Fr, content: Fr) { + this.innerCallTrace.traceNewL2ToL1Message(contractAddress, recipient, content); + this.enqueuedCallTrace.traceNewL2ToL1Message(contractAddress, recipient, content); + } + + public traceUnencryptedLog(contractAddress: Fr, log: Fr[]) { + this.innerCallTrace.traceUnencryptedLog(contractAddress, log); + this.enqueuedCallTrace.traceUnencryptedLog(contractAddress, log); + } + + public traceGetContractInstance(instance: TracedContractInstance) { + this.innerCallTrace.traceGetContractInstance(instance); + this.enqueuedCallTrace.traceGetContractInstance(instance); + } + + /** + * Trace a nested call. + * Accept some results from a finished nested call's trace into this one. + */ + public traceNestedCall( + /** The trace of the nested call. */ + nestedCallTrace: this, + /** The execution environment of the nested call. */ + nestedEnvironment: AvmExecutionEnvironment, + /** How much gas was available for this public execution. */ + startGasLeft: Gas, + /** How much gas was left after this public execution. */ + endGasLeft: Gas, + /** Bytecode used for this execution. */ + bytecode: Buffer, + /** The call's results */ + avmCallResults: AvmContractCallResult, + /** Function name for logging */ + functionName: string = 'unknown', + ) { + this.innerCallTrace.traceNestedCall( + nestedCallTrace.innerCallTrace, + nestedEnvironment, + startGasLeft, + endGasLeft, + bytecode, + avmCallResults, + functionName, + ); + this.enqueuedCallTrace.traceNestedCall( + nestedCallTrace.enqueuedCallTrace, + nestedEnvironment, + startGasLeft, + endGasLeft, + bytecode, + avmCallResults, + functionName, + ); + } + + /** + * Convert this trace to a PublicExecutionResult for use externally to the simulator. + */ + public toPublicExecutionResult( + /** The execution environment of the nested call. */ + avmEnvironment: AvmExecutionEnvironment, + /** How much gas was available for this public execution. */ + startGasLeft: Gas, + /** How much gas was left after this public execution. */ + endGasLeft: Gas, + /** Bytecode used for this execution. */ + bytecode: Buffer, + /** The call's results */ + avmCallResults: AvmContractCallResult, + /** Function name for logging */ + functionName: string = 'unknown', + ): PublicExecutionResult { + return this.innerCallTrace.toPublicExecutionResult( + avmEnvironment, + startGasLeft, + endGasLeft, + bytecode, + avmCallResults, + functionName, + ); + } + + public toVMCircuitPublicInputs( + /** Constants */ + constants: CombinedConstantData, + /** The execution environment of the nested call. */ + avmEnvironment: AvmExecutionEnvironment, + /** How much gas was available for this public execution. */ + startGasLeft: Gas, + /** How much gas was left after this public execution. */ + endGasLeft: Gas, + /** The call's results */ + avmCallResults: AvmContractCallResult, + ): VMCircuitPublicInputs { + return this.enqueuedCallTrace.toVMCircuitPublicInputs( + constants, + avmEnvironment, + startGasLeft, + endGasLeft, + avmCallResults, + ); + } +} 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 new file mode 100644 index 00000000000..950b4411c31 --- /dev/null +++ b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.test.ts @@ -0,0 +1,489 @@ +import { UnencryptedL2Log } from '@aztec/circuit-types'; +import { + AztecAddress, + CombinedConstantData, + EthAddress, + Gas, + L2ToL1Message, + LogHash, + MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX, + MAX_L2_TO_L1_MSGS_PER_TX, + MAX_NOTE_HASHES_PER_TX, + MAX_NOTE_HASH_READ_REQUESTS_PER_TX, + MAX_NULLIFIERS_PER_TX, + MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX, + MAX_NULLIFIER_READ_REQUESTS_PER_TX, + MAX_PUBLIC_DATA_READS_PER_TX, + MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, + MAX_UNENCRYPTED_LOGS_PER_TX, + NoteHash, + Nullifier, + PublicAccumulatedData, + PublicAccumulatedDataArrayLengths, + PublicDataRead, + PublicDataUpdateRequest, + PublicValidationRequestArrayLengths, + PublicValidationRequests, + ReadRequest, + TreeLeafReadRequest, +} from '@aztec/circuits.js'; +import { Fr } from '@aztec/foundation/fields'; +import { SerializableContractInstance } from '@aztec/types/contracts'; + +import { randomBytes, randomInt } from 'crypto'; + +import { AvmContractCallResult } from '../avm/avm_contract_call_result.js'; +import { initExecutionEnvironment } from '../avm/fixtures/index.js'; +import { PublicEnqueuedCallSideEffectTrace, type TracedContractInstance } from './enqueued_call_side_effect_trace.js'; +import { SideEffectLimitReachedError } from './side_effect_errors.js'; + +function randomTracedContractInstance(): TracedContractInstance { + const instance = SerializableContractInstance.random(); + const address = AztecAddress.random(); + return { exists: true, ...instance, address }; +} + +describe('Enqueued-call Side Effect Trace', () => { + const address = Fr.random(); + const utxo = Fr.random(); + const leafIndex = Fr.random(); + const slot = Fr.random(); + const value = Fr.random(); + const recipient = Fr.random(); + const content = Fr.random(); + const log = [Fr.random(), Fr.random(), Fr.random()]; + const contractInstance = SerializableContractInstance.empty().withAddress(new Fr(42)); + + const startGasLeft = Gas.fromFields([new Fr(randomInt(10000)), new Fr(randomInt(10000))]); + 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(); + const avmEnvironment = initExecutionEnvironment({ + address, + calldata, + transactionFee, + }); + const avmCallResults = new AvmContractCallResult(/*reverted=*/ false, returnValues); + const avmCallRevertedResults = new AvmContractCallResult(/*reverted=*/ true, returnValues); + + const emptyValidationRequests = PublicValidationRequests.empty(); + + let startCounter: number; + let startCounterFr: Fr; + let startCounterPlus1: number; + let trace: PublicEnqueuedCallSideEffectTrace; + + beforeEach(() => { + startCounter = randomInt(/*max=*/ 1000000); + startCounterFr = new Fr(startCounter); + startCounterPlus1 = startCounter + 1; + trace = new PublicEnqueuedCallSideEffectTrace(startCounter); + }); + + const toVMCircuitPublicInputs = (trc: PublicEnqueuedCallSideEffectTrace) => { + return trc.toVMCircuitPublicInputs(constants, avmEnvironment, startGasLeft, endGasLeft, avmCallResults); + }; + + it('Should trace storage reads', () => { + const exists = true; + const cached = false; + trace.tracePublicStorageRead(address, slot, value, exists, cached); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const expectedArray = PublicValidationRequests.empty().publicDataReads; + expectedArray[0] = new PublicDataRead(slot, value, startCounter /*contractAddress*/); + + const circuitPublicInputs = toVMCircuitPublicInputs(trace); + expect(circuitPublicInputs.validationRequests.publicDataReads).toEqual(expectedArray); + expect(trace.getAvmCircuitHints().storageValues.items).toEqual([{ key: startCounterFr, value: value }]); + }); + + it('Should trace storage writes', () => { + trace.tracePublicStorageWrite(address, slot, value); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const expectedArray = PublicAccumulatedData.empty().publicDataUpdateRequests; + expectedArray[0] = new PublicDataUpdateRequest(slot, value, startCounter /*contractAddress*/); + + const circuitPublicInputs = toVMCircuitPublicInputs(trace); + expect(circuitPublicInputs.accumulatedData.publicDataUpdateRequests).toEqual(expectedArray); + }); + + it('Should trace note hash checks', () => { + const exists = true; + trace.traceNoteHashCheck(address, utxo, leafIndex, exists); + + const expectedArray = PublicValidationRequests.empty().noteHashReadRequests; + expectedArray[0] = new TreeLeafReadRequest(utxo, leafIndex); + + const circuitPublicInputs = toVMCircuitPublicInputs(trace); + expect(circuitPublicInputs.validationRequests.noteHashReadRequests).toEqual(expectedArray); + + expect(trace.getAvmCircuitHints().noteHashExists.items).toEqual([{ key: leafIndex, value: new Fr(exists) }]); + }); + + it('Should trace note hashes', () => { + trace.traceNewNoteHash(address, utxo); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const expectedArray = PublicAccumulatedData.empty().noteHashes; + expectedArray[0] = new NoteHash(utxo, startCounter).scope(address); + + const circuitPublicInputs = toVMCircuitPublicInputs(trace); + expect(circuitPublicInputs.accumulatedData.noteHashes).toEqual(expectedArray); + }); + + it('Should trace nullifier checks', () => { + const exists = true; + const isPending = false; + trace.traceNullifierCheck(address, utxo, leafIndex, exists, isPending); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const expectedArray = PublicValidationRequests.empty().nullifierReadRequests; + expectedArray[0] = new ReadRequest(utxo, startCounter).scope(address); + + const circuitPublicInputs = toVMCircuitPublicInputs(trace); + expect(circuitPublicInputs.validationRequests.nullifierReadRequests).toEqual(expectedArray); + expect(circuitPublicInputs.validationRequests.nullifierNonExistentReadRequests).toEqual( + emptyValidationRequests.nullifierNonExistentReadRequests, + ); + + expect(trace.getAvmCircuitHints().nullifierExists.items).toEqual([{ key: startCounterFr, value: new Fr(exists) }]); + }); + + it('Should trace non-existent nullifier checks', () => { + const exists = false; + const isPending = false; + trace.traceNullifierCheck(address, utxo, leafIndex, exists, isPending); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const expectedArray = PublicValidationRequests.empty().nullifierNonExistentReadRequests; + expectedArray[0] = new ReadRequest(utxo, startCounter).scope(address); + + const circuitPublicInputs = toVMCircuitPublicInputs(trace); + expect(circuitPublicInputs.validationRequests.nullifierReadRequests).toEqual( + emptyValidationRequests.nullifierReadRequests, + ); + expect(circuitPublicInputs.validationRequests.nullifierNonExistentReadRequests).toEqual(expectedArray); + + expect(trace.getAvmCircuitHints().nullifierExists.items).toEqual([{ key: startCounterFr, value: new Fr(exists) }]); + }); + + it('Should trace nullifiers', () => { + trace.traceNewNullifier(address, utxo); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const expectedArray = PublicAccumulatedData.empty().nullifiers; + expectedArray[0] = new Nullifier(utxo, startCounter, Fr.ZERO); + + const circuitPublicInputs = toVMCircuitPublicInputs(trace); + expect(circuitPublicInputs.accumulatedData.nullifiers).toEqual(expectedArray); + }); + + it('Should trace L1ToL2 Message checks', () => { + const exists = true; + trace.traceL1ToL2MessageCheck(address, utxo, leafIndex, exists); + + const expectedArray = PublicValidationRequests.empty().l1ToL2MsgReadRequests; + expectedArray[0] = new TreeLeafReadRequest(utxo, leafIndex); + + const circuitPublicInputs = toVMCircuitPublicInputs(trace); + expect(circuitPublicInputs.validationRequests.l1ToL2MsgReadRequests).toEqual(expectedArray); + + expect(trace.getAvmCircuitHints().l1ToL2MessageExists.items).toEqual([ + { + key: leafIndex, + value: new Fr(exists), + }, + ]); + }); + + it('Should trace new L2ToL1 messages', () => { + trace.traceNewL2ToL1Message(address, recipient, content); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const expectedArray = PublicAccumulatedData.empty().l2ToL1Msgs; + expectedArray[0] = new L2ToL1Message(EthAddress.fromField(recipient), content, startCounter).scope(address); + + const circuitPublicInputs = toVMCircuitPublicInputs(trace); + expect(circuitPublicInputs.accumulatedData.l2ToL1Msgs).toEqual(expectedArray); + }); + + it('Should trace new unencrypted logs', () => { + trace.traceUnencryptedLog(address, log); + expect(trace.getCounter()).toBe(startCounterPlus1); + + const expectedLog = new UnencryptedL2Log( + AztecAddress.fromField(address), + Buffer.concat(log.map(f => f.toBuffer())), + ); + const expectedArray = PublicAccumulatedData.empty().unencryptedLogsHashes; + expectedArray[0] = new LogHash( + Fr.fromBuffer(expectedLog.hash()), + startCounter, + new Fr(expectedLog.length + 4), + ).scope(AztecAddress.fromField(address)); + + const circuitPublicInputs = toVMCircuitPublicInputs(trace); + expect(trace.getUnencryptedLogs()).toEqual([expectedLog]); + expect(circuitPublicInputs.accumulatedData.unencryptedLogsHashes).toEqual(expectedArray); + }); + + it('Should trace get contract instance', () => { + const instance = randomTracedContractInstance(); + const { version: _, ...instanceWithoutVersion } = instance; + trace.traceGetContractInstance(instance); + expect(trace.getCounter()).toBe(startCounterPlus1); + + //const circuitPublicInputs = toVMCircuitPublicInputs(trace); + // TODO(dbanks12): once this emits nullifier read, check here + expect(trace.getAvmCircuitHints().contractInstances.items).toEqual([ + { + // hint omits "version" and has "exists" as an Fr + ...instanceWithoutVersion, + exists: new Fr(instance.exists), + }, + ]); + }); + describe('Maximum accesses', () => { + it('Should enforce maximum number of public storage reads', () => { + for (let i = 0; i < MAX_PUBLIC_DATA_READS_PER_TX; i++) { + trace.tracePublicStorageRead(new Fr(i), new Fr(i), new Fr(i), true, true); + } + expect(() => trace.tracePublicStorageRead(new Fr(42), new Fr(42), new Fr(42), true, true)).toThrow( + SideEffectLimitReachedError, + ); + }); + + it('Should enforce maximum number of public storage writes', () => { + for (let i = 0; i < MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX; i++) { + trace.tracePublicStorageWrite(new Fr(i), new Fr(i), new Fr(i)); + } + expect(() => trace.tracePublicStorageWrite(new Fr(42), new Fr(42), new Fr(42))).toThrow( + SideEffectLimitReachedError, + ); + }); + + it('Should enforce maximum number of note hash checks', () => { + for (let i = 0; i < MAX_NOTE_HASH_READ_REQUESTS_PER_TX; i++) { + trace.traceNoteHashCheck(new Fr(i), new Fr(i), new Fr(i), true); + } + expect(() => trace.traceNoteHashCheck(new Fr(42), new Fr(42), new Fr(42), true)).toThrow( + SideEffectLimitReachedError, + ); + }); + + it('Should enforce maximum number of new note hashes', () => { + for (let i = 0; i < MAX_NOTE_HASHES_PER_TX; i++) { + trace.traceNewNoteHash(new Fr(i), new Fr(i)); + } + expect(() => trace.traceNewNoteHash(new Fr(42), new Fr(42))).toThrow(SideEffectLimitReachedError); + }); + + it('Should enforce maximum number of nullifier checks', () => { + for (let i = 0; i < MAX_NULLIFIER_READ_REQUESTS_PER_TX; i++) { + trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), true, true); + } + expect(() => trace.traceNullifierCheck(new Fr(42), new Fr(42), new Fr(42), true, true)).toThrow( + SideEffectLimitReachedError, + ); + // NOTE: also cannot do a non-existent check once existent checks have filled up + expect(() => trace.traceNullifierCheck(new Fr(42), new Fr(42), new Fr(42), false, true)).toThrow( + SideEffectLimitReachedError, + ); + }); + + it('Should enforce maximum number of nullifier non-existent checks', () => { + for (let i = 0; i < MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX; i++) { + trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), false, true); + } + expect(() => trace.traceNullifierCheck(new Fr(42), new Fr(42), new Fr(42), false, true)).toThrow( + SideEffectLimitReachedError, + ); + // NOTE: also cannot do a existent check once non-existent checks have filled up + expect(() => trace.traceNullifierCheck(new Fr(42), new Fr(42), new Fr(42), true, true)).toThrow( + SideEffectLimitReachedError, + ); + }); + + it('Should enforce maximum number of new nullifiers', () => { + for (let i = 0; i < MAX_NULLIFIERS_PER_TX; i++) { + trace.traceNewNullifier(new Fr(i), new Fr(i)); + } + expect(() => trace.traceNewNullifier(new Fr(42), new Fr(42))).toThrow(SideEffectLimitReachedError); + }); + + it('Should enforce maximum number of L1 to L2 message checks', () => { + for (let i = 0; i < MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX; i++) { + trace.traceL1ToL2MessageCheck(new Fr(i), new Fr(i), new Fr(i), true); + } + expect(() => trace.traceL1ToL2MessageCheck(new Fr(42), new Fr(42), new Fr(42), true)).toThrow( + SideEffectLimitReachedError, + ); + }); + + it('Should enforce maximum number of new l2 to l1 messages', () => { + for (let i = 0; i < MAX_L2_TO_L1_MSGS_PER_TX; i++) { + trace.traceNewL2ToL1Message(new Fr(i), new Fr(i), new Fr(i)); + } + expect(() => trace.traceNewL2ToL1Message(new Fr(42), new Fr(42), new Fr(42))).toThrow( + SideEffectLimitReachedError, + ); + }); + + it('Should enforce maximum number of new logs hashes', () => { + for (let i = 0; i < MAX_UNENCRYPTED_LOGS_PER_TX; i++) { + trace.traceUnencryptedLog(new Fr(i), [new Fr(i), new Fr(i)]); + } + expect(() => trace.traceUnencryptedLog(new Fr(42), [new Fr(42), new Fr(42)])).toThrow( + SideEffectLimitReachedError, + ); + }); + + it('Should enforce maximum number of nullifier checks for GETCONTRACTINSTANCE', () => { + for (let i = 0; i < MAX_NULLIFIER_READ_REQUESTS_PER_TX; i++) { + trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), true, true); + } + expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: true })).toThrow( + SideEffectLimitReachedError, + ); + // NOTE: also cannot do a existent check once non-existent checks have filled up + expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: false })).toThrow( + SideEffectLimitReachedError, + ); + }); + + it('Should enforce maximum number of nullifier non-existent checks for GETCONTRACTINSTANCE', () => { + for (let i = 0; i < MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX; i++) { + trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), false, true); + } + expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: false })).toThrow( + SideEffectLimitReachedError, + ); + // NOTE: also cannot do a existent check once non-existent checks have filled up + expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: true })).toThrow( + SideEffectLimitReachedError, + ); + }); + + it('PreviousValidationRequestArrayLengths and PreviousAccumulatedDataArrayLengths contribute to limits', () => { + trace = new PublicEnqueuedCallSideEffectTrace( + 0, + new PublicValidationRequestArrayLengths( + MAX_NOTE_HASH_READ_REQUESTS_PER_TX, + MAX_NULLIFIER_READ_REQUESTS_PER_TX, + MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX, + MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX, + MAX_PUBLIC_DATA_READS_PER_TX, + ), + new PublicAccumulatedDataArrayLengths( + MAX_NOTE_HASHES_PER_TX, + MAX_NULLIFIERS_PER_TX, + MAX_L2_TO_L1_MSGS_PER_TX, + 0, + 0, + MAX_UNENCRYPTED_LOGS_PER_TX, + MAX_PUBLIC_DATA_READS_PER_TX, + 0, + ), + ); + expect(() => trace.tracePublicStorageRead(new Fr(42), new Fr(42), new Fr(42), true, true)).toThrow( + SideEffectLimitReachedError, + ); + expect(() => trace.tracePublicStorageWrite(new Fr(42), new Fr(42), new Fr(42))).toThrow( + SideEffectLimitReachedError, + ); + expect(() => trace.traceNoteHashCheck(new Fr(42), new Fr(42), new Fr(42), true)).toThrow( + SideEffectLimitReachedError, + ); + expect(() => trace.traceNewNoteHash(new Fr(42), new Fr(42))).toThrow(SideEffectLimitReachedError); + expect(() => trace.traceNullifierCheck(new Fr(42), new Fr(42), new Fr(42), false, true)).toThrow( + SideEffectLimitReachedError, + ); + expect(() => trace.traceNullifierCheck(new Fr(42), new Fr(42), new Fr(42), true, true)).toThrow( + SideEffectLimitReachedError, + ); + expect(() => trace.traceNewNullifier(new Fr(42), new Fr(42))).toThrow(SideEffectLimitReachedError); + expect(() => trace.traceL1ToL2MessageCheck(new Fr(42), new Fr(42), new Fr(42), true)).toThrow( + SideEffectLimitReachedError, + ); + expect(() => trace.traceNewL2ToL1Message(new Fr(42), new Fr(42), new Fr(42))).toThrow( + SideEffectLimitReachedError, + ); + expect(() => trace.traceUnencryptedLog(new Fr(42), [new Fr(42), new Fr(42)])).toThrow( + SideEffectLimitReachedError, + ); + expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: false })).toThrow( + SideEffectLimitReachedError, + ); + expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: true })).toThrow( + SideEffectLimitReachedError, + ); + }); + }); + + describe.each([avmCallResults, avmCallRevertedResults])('Should trace & absorb nested calls', callResults => { + it(`${callResults.reverted ? 'Reverted' : 'Successful'} calls should be traced and absorbed properly`, () => { + const existsDefault = true; + const cached = false; + const isPending = false; + + const nestedTrace = new PublicEnqueuedCallSideEffectTrace(startCounter); + let testCounter = startCounter; + nestedTrace.tracePublicStorageRead(address, slot, value, existsDefault, cached); + testCounter++; + nestedTrace.tracePublicStorageWrite(address, slot, value); + testCounter++; + nestedTrace.traceNoteHashCheck(address, utxo, leafIndex, existsDefault); + // counter does not increment for note hash checks + nestedTrace.traceNewNoteHash(address, utxo); + testCounter++; + nestedTrace.traceNullifierCheck(address, utxo, leafIndex, /*exists=*/ true, isPending); + testCounter++; + nestedTrace.traceNullifierCheck(address, utxo, leafIndex, /*exists=*/ false, isPending); + testCounter++; + nestedTrace.traceNewNullifier(address, utxo); + testCounter++; + nestedTrace.traceL1ToL2MessageCheck(address, utxo, leafIndex, existsDefault); + // counter does not increment for l1tol2 message checks + nestedTrace.traceNewL2ToL1Message(address, recipient, content); + testCounter++; + nestedTrace.traceUnencryptedLog(address, log); + testCounter++; + nestedTrace.traceGetContractInstance({ ...contractInstance, exists: true }); + testCounter++; + nestedTrace.traceGetContractInstance({ ...contractInstance, exists: false }); + testCounter++; + + trace.traceNestedCall(nestedTrace, avmEnvironment, startGasLeft, endGasLeft, bytecode, callResults); + + // parent trace adopts nested call's counter + expect(trace.getCounter()).toBe(testCounter); + + // parent absorbs child's side effects + const parentSideEffects = trace.getSideEffects(); + const childSideEffects = nestedTrace.getSideEffects(); + if (callResults.reverted) { + expect(parentSideEffects.contractStorageReads).toEqual(childSideEffects.contractStorageReads); + expect(parentSideEffects.contractStorageUpdateRequests).toEqual(childSideEffects.contractStorageUpdateRequests); + expect(parentSideEffects.noteHashReadRequests).toEqual(childSideEffects.noteHashReadRequests); + 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.l2ToL1Msgs).toEqual([]); + expect(parentSideEffects.unencryptedLogs).toEqual([]); + expect(parentSideEffects.unencryptedLogsHashes).toEqual([]); + } else { + expect(parentSideEffects).toEqual(childSideEffects); + } + }); + }); +}); 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 new file mode 100644 index 00000000000..4254087aaf8 --- /dev/null +++ b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.ts @@ -0,0 +1,555 @@ +import { UnencryptedL2Log } from '@aztec/circuit-types'; +import { + AvmContractInstanceHint, + AvmExecutionHints, + AvmExternalCallHint, + AvmKeyValueHint, + AztecAddress, + CallContext, + type CombinedConstantData, + ContractStorageRead, + ContractStorageUpdateRequest, + EthAddress, + Gas, + L2ToL1Message, + LogHash, + MAX_ENCRYPTED_LOGS_PER_TX, + MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX, + MAX_L2_TO_L1_MSGS_PER_TX, + MAX_NOTE_ENCRYPTED_LOGS_PER_TX, + MAX_NOTE_HASHES_PER_TX, + MAX_NOTE_HASH_READ_REQUESTS_PER_TX, + MAX_NULLIFIERS_PER_TX, + MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX, + MAX_NULLIFIER_READ_REQUESTS_PER_TX, + MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, + MAX_PUBLIC_DATA_READS_PER_TX, + MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, + MAX_UNENCRYPTED_LOGS_PER_TX, + NoteHash, + Nullifier, + PublicAccumulatedData, + PublicAccumulatedDataArrayLengths, + PublicCallRequest, + PublicDataRead, + PublicDataUpdateRequest, + PublicInnerCallRequest, + PublicValidationRequestArrayLengths, + PublicValidationRequests, + ReadRequest, + RollupValidationRequests, + ScopedL2ToL1Message, + ScopedLogHash, + ScopedNoteHash, + type ScopedNullifier, + ScopedReadRequest, + TreeLeafReadRequest, + VMCircuitPublicInputs, +} from '@aztec/circuits.js'; +import { computeVarArgsHash } from '@aztec/circuits.js/hash'; +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 ContractInstanceWithAddress } from '@aztec/types/contracts'; + +import { type AvmContractCallResult } from '../avm/avm_contract_call_result.js'; +import { type AvmExecutionEnvironment } from '../avm/avm_execution_environment.js'; +import { SideEffectLimitReachedError } from './side_effect_errors.js'; +import { type PublicSideEffectTraceInterface } from './side_effect_trace_interface.js'; + +export type TracedContractInstance = { exists: boolean } & ContractInstanceWithAddress; + +/** + * A struct containing just the side effects as regular arrays + * as opposed to "Tuple" arrays used by circuit public inputs. + * This struct is helpful for testing and checking array lengths. + **/ +export type SideEffects = { + contractStorageReads: ContractStorageRead[]; + contractStorageUpdateRequests: ContractStorageUpdateRequest[]; + + noteHashReadRequests: TreeLeafReadRequest[]; + noteHashes: ScopedNoteHash[]; + + nullifierReadRequests: ScopedReadRequest[]; + nullifierNonExistentReadRequests: ScopedReadRequest[]; + nullifiers: ScopedNullifier[]; + + l1ToL2MsgReadRequests: TreeLeafReadRequest[]; + l2ToL1Msgs: ScopedL2ToL1Message[]; + + unencryptedLogs: UnencryptedL2Log[]; + unencryptedLogsHashes: ScopedLogHash[]; +}; + +/** + * Trace side effects for an entire enqueued call. + */ +export class PublicEnqueuedCallSideEffectTrace implements PublicSideEffectTraceInterface { + public logger = createDebugLogger('aztec:public_side_effect_trace'); + + /** The side effect counter increments with every call to the trace. */ + private sideEffectCounter: number; + + // TODO(dbanks12): make contract address mandatory in ContractStorage* structs, + // and include it in serialization, or modify PublicData* structs for this. + private contractStorageReads: ContractStorageRead[] = []; + private contractStorageUpdateRequests: ContractStorageUpdateRequest[] = []; + + private noteHashReadRequests: TreeLeafReadRequest[] = []; + private noteHashes: ScopedNoteHash[] = []; + + private nullifierReadRequests: ScopedReadRequest[] = []; + private nullifierNonExistentReadRequests: ScopedReadRequest[] = []; + private nullifiers: ScopedNullifier[] = []; + + private l1ToL2MsgReadRequests: TreeLeafReadRequest[] = []; + private l2ToL1Msgs: ScopedL2ToL1Message[] = []; + + private unencryptedLogs: UnencryptedL2Log[] = []; + private unencryptedLogsHashes: ScopedLogHash[] = []; + + private avmCircuitHints: AvmExecutionHints; + + constructor( + /** The counter of this trace's first side effect. */ + public readonly startSideEffectCounter: number = 0, + /** Track parent's (or previous kernel's) lengths so the AVM can properly enforce TX-wide limits, + * otherwise the public kernel can fail to prove because TX limits are breached. + */ + private readonly previousValidationRequestArrayLengths: PublicValidationRequestArrayLengths = PublicValidationRequestArrayLengths.empty(), + private readonly previousAccumulatedDataArrayLengths: PublicAccumulatedDataArrayLengths = PublicAccumulatedDataArrayLengths.empty(), + ) { + this.sideEffectCounter = startSideEffectCounter; + this.avmCircuitHints = AvmExecutionHints.empty(); + } + + public fork() { + return new PublicEnqueuedCallSideEffectTrace( + this.sideEffectCounter, + new PublicValidationRequestArrayLengths( + this.previousValidationRequestArrayLengths.noteHashReadRequests + this.noteHashReadRequests.length, + this.previousValidationRequestArrayLengths.nullifierReadRequests + this.nullifierReadRequests.length, + this.previousValidationRequestArrayLengths.nullifierNonExistentReadRequests + + this.nullifierNonExistentReadRequests.length, + this.previousValidationRequestArrayLengths.l1ToL2MsgReadRequests + this.l1ToL2MsgReadRequests.length, + this.previousValidationRequestArrayLengths.publicDataReads + this.contractStorageReads.length, + ), + new PublicAccumulatedDataArrayLengths( + this.previousAccumulatedDataArrayLengths.noteHashes + this.noteHashes.length, + this.previousAccumulatedDataArrayLengths.nullifiers + this.nullifiers.length, + this.previousAccumulatedDataArrayLengths.l2ToL1Msgs + this.l2ToL1Msgs.length, + this.previousAccumulatedDataArrayLengths.noteEncryptedLogsHashes, + this.previousAccumulatedDataArrayLengths.encryptedLogsHashes, + this.previousAccumulatedDataArrayLengths.unencryptedLogsHashes + this.unencryptedLogsHashes.length, + this.previousAccumulatedDataArrayLengths.publicDataUpdateRequests + this.contractStorageUpdateRequests.length, + this.previousAccumulatedDataArrayLengths.publicCallStack, + ), + ); + } + + public getCounter() { + return this.sideEffectCounter; + } + + private incrementSideEffectCounter() { + this.sideEffectCounter++; + } + + public tracePublicStorageRead(storageAddress: Fr, slot: Fr, value: Fr, _exists: boolean, _cached: boolean) { + // NOTE: exists and cached are unused for now but may be used for optimizations or kernel hints later + if ( + this.contractStorageReads.length + this.previousValidationRequestArrayLengths.publicDataReads >= + MAX_PUBLIC_DATA_READS_PER_TX + ) { + throw new SideEffectLimitReachedError('contract storage read', MAX_PUBLIC_DATA_READS_PER_TX); + } + + this.contractStorageReads.push( + new ContractStorageRead(slot, value, this.sideEffectCounter, AztecAddress.fromField(storageAddress)), + ); + this.avmCircuitHints.storageValues.items.push( + new AvmKeyValueHint(/*key=*/ new Fr(this.sideEffectCounter), /*value=*/ value), + ); + this.logger.debug(`SLOAD cnt: ${this.sideEffectCounter} val: ${value} slot: ${slot}`); + this.incrementSideEffectCounter(); + } + + public tracePublicStorageWrite(storageAddress: Fr, slot: Fr, value: Fr) { + if ( + this.contractStorageUpdateRequests.length + this.previousAccumulatedDataArrayLengths.publicDataUpdateRequests >= + MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX + ) { + throw new SideEffectLimitReachedError('contract storage write', MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX); + } + + this.contractStorageUpdateRequests.push( + new ContractStorageUpdateRequest(slot, value, this.sideEffectCounter, storageAddress), + ); + this.logger.debug(`SSTORE cnt: ${this.sideEffectCounter} val: ${value} slot: ${slot}`); + this.incrementSideEffectCounter(); + } + + // TODO(8287): _exists can be removed once we have the vm properly handling the equality check + public traceNoteHashCheck(_storageAddress: Fr, noteHash: Fr, leafIndex: Fr, exists: boolean) { + // NOTE: storageAddress is unused because noteHash is an already-siloed leaf + if ( + this.noteHashReadRequests.length + this.previousValidationRequestArrayLengths.noteHashReadRequests >= + MAX_NOTE_HASH_READ_REQUESTS_PER_TX + ) { + throw new SideEffectLimitReachedError('note hash read request', MAX_NOTE_HASH_READ_REQUESTS_PER_TX); + } + + this.noteHashReadRequests.push(new TreeLeafReadRequest(noteHash, leafIndex)); + this.avmCircuitHints.noteHashExists.items.push( + new AvmKeyValueHint(/*key=*/ new Fr(leafIndex), /*value=*/ exists ? Fr.ONE : Fr.ZERO), + ); + // NOTE: counter does not increment for note hash checks (because it doesn't rely on pending note hashes) + } + + public traceNewNoteHash(storageAddress: Fr, noteHash: Fr) { + if (this.noteHashes.length + this.previousAccumulatedDataArrayLengths.noteHashes >= MAX_NOTE_HASHES_PER_TX) { + throw new SideEffectLimitReachedError('note hash', MAX_NOTE_HASHES_PER_TX); + } + + this.noteHashes.push(new NoteHash(noteHash, this.sideEffectCounter).scope(AztecAddress.fromField(storageAddress))); + this.logger.debug(`NEW_NOTE_HASH cnt: ${this.sideEffectCounter}`); + this.incrementSideEffectCounter(); + } + + public traceNullifierCheck(storageAddress: Fr, nullifier: Fr, _leafIndex: Fr, exists: boolean, _isPending: boolean) { + // NOTE: isPending and leafIndex are unused for now but may be used for optimizations or kernel hints later + this.enforceLimitOnNullifierChecks(); + + const readRequest = new ReadRequest(nullifier, this.sideEffectCounter).scope( + AztecAddress.fromField(storageAddress), + ); + if (exists) { + this.nullifierReadRequests.push(readRequest); + } else { + this.nullifierNonExistentReadRequests.push(readRequest); + } + this.avmCircuitHints.nullifierExists.items.push( + new AvmKeyValueHint(/*key=*/ new Fr(this.sideEffectCounter), /*value=*/ new Fr(exists ? 1 : 0)), + ); + this.logger.debug(`NULLIFIER_EXISTS cnt: ${this.sideEffectCounter}`); + this.incrementSideEffectCounter(); + } + + public traceNewNullifier(storageAddress: Fr, nullifier: Fr) { + if (this.nullifiers.length + this.previousAccumulatedDataArrayLengths.nullifiers >= MAX_NULLIFIERS_PER_TX) { + throw new SideEffectLimitReachedError('nullifier', MAX_NULLIFIERS_PER_TX); + } + + this.nullifiers.push( + new Nullifier(nullifier, this.sideEffectCounter, /*noteHash=*/ Fr.ZERO).scope( + AztecAddress.fromField(storageAddress), + ), + ); + this.logger.debug(`NEW_NULLIFIER cnt: ${this.sideEffectCounter}`); + this.incrementSideEffectCounter(); + } + + // TODO(8287): _exists can be removed once we have the vm properly handling the equality check + public traceL1ToL2MessageCheck(_contractAddress: Fr, msgHash: Fr, msgLeafIndex: Fr, exists: boolean) { + // NOTE: contractAddress is unused because msgHash is an already-siloed leaf + if ( + this.l1ToL2MsgReadRequests.length + this.previousValidationRequestArrayLengths.l1ToL2MsgReadRequests >= + MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX + ) { + throw new SideEffectLimitReachedError('l1 to l2 message read request', MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX); + } + + if (this.l1ToL2MsgReadRequests.length >= MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX) { + throw new SideEffectLimitReachedError('l1 to l2 message read request', MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX); + } + this.l1ToL2MsgReadRequests.push(new TreeLeafReadRequest(msgHash, msgLeafIndex)); + this.avmCircuitHints.l1ToL2MessageExists.items.push( + new AvmKeyValueHint(/*key=*/ new Fr(msgLeafIndex), /*value=*/ exists ? Fr.ONE : Fr.ZERO), + ); + } + + public traceNewL2ToL1Message(contractAddress: Fr, recipient: Fr, content: Fr) { + if (this.l2ToL1Msgs.length + this.previousAccumulatedDataArrayLengths.l2ToL1Msgs >= MAX_L2_TO_L1_MSGS_PER_TX) { + throw new SideEffectLimitReachedError('l2 to l1 message', MAX_L2_TO_L1_MSGS_PER_TX); + } + + const recipientAddress = EthAddress.fromField(recipient); + this.l2ToL1Msgs.push( + new L2ToL1Message(recipientAddress, content, this.sideEffectCounter).scope( + AztecAddress.fromField(contractAddress), + ), + ); + this.logger.debug(`NEW_L2_TO_L1_MSG cnt: ${this.sideEffectCounter}`); + this.incrementSideEffectCounter(); + } + + public traceUnencryptedLog(contractAddress: Fr, log: Fr[]) { + if ( + this.unencryptedLogs.length + this.previousAccumulatedDataArrayLengths.unencryptedLogsHashes >= + MAX_UNENCRYPTED_LOGS_PER_TX + ) { + throw new SideEffectLimitReachedError('unencrypted log', MAX_UNENCRYPTED_LOGS_PER_TX); + } + + const ulog = new UnencryptedL2Log( + AztecAddress.fromField(contractAddress), + Buffer.concat(log.map(f => f.toBuffer())), + ); + const basicLogHash = Fr.fromBuffer(ulog.hash()); + this.unencryptedLogs.push(ulog); + // This length is for charging DA and is checked on-chain - has to be length of log preimage + 4 bytes. + // The .length call also has a +4 but that is unrelated + this.unencryptedLogsHashes.push( + new LogHash(basicLogHash, this.sideEffectCounter, new Fr(ulog.length + 4)).scope( + AztecAddress.fromField(contractAddress), + ), + ); + this.logger.debug(`NEW_UNENCRYPTED_LOG cnt: ${this.sideEffectCounter}`); + this.incrementSideEffectCounter(); + } + + public traceGetContractInstance(instance: TracedContractInstance) { + this.enforceLimitOnNullifierChecks('(contract address nullifier from GETCONTRACTINSTANCE)'); + // TODO(dbanks12): should emit a nullifier read request + + this.avmCircuitHints.contractInstances.items.push( + new AvmContractInstanceHint( + instance.address, + new Fr(instance.exists ? 1 : 0), + instance.salt, + instance.deployer, + instance.contractClassId, + instance.initializationHash, + instance.publicKeysHash, + ), + ); + this.logger.debug(`CONTRACT_INSTANCE cnt: ${this.sideEffectCounter}`); + this.incrementSideEffectCounter(); + } + + /** + * Trace a nested call. + * Accept some results from a finished nested call's trace into this one. + */ + public traceNestedCall( + /** The trace of the nested call. */ + nestedCallTrace: this, + /** The execution environment of the nested call. */ + _nestedEnvironment: AvmExecutionEnvironment, + /** How much gas was available for this public execution. */ + startGasLeft: Gas, + /** How much gas was left after this public execution. */ + endGasLeft: Gas, + /** Bytecode used for this execution. */ + _bytecode: Buffer, + /** The call's results */ + avmCallResults: AvmContractCallResult, + /** Function name for logging */ + _functionName: string = 'unknown', + ) { + // 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.absorbRevertedNestedTrace(nestedCallTrace); + } else { + this.absorbSuccessfulNestedTrace(nestedCallTrace); + } + + const gasUsed = new Gas(startGasLeft.daGas - endGasLeft.daGas, startGasLeft.l2Gas - endGasLeft.l2Gas); + + this.avmCircuitHints.externalCalls.items.push( + new AvmExternalCallHint( + /*success=*/ new Fr(avmCallResults.reverted ? 0 : 1), + avmCallResults.output, + gasUsed, + endSideEffectCounter, + ), + ); + } + + public absorbSuccessfulNestedTrace(nestedTrace: this) { + this.sideEffectCounter = nestedTrace.sideEffectCounter; + this.contractStorageReads.push(...nestedTrace.contractStorageReads); + this.contractStorageUpdateRequests.push(...nestedTrace.contractStorageUpdateRequests); + this.noteHashReadRequests.push(...nestedTrace.noteHashReadRequests); + this.noteHashes.push(...nestedTrace.noteHashes); + this.nullifierReadRequests.push(...nestedTrace.nullifierReadRequests); + this.nullifierNonExistentReadRequests.push(...nestedTrace.nullifierNonExistentReadRequests); + this.nullifiers.push(...nestedTrace.nullifiers); + this.l1ToL2MsgReadRequests.push(...nestedTrace.l1ToL2MsgReadRequests); + this.l2ToL1Msgs.push(...nestedTrace.l2ToL1Msgs); + this.unencryptedLogs.push(...nestedTrace.unencryptedLogs); + this.unencryptedLogsHashes.push(...nestedTrace.unencryptedLogsHashes); + } + + public absorbRevertedNestedTrace(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. + + // TODO(dbanks12): What should happen to side effect counter on revert? + this.sideEffectCounter = nestedTrace.sideEffectCounter; + this.contractStorageReads.push(...nestedTrace.contractStorageReads); + this.contractStorageUpdateRequests.push(...nestedTrace.contractStorageUpdateRequests); + 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 + } + + public getSideEffects(): SideEffects { + return { + contractStorageReads: this.contractStorageReads, + contractStorageUpdateRequests: this.contractStorageUpdateRequests, + noteHashReadRequests: this.noteHashReadRequests, + noteHashes: this.noteHashes, + nullifierReadRequests: this.nullifierReadRequests, + nullifierNonExistentReadRequests: this.nullifierNonExistentReadRequests, + nullifiers: this.nullifiers, + l1ToL2MsgReadRequests: this.l1ToL2MsgReadRequests, + l2ToL1Msgs: this.l2ToL1Msgs, + unencryptedLogs: this.unencryptedLogs, + unencryptedLogsHashes: this.unencryptedLogsHashes, + }; + } + + public toVMCircuitPublicInputs( + /** Constants. */ + constants: CombinedConstantData, + /** The execution environment of the nested call. */ + avmEnvironment: AvmExecutionEnvironment, + /** How much gas was available for this public execution. */ + startGasLeft: Gas, + /** How much gas was left after this public execution. */ + endGasLeft: Gas, + /** The call's results */ + avmCallResults: AvmContractCallResult, + ): VMCircuitPublicInputs { + return new VMCircuitPublicInputs( + /*constants=*/ constants, + /*callRequest=*/ createPublicCallRequest(avmEnvironment), + /*publicCallStack=*/ makeTuple(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, PublicInnerCallRequest.empty), + /*previousValidationRequestArrayLengths=*/ this.previousValidationRequestArrayLengths, + /*validationRequests=*/ this.getValidationRequests(), + /*previousAccumulatedDataArrayLengths=*/ this.previousAccumulatedDataArrayLengths, + /*accumulatedData=*/ this.getAccumulatedData(startGasLeft.sub(endGasLeft)), + /*startSideEffectCounter=*/ this.startSideEffectCounter, + /*endSideEffectCounter=*/ this.sideEffectCounter, + /*startGasLeft=*/ startGasLeft, + // TODO(dbanks12): should have endGasLeft + /*transactionFee=*/ avmEnvironment.transactionFee, + /*reverted=*/ avmCallResults.reverted, + ); + } + + public getUnencryptedLogs() { + return this.unencryptedLogs; + } + + public getAvmCircuitHints() { + return this.avmCircuitHints; + } + + private getValidationRequests() { + return new PublicValidationRequests( + RollupValidationRequests.empty(), // TODO(dbanks12): what should this be? + padArrayEnd(this.noteHashReadRequests, TreeLeafReadRequest.empty(), MAX_NOTE_HASH_READ_REQUESTS_PER_TX), + padArrayEnd(this.nullifierReadRequests, ScopedReadRequest.empty(), MAX_NULLIFIER_READ_REQUESTS_PER_TX), + padArrayEnd( + this.nullifierNonExistentReadRequests, + ScopedReadRequest.empty(), + MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX, + ), + padArrayEnd(this.l1ToL2MsgReadRequests, TreeLeafReadRequest.empty(), MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX), + // TODO(dbanks12): this is only necessary until VMCircuitPublicInputs uses unsiloed storage slots and pairs storage accesses with contract address + padArrayEnd( + this.contractStorageReads.map(r => new PublicDataRead(r.storageSlot, r.currentValue, r.counter)), + PublicDataRead.empty(), + MAX_PUBLIC_DATA_READS_PER_TX, + ), + ); + } + + private getAccumulatedData(gasUsed: Gas) { + return new PublicAccumulatedData( + padArrayEnd(this.noteHashes, ScopedNoteHash.empty(), MAX_NOTE_HASHES_PER_TX), + // TODO(dbanks12): should be able to use ScopedNullifier here + padArrayEnd( + this.nullifiers.map(n => new Nullifier(n.nullifier.value, n.nullifier.counter, n.nullifier.noteHash)), + Nullifier.empty(), + MAX_NULLIFIERS_PER_TX, + ), + padArrayEnd(this.l2ToL1Msgs, ScopedL2ToL1Message.empty(), MAX_L2_TO_L1_MSGS_PER_TX), + /*noteEncryptedLogsHashes=*/ makeTuple(MAX_NOTE_ENCRYPTED_LOGS_PER_TX, LogHash.empty), + /*encryptedLogsHashes=*/ makeTuple(MAX_ENCRYPTED_LOGS_PER_TX, ScopedLogHash.empty), + padArrayEnd(this.unencryptedLogsHashes, ScopedLogHash.empty(), MAX_UNENCRYPTED_LOGS_PER_TX), + // TODO(dbanks12): this is only necessary until VMCircuitPublicInputs uses unsiloed storage slots and pairs storage accesses with contract address + padArrayEnd( + this.contractStorageUpdateRequests.map(w => new PublicDataUpdateRequest(w.storageSlot, w.newValue, w.counter)), + PublicDataUpdateRequest.empty(), + MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, + ), + /*publicCallStack=*/ makeTuple(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, PublicCallRequest.empty), + /*gasUsed=*/ gasUsed, + ); + } + + private enforceLimitOnNullifierChecks(errorMsgOrigin: string = '') { + // NOTE: Why error if _either_ limit was reached? If user code emits either an existent or non-existent + // nullifier read request (NULLIFIEREXISTS, GETCONTRACTINSTANCE, *CALL), and one of the limits has been + // reached (MAX_NULLIFIER_NON_EXISTENT_RRS vs MAX_NULLIFIER_RRS), but not the other, we must prevent the + // sequencer from lying and saying "this nullifier exists, but MAX_NULLIFIER_RRS has been reached, so I'm + // going to skip the read request and just revert instead" when the nullifier actually doesn't exist + // (or vice versa). So, if either maximum has been reached, any nullifier-reading operation must error. + if ( + this.nullifierReadRequests.length + this.previousValidationRequestArrayLengths.nullifierReadRequests >= + MAX_NULLIFIER_READ_REQUESTS_PER_TX + ) { + throw new SideEffectLimitReachedError( + `nullifier read request ${errorMsgOrigin}`, + MAX_NULLIFIER_READ_REQUESTS_PER_TX, + ); + } + if ( + this.nullifierNonExistentReadRequests.length + + this.previousValidationRequestArrayLengths.nullifierNonExistentReadRequests >= + MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX + ) { + throw new SideEffectLimitReachedError( + `nullifier non-existent read request ${errorMsgOrigin}`, + MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX, + ); + } + } +} + +/** + * Helper function to create a public execution request from an AVM execution environment + */ +function createPublicCallRequest(avmEnvironment: AvmExecutionEnvironment): PublicCallRequest { + const callContext = CallContext.from({ + msgSender: avmEnvironment.sender, + storageContractAddress: avmEnvironment.storageAddress, + functionSelector: avmEnvironment.functionSelector, + isDelegateCall: avmEnvironment.isDelegateCall, + isStaticCall: avmEnvironment.isStaticCall, + }); + return new PublicCallRequest( + avmEnvironment.address, + callContext, + computeVarArgsHash(avmEnvironment.calldata), + /*counter=*/ 0, + ); +} diff --git a/yarn-project/simulator/src/public/enqueued_call_simulator.ts b/yarn-project/simulator/src/public/enqueued_call_simulator.ts index b2f5a12d9e4..2ddfb2ac99a 100644 --- a/yarn-project/simulator/src/public/enqueued_call_simulator.ts +++ b/yarn-project/simulator/src/public/enqueued_call_simulator.ts @@ -123,33 +123,43 @@ export class EnqueuedCallSimulator { ): Promise { const pendingNullifiers = this.getSiloedPendingNullifiers(previousPublicKernelOutput); const startSideEffectCounter = previousPublicKernelOutput.endSideEffectCounter + 1; + + const prevAccumulatedData = + phase === PublicKernelPhase.SETUP + ? previousPublicKernelOutput.endNonRevertibleData + : previousPublicKernelOutput.end; + const previousValidationRequestArrayLengths = PublicValidationRequestArrayLengths.new( + previousPublicKernelOutput.validationRequests, + ); + const previousAccumulatedDataArrayLengths = PublicAccumulatedDataArrayLengths.new(prevAccumulatedData); + const result = await this.publicExecutor.simulate( executionRequest, - this.globalVariables, + previousPublicKernelOutput.constants, availableGas, tx.data.constants.txContext, pendingNullifiers, transactionFee, startSideEffectCounter, + previousValidationRequestArrayLengths, + previousAccumulatedDataArrayLengths, ); const callStack = makeTuple(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, PublicInnerCallRequest.empty); callStack[0].item.contractAddress = callRequest.contractAddress; callStack[0].item.callContext = callRequest.callContext; callStack[0].item.argsHash = callRequest.argsHash; - const prevAccumulatedData = - phase === PublicKernelPhase.SETUP - ? previousPublicKernelOutput.endNonRevertibleData - : previousPublicKernelOutput.end; + const accumulatedData = PublicAccumulatedData.empty(); accumulatedData.publicCallStack[0] = callRequest; + const startVMCircuitOutput = new VMCircuitPublicInputs( previousPublicKernelOutput.constants, callRequest, callStack, - PublicValidationRequestArrayLengths.new(previousPublicKernelOutput.validationRequests), + previousValidationRequestArrayLengths, PublicValidationRequests.empty(), - PublicAccumulatedDataArrayLengths.new(prevAccumulatedData), + previousAccumulatedDataArrayLengths, accumulatedData, startSideEffectCounter, startSideEffectCounter, diff --git a/yarn-project/simulator/src/public/executor.ts b/yarn-project/simulator/src/public/executor.ts index 95204219af3..cb4c3ec7692 100644 --- a/yarn-project/simulator/src/public/executor.ts +++ b/yarn-project/simulator/src/public/executor.ts @@ -1,6 +1,15 @@ import { type PublicExecutionRequest } from '@aztec/circuit-types'; import { type AvmSimulationStats } from '@aztec/circuit-types/stats'; -import { Fr, Gas, type GlobalVariables, type Nullifier, type TxContext } from '@aztec/circuits.js'; +import { + type CombinedConstantData, + Fr, + Gas, + type GlobalVariables, + type Nullifier, + PublicAccumulatedDataArrayLengths, + PublicValidationRequestArrayLengths, + type TxContext, +} from '@aztec/circuits.js'; import { createDebugLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; import { type TelemetryClient } from '@aztec/telemetry-client'; @@ -11,6 +20,8 @@ import { AvmMachineState } from '../avm/avm_machine_state.js'; import { AvmSimulator } from '../avm/avm_simulator.js'; import { AvmPersistableStateManager } from '../avm/journal/index.js'; import { getPublicFunctionDebugName } from '../common/debug_fn_name.js'; +import { DualSideEffectTrace } from './dual_side_effect_trace.js'; +import { PublicEnqueuedCallSideEffectTrace } from './enqueued_call_side_effect_trace.js'; import { type PublicExecutionResult } from './execution.js'; import { ExecutorMetrics } from './executor_metrics.js'; import { type WorldStateDB } from './public_db_sources.js'; @@ -31,22 +42,26 @@ export class PublicExecutor { /** * Executes a public execution request. * @param executionRequest - The execution to run. - * @param globalVariables - The global variables to use. + * @param constants - The constants (including global variables) to use. * @param availableGas - The gas available at the start of this enqueued call. * @param txContext - Transaction context. * @param pendingSiloedNullifiers - The pending nullifier set from earlier parts of this TX. * @param transactionFee - Fee offered for this TX. * @param startSideEffectCounter - The counter of the first side-effect generated by this simulation. + * @param previousValidationRequestArrayLengths - Side effect array lengths from previous kernel + * @param previousAccumulatedDataArrayLengths - Side effect array lengths from previous kernel * @returns The result of execution, including the results of all nested calls. */ public async simulate( executionRequest: PublicExecutionRequest, - globalVariables: GlobalVariables, + constants: CombinedConstantData, availableGas: Gas, _txContext: TxContext, pendingSiloedNullifiers: Nullifier[], transactionFee: Fr = Fr.ZERO, startSideEffectCounter: number = 0, + previousValidationRequestArrayLengths: PublicValidationRequestArrayLengths = PublicValidationRequestArrayLengths.empty(), + previousAccumulatedDataArrayLengths: PublicAccumulatedDataArrayLengths = PublicAccumulatedDataArrayLengths.empty(), ): Promise { const address = executionRequest.contractAddress; const selector = executionRequest.callContext.functionSelector; @@ -55,14 +70,20 @@ export class PublicExecutor { PublicExecutor.log.verbose(`[AVM] Executing public external function ${fnName}@${address}.`); const timer = new Timer(); - const trace = new PublicSideEffectTrace(startSideEffectCounter); + const innerCallTrace = new PublicSideEffectTrace(startSideEffectCounter); + const enqueuedCallTrace = new PublicEnqueuedCallSideEffectTrace( + startSideEffectCounter, + previousValidationRequestArrayLengths, + previousAccumulatedDataArrayLengths, + ); + const trace = new DualSideEffectTrace(innerCallTrace, enqueuedCallTrace); const avmPersistableState = AvmPersistableStateManager.newWithPendingSiloedNullifiers( this.worldStateDB, trace, pendingSiloedNullifiers.map(n => n.value), ); - const avmExecutionEnv = createAvmExecutionEnvironment(executionRequest, globalVariables, transactionFee); + const avmExecutionEnv = createAvmExecutionEnvironment(executionRequest, constants.globalVariables, transactionFee); const avmMachineState = new AvmMachineState(availableGas); const avmContext = new AvmContext(avmPersistableState, avmExecutionEnv, avmMachineState); @@ -103,6 +124,14 @@ export class PublicExecutor { this.metrics.recordFunctionSimulation(bytecode.length, timer.ms()); } + const _vmCircuitPublicInputs = enqueuedCallTrace.toVMCircuitPublicInputs( + constants, + avmExecutionEnv, + /*startGasLeft=*/ availableGas, + /*endGasLeft=*/ Gas.from(avmContext.machineState.gasLeft), + avmResult, + ); + return publicExecutionResult; } } diff --git a/yarn-project/simulator/src/public/public_processor.test.ts b/yarn-project/simulator/src/public/public_processor.test.ts index 8ebde5516a7..db569517399 100644 --- a/yarn-project/simulator/src/public/public_processor.test.ts +++ b/yarn-project/simulator/src/public/public_processor.test.ts @@ -852,6 +852,8 @@ describe('public_processor', () => { expect.anything(), // pendingNullifiers new Fr(txFee), expect.anything(), // SideEffectCounter + expect.anything(), // PublicValidationRequestArrayLengths + expect.anything(), // PublicAccumulatedDataArrayLengths ]; expect(publicExecutor.simulate).toHaveBeenCalledTimes(3); diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 69f7884c697..b552fa454d7 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -12,8 +12,8 @@ import { import { type CircuitWitnessGenerationStats } from '@aztec/circuit-types/stats'; import { CallContext, + CombinedConstantData, Gas, - GlobalVariables, Header, type KeyValidationRequest, NULLIFIER_SUBTREE_HEIGHT, @@ -689,7 +689,7 @@ export class TXE implements TypedOracle { const executionResult = executor.simulate( execution, - GlobalVariables.empty(), + CombinedConstantData.empty(), Gas.test(), TxContext.empty(), /* pendingNullifiers */ [],