diff --git a/yarn-project/circuit-types/src/logs/unencrypted_l2_log.ts b/yarn-project/circuit-types/src/logs/unencrypted_l2_log.ts index 266167463403..3d5e5b3471fd 100644 --- a/yarn-project/circuit-types/src/logs/unencrypted_l2_log.ts +++ b/yarn-project/circuit-types/src/logs/unencrypted_l2_log.ts @@ -9,11 +9,6 @@ export class UnencryptedL2Log { constructor( /** * Address of the contract that emitted the event - * NOTE: It would make sense to have the address only in `FunctionL2Logs` because contract address is shared for all - * function logs. I didn't do this because it would require us to have 2 FunctionL2Logs classes (one with contract - * address and one without) for unencrypted and encrypted because encrypted logs can't expose the address in an - * unencrypted form. For this reason separating the classes seems like a premature optimization. - * TODO: Optimize this once it makes sense. */ public readonly contractAddress: AztecAddress, /** The data contents of the log. */ diff --git a/yarn-project/circuits.js/src/structs/kernel/public_accumulated_data.ts b/yarn-project/circuits.js/src/structs/kernel/public_accumulated_data.ts index 1dd6f2beb56c..2efa4ba5366c 100644 --- a/yarn-project/circuits.js/src/structs/kernel/public_accumulated_data.ts +++ b/yarn-project/circuits.js/src/structs/kernel/public_accumulated_data.ts @@ -21,9 +21,9 @@ import { Gas } from '../gas.js'; import { ScopedL2ToL1Message } from '../l2_to_l1_message.js'; import { LogHash, ScopedLogHash } from '../log_hash.js'; import { ScopedNoteHash } from '../note_hash.js'; -import { Nullifier } from '../nullifier.js'; +import { ScopedNullifier } from '../nullifier.js'; import { PublicCallRequest } from '../public_call_request.js'; -import { PublicDataUpdateRequest } from '../public_data_update_request.js'; +import { ContractStorageUpdateRequest } from '../contract_storage_update_request.js'; export class PublicAccumulatedData { constructor( @@ -34,7 +34,7 @@ export class PublicAccumulatedData { /** * The new nullifiers made in this transaction. */ - public readonly nullifiers: Tuple, + public readonly nullifiers: Tuple, /** * All the new L2 to L1 messages created in this transaction. */ @@ -58,7 +58,7 @@ export class PublicAccumulatedData { * All the public data update requests made in this transaction. */ public readonly publicDataUpdateRequests: Tuple< - PublicDataUpdateRequest, + ContractStorageUpdateRequest, typeof MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX >, /** @@ -164,12 +164,12 @@ export class PublicAccumulatedData { const reader = BufferReader.asReader(buffer); return new this( reader.readArray(MAX_NOTE_HASHES_PER_TX, ScopedNoteHash), - reader.readArray(MAX_NULLIFIERS_PER_TX, Nullifier), + reader.readArray(MAX_NULLIFIERS_PER_TX, ScopedNullifier), reader.readArray(MAX_L2_TO_L1_MSGS_PER_TX, ScopedL2ToL1Message), reader.readArray(MAX_NOTE_ENCRYPTED_LOGS_PER_TX, LogHash), reader.readArray(MAX_ENCRYPTED_LOGS_PER_TX, ScopedLogHash), reader.readArray(MAX_UNENCRYPTED_LOGS_PER_TX, ScopedLogHash), - reader.readArray(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, PublicDataUpdateRequest), + reader.readArray(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, ContractStorageUpdateRequest), reader.readArray(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, PublicCallRequest), reader.readObject(Gas), ); @@ -179,12 +179,12 @@ export class PublicAccumulatedData { const reader = FieldReader.asReader(fields); return new this( reader.readArray(MAX_NOTE_HASHES_PER_TX, ScopedNoteHash), - reader.readArray(MAX_NULLIFIERS_PER_TX, Nullifier), + reader.readArray(MAX_NULLIFIERS_PER_TX, ScopedNullifier), reader.readArray(MAX_L2_TO_L1_MSGS_PER_TX, ScopedL2ToL1Message), reader.readArray(MAX_NOTE_ENCRYPTED_LOGS_PER_TX, LogHash), reader.readArray(MAX_ENCRYPTED_LOGS_PER_TX, ScopedLogHash), reader.readArray(MAX_UNENCRYPTED_LOGS_PER_TX, ScopedLogHash), - reader.readArray(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, PublicDataUpdateRequest), + reader.readArray(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, ContractStorageUpdateRequest), reader.readArray(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, PublicCallRequest), reader.readObject(Gas), ); @@ -202,12 +202,12 @@ export class PublicAccumulatedData { static empty() { return new this( makeTuple(MAX_NOTE_HASHES_PER_TX, ScopedNoteHash.empty), - makeTuple(MAX_NULLIFIERS_PER_TX, Nullifier.empty), + makeTuple(MAX_NULLIFIERS_PER_TX, ScopedNullifier.empty), makeTuple(MAX_L2_TO_L1_MSGS_PER_TX, ScopedL2ToL1Message.empty), makeTuple(MAX_NOTE_ENCRYPTED_LOGS_PER_TX, LogHash.empty), makeTuple(MAX_ENCRYPTED_LOGS_PER_TX, ScopedLogHash.empty), makeTuple(MAX_UNENCRYPTED_LOGS_PER_TX, ScopedLogHash.empty), - makeTuple(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, PublicDataUpdateRequest.empty), + makeTuple(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, ContractStorageUpdateRequest.empty), makeTuple(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, PublicCallRequest.empty), Gas.empty(), ); 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 b3343a2f0d30..37d1ae3c8115 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 a51086e5d113..b0691826bbf3 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/circuits.js/src/structs/public_validation_requests.ts b/yarn-project/circuits.js/src/structs/public_validation_requests.ts index 4d45328914c1..bd949c705267 100644 --- a/yarn-project/circuits.js/src/structs/public_validation_requests.ts +++ b/yarn-project/circuits.js/src/structs/public_validation_requests.ts @@ -14,10 +14,10 @@ import { NUM_PUBLIC_VALIDATION_REQUEST_ARRAYS, } from '../constants.gen.js'; import { countAccumulatedItems } from '../utils/index.js'; -import { PublicDataRead } from './public_data_read.js'; import { ScopedReadRequest } from './read_request.js'; import { RollupValidationRequests } from './rollup_validation_requests.js'; import { TreeLeafReadRequest } from './tree_leaf_read_request.js'; +import { ContractStorageRead } from './contract_storage_read.js'; /** * Validation requests accumulated during the execution of the transaction. @@ -45,7 +45,7 @@ export class PublicValidationRequests { /** * All the public data reads made in this transaction. */ - public publicDataReads: Tuple, + public publicDataReads: Tuple, ) {} getSize() { @@ -82,7 +82,7 @@ export class PublicValidationRequests { reader.readArray(MAX_NULLIFIER_READ_REQUESTS_PER_TX, ScopedReadRequest), reader.readArray(MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX, ScopedReadRequest), reader.readArray(MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX, TreeLeafReadRequest), - reader.readArray(MAX_PUBLIC_DATA_READS_PER_TX, PublicDataRead), + reader.readArray(MAX_PUBLIC_DATA_READS_PER_TX, ContractStorageRead), ); } @@ -99,7 +99,7 @@ export class PublicValidationRequests { reader.readArray(MAX_NULLIFIER_READ_REQUESTS_PER_TX, ScopedReadRequest), reader.readArray(MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX, ScopedReadRequest), reader.readArray(MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX, TreeLeafReadRequest), - reader.readArray(MAX_PUBLIC_DATA_READS_PER_TX, PublicDataRead), + reader.readArray(MAX_PUBLIC_DATA_READS_PER_TX, ContractStorageRead), ); } @@ -119,7 +119,7 @@ export class PublicValidationRequests { makeTuple(MAX_NULLIFIER_READ_REQUESTS_PER_TX, ScopedReadRequest.empty), makeTuple(MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX, ScopedReadRequest.empty), makeTuple(MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX, TreeLeafReadRequest.empty), - makeTuple(MAX_PUBLIC_DATA_READS_PER_TX, PublicDataRead.empty), + makeTuple(MAX_PUBLIC_DATA_READS_PER_TX, ContractStorageRead.empty), ); } 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 000000000000..8cc463385931 --- /dev/null +++ b/yarn-project/simulator/src/public/dual_side_effect_trace.ts @@ -0,0 +1,179 @@ +import { + Gas, + VMCircuitPublicInputs, + CombinedConstantData, +} from '@aztec/circuits.js'; +import { Fr } from '@aztec/foundation/fields'; +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 { type PublicExecutionResult } from './execution.js'; +import { type PublicSideEffectTraceInterface } from './side_effect_trace_interface.js'; +import { PublicSideEffectTrace } from './side_effect_trace.js'; +import { PublicEnqueuedCallSideEffectTrace } from './enqueued_call_side_effect_trace.js'; +import { assert } from 'console'; + +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, + /** Bytecode used for this execution. */ + bytecode: Buffer, + /** The call's results */ + avmCallResults: AvmContractCallResult, + /** Function name for logging */ + functionName: string = 'unknown', + ): VMCircuitPublicInputs { + return this.enqueuedCallTrace.toVMCircuitPublicInputs( + constants, + avmEnvironment, + startGasLeft, + endGasLeft, + bytecode, + avmCallResults, + functionName, + ); + } +} + 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 000000000000..56ddebd978d7 --- /dev/null +++ b/yarn-project/simulator/src/public/enqueued_call_side_effect_trace.ts @@ -0,0 +1,416 @@ +import { UnencryptedL2Log } from '@aztec/circuit-types'; +import { + AvmContractInstanceHint, + AvmExecutionHints, + AvmExternalCallHint, + AvmKeyValueHint, + AztecAddress, + CallContext, + ContractStorageRead, + ContractStorageUpdateRequest, + EthAddress, + Gas, + L2ToL1Message, + LogHash, + NoteHash, + Nullifier, + PublicInnerCallRequest, + ReadRequest, + TreeLeafReadRequest, + ScopedReadRequest, + ScopedNoteHash, + ScopedNullifier, + ScopedL2ToL1Message, + ScopedLogHash, + PublicValidationRequestArrayLengths, + VMCircuitPublicInputs, + CombinedConstantData, + PublicAccumulatedDataArrayLengths, + PublicValidationRequests, + PublicAccumulatedData, + MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, + PublicCallRequest, + RollupValidationRequests, + 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, + MAX_NOTE_HASHES_PER_TX, + MAX_NULLIFIERS_PER_TX, + MAX_L2_TO_L1_MSGS_PER_TX, + MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, + MAX_NOTE_ENCRYPTED_LOGS_PER_TX, + MAX_ENCRYPTED_LOGS_PER_TX, + MAX_UNENCRYPTED_LOGS_PER_TX, +} from '@aztec/circuits.js'; +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 { type PublicSideEffectTraceInterface } from './side_effect_trace_interface.js'; +import { SideEffectLimitReachedError } from './side_effect_errors.js'; +import { computeVarArgsHash } from '@aztec/circuits.js/hash'; +import { makeTuple } from '@aztec/foundation/array'; +import { assertLength } from '@aztec/foundation/serialize'; + +export type TracedContractInstance = { exists: boolean } & ContractInstanceWithAddress; + +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; // kept as number until finalized for efficiency + + // TODO(dbanks12): make contract address mandatory in ContractStorage* structs + // and include it in serialization + 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 newL2ToL1Messages: ScopedL2ToL1Message[] = []; + + private allUnencryptedLogs: UnencryptedL2Log[] = []; + private unencryptedLogsHashes: ScopedLogHash[] = []; + + private publicCallRequests: PublicInnerCallRequest[] = []; + + private avmCircuitHints: AvmExecutionHints; + + constructor( + /** The counter of this trace's first side effect. */ + public readonly startSideEffectCounter: number = 0, + ) { + this.sideEffectCounter = startSideEffectCounter; + this.avmCircuitHints = AvmExecutionHints.empty(); + } + + public fork() { + return new PublicEnqueuedCallSideEffectTrace(this.sideEffectCounter); + } + + public getCounter() { + return this.sideEffectCounter; + } + + private incrementSideEffectCounter() { + this.sideEffectCounter++; + } + + public tracePublicStorageRead(storageAddress: Fr, slot: Fr, value: Fr, _exists: boolean, _cached: boolean) { + // TODO(4805): this threshold should enforce a TX-level limit + // (need access to parent length, or trace needs to be initialized with parent's contents) + // NOTE: exists and cached are unused for now but may be used for optimizations or kernel hints later + if (this.contractStorageReads.length >= 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) { + // TODO(4805): this threshold should enforce a TX-level limit + // (need access to parent length, or trace needs to be initialized with parent's contents) + if (this.contractStorageUpdateRequests.length >= 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: user must provide an actual tree leaf as note hash, so _storageAddress is not used + // for any siloing. In other words, incoming noteHash here must already be siloed by user code. + + // TODO(4805): this threshold should enforce a TX-level limit + if (this.noteHashReadRequests.length >= 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), + ); + } + + public traceNewNoteHash(storageAddress: Fr, noteHash: Fr) { + // TODO(4805): this threshold should enforce a TX-level limit + if (this.noteHashes.length >= 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) { + // TODO(4805): check if some threshold is reached for max new nullifier + // NOTE: isPending and leafIndex are unused for now but may be used for optimizations or kernel hints later + + // TODO(4805): this threshold should enforce a TX-level limit + // 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 >= MAX_NULLIFIER_READ_REQUESTS_PER_TX) { + throw new SideEffectLimitReachedError("nullifier read request", MAX_NULLIFIER_READ_REQUESTS_PER_TX); + } + if (this.nullifierNonExistentReadRequests.length >= MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX) { + throw new SideEffectLimitReachedError("nullifier non-existent read request", MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX); + } + + 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) { + // TODO(4805): this threshold should enforce a TX-level limit + if (this.nullifiers.length >= 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: user must provide an actual tree leaf as msgHash, so _contractAddress is not used + // for any siloing. In other words, incoming msgHash here must already be siloed by user code. + + // TODO(4805): this threshold should enforce a TX-level limit + 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) { + // TODO(4805): this threshold should enforce a TX-level limit + if (this.newL2ToL1Messages.length >= 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.newL2ToL1Messages.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.allUnencryptedLogs.length >= 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.allUnencryptedLogs.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) { + // TODO(4805): check if some threshold is reached for max contract instance retrievals + // TODO(dbanks12): should emit a nullifier read request and threshold (^) should be based on + // nullifier read limits + 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. + 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(...this.contractStorageReads); + this.contractStorageUpdateRequests.push(...this.contractStorageUpdateRequests); + this.noteHashReadRequests.push(...this.noteHashReadRequests); + this.noteHashes.push(...this.noteHashes); + this.nullifierReadRequests.push(...this.nullifierReadRequests); + this.nullifierNonExistentReadRequests.push(...this.nullifierNonExistentReadRequests); + this.nullifiers.push(...this.nullifiers); + this.l1ToL2MsgReadRequests.push(...this.l1ToL2MsgReadRequests); + this.newL2ToL1Messages.push(...this.newL2ToL1Messages); + this.allUnencryptedLogs.push(...this.allUnencryptedLogs); + this.unencryptedLogsHashes.push(...this.unencryptedLogsHashes); + this.publicCallRequests.push(...this.publicCallRequests); + } + + public absorbRevertedNestedTrace(nestedTrace: this) { + // TODO(dbanks12): What should happen to side effect counter on revert? + this.sideEffectCounter = nestedTrace.sideEffectCounter; + this.contractStorageReads.push(...this.contractStorageReads); + this.contractStorageUpdateRequests.push(...this.contractStorageUpdateRequests); + this.noteHashReadRequests.push(...this.noteHashReadRequests); + this.nullifierReadRequests.push(...this.nullifierReadRequests); + this.nullifierNonExistentReadRequests.push(...this.nullifierNonExistentReadRequests); + this.nullifiers.push(...this.nullifiers); + this.l1ToL2MsgReadRequests.push(...this.l1ToL2MsgReadRequests); + this.publicCallRequests.push(...this.publicCallRequests); + // Toss reverted note hashes, new l2 to l1 messages, and logs. + // All read requests, and any writes (storage & nullifiers) that + // require complex validation in public kernel (with end lifetimes) + // must be absorbed even on revert. + } + + public toVMCircuitPublicInputs( + // TODO(dbanks12): pass in 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), + // TODO(dbanks12): trace should know about these lengths! accept in constructor? + /*previousValidationRequestArrayLengths=*/ PublicValidationRequestArrayLengths.empty(), + /*validationRequests=*/ this.getValidationRequests(), + // TODO(dbanks12): trace should know about these lengths! accept in constructor? + /*previousAccumulatedDataArrayLengths=*/ PublicAccumulatedDataArrayLengths.empty(), + /*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, + ); + } + + private getValidationRequests() { + return new PublicValidationRequests( + RollupValidationRequests.empty(), // TODO(dbanks12): what should this be? + assertLength(this.noteHashReadRequests, MAX_NOTE_HASH_READ_REQUESTS_PER_TX), + assertLength(this.nullifierReadRequests, MAX_NULLIFIER_READ_REQUESTS_PER_TX), + assertLength(this.nullifierNonExistentReadRequests, MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX), + assertLength(this.l1ToL2MsgReadRequests, MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX), + assertLength(this.contractStorageReads, MAX_PUBLIC_DATA_READS_PER_TX), + ); + } + + private getAccumulatedData(gasUsed: Gas) { + return new PublicAccumulatedData( + assertLength(this.noteHashes, MAX_NOTE_HASHES_PER_TX), + assertLength(this.nullifiers, MAX_NULLIFIERS_PER_TX), + assertLength(this.newL2ToL1Messages, 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), + assertLength(this.unencryptedLogsHashes, MAX_UNENCRYPTED_LOGS_PER_TX), + assertLength(this.contractStorageUpdateRequests, MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX), + /*publicCallStack=*/ makeTuple(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, PublicCallRequest.empty), + /*gasUsed=*/ gasUsed, + ); + } + +} + +/** + * 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, + }); + // TODO(dbanks12): what is the right counter? + 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 b2f5a12d9e4d..04a2a263a71f 100644 --- a/yarn-project/simulator/src/public/enqueued_call_simulator.ts +++ b/yarn-project/simulator/src/public/enqueued_call_simulator.ts @@ -67,6 +67,7 @@ import { accumulateReturnValues } from '../common/index.js'; import { type PublicExecutionResult, collectExecutionResults } from './execution.js'; import { type PublicExecutor } from './executor.js'; import { type PublicKernelCircuitSimulator } from './public_kernel_circuit_simulator.js'; +import { assert } from 'console'; function makeAvmProvingRequest( inputs: PublicKernelInnerCircuitPrivateInputs, @@ -123,33 +124,43 @@ export class EnqueuedCallSimulator { ): Promise { const pendingNullifiers = this.getSiloedPendingNullifiers(previousPublicKernelOutput); const startSideEffectCounter = previousPublicKernelOutput.endSideEffectCounter + 1; - const result = await this.publicExecutor.simulate( + + const prevAccumulatedData = + phase === PublicKernelPhase.SETUP + ? previousPublicKernelOutput.endNonRevertibleData + : previousPublicKernelOutput.end; + const previousValidationRequestArrayLengths = PublicValidationRequestArrayLengths.new( + previousPublicKernelOutput.validationRequests, + ); + const previousAccumulatedDataArrayLengths = PublicAccumulatedDataArrayLengths.new(prevAccumulatedData); + + const [result, vmCircuitPublicInputs] = 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, @@ -158,7 +169,9 @@ export class EnqueuedCallSimulator { result.reverted, ); - return await this.combineNestedExecutionResults(result, startVMCircuitOutput); + const enqueuedCallResults = await this.combineNestedExecutionResults(result, startVMCircuitOutput); + assert(JSON.stringify(enqueuedCallResults.kernelOutput) == JSON.stringify(vmCircuitPublicInputs)); + return enqueuedCallResults; } private async combineNestedExecutionResults( diff --git a/yarn-project/simulator/src/public/execution.ts b/yarn-project/simulator/src/public/execution.ts index 216bd09eecac..10d56f40474a 100644 --- a/yarn-project/simulator/src/public/execution.ts +++ b/yarn-project/simulator/src/public/execution.ts @@ -1,4 +1,5 @@ import { + UnencryptedL2Log, type PublicExecutionRequest, type SimulationError, type UnencryptedFunctionL2Logs, @@ -15,9 +16,14 @@ import { type Nullifier, PublicCallStackItemCompressed, PublicInnerCallRequest, - type ReadRequest, RevertCode, type TreeLeafReadRequest, + ScopedNoteHash, + ScopedL2ToL1Message, + ScopedNullifier, + ScopedReadRequest, + ScopedLogHash, + ReadRequest, } from '@aztec/circuits.js'; import { computeVarArgsHash } from '@aztec/circuits.js/hash'; diff --git a/yarn-project/simulator/src/public/executor.ts b/yarn-project/simulator/src/public/executor.ts index 52f664df4361..7d60d1a0c775 100644 --- a/yarn-project/simulator/src/public/executor.ts +++ b/yarn-project/simulator/src/public/executor.ts @@ -1,6 +1,17 @@ import { type PublicExecutionRequest } from '@aztec/circuit-types'; import { type AvmSimulationStats } from '@aztec/circuit-types/stats'; -import { Fr, Gas, type GlobalVariables, type Header, type Nullifier, type TxContext } from '@aztec/circuits.js'; +import { + CombinedConstantData, + Fr, + Gas, + type GlobalVariables, + type Header, + PublicAccumulatedDataArrayLengths, + PublicValidationRequestArrayLengths, + type ScopedNullifier, + type TxContext, + VMCircuitPublicInputs, +} from '@aztec/circuits.js'; import { createDebugLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; import { type TelemetryClient } from '@aztec/telemetry-client'; @@ -14,6 +25,8 @@ import { type PublicExecutionResult } from './execution.js'; import { ExecutorMetrics } from './executor_metrics.js'; import { type WorldStateDB } from './public_db_sources.js'; import { PublicSideEffectTrace } from './side_effect_trace.js'; +import { PublicEnqueuedCallSideEffectTrace } from './enqueued_call_side_effect_trace.js'; +import { DualSideEffectTrace } from './dual_side_effect_trace.js'; /** * Handles execution of public functions. @@ -40,13 +53,15 @@ export class PublicExecutor { */ public async simulate( executionRequest: PublicExecutionRequest, - globalVariables: GlobalVariables, + constants: CombinedConstantData, availableGas: Gas, _txContext: TxContext, - pendingSiloedNullifiers: Nullifier[], + pendingSiloedNullifiers: ScopedNullifier[], transactionFee: Fr = Fr.ZERO, startSideEffectCounter: number = 0, - ): Promise { + previousValidationRequestArrayLengths: PublicValidationRequestArrayLengths = PublicValidationRequestArrayLengths.empty(), + previousAccumulatedDataArrayLengths: PublicAccumulatedDataArrayLengths = PublicAccumulatedDataArrayLengths.empty(), + ): Promise<[PublicExecutionResult, VMCircuitPublicInputs]> { const address = executionRequest.contractAddress; const selector = executionRequest.callContext.functionSelector; const fnName = (await this.worldStateDB.getDebugFunctionName(address, selector)) ?? `${address}:${selector}`; @@ -54,7 +69,14 @@ export class PublicExecutor { PublicExecutor.log.verbose(`[AVM] Executing public external function ${fnName}.`); const timer = new Timer(); - const trace = new PublicSideEffectTrace(startSideEffectCounter); + const innerCallTrace = new PublicSideEffectTrace( + startSideEffectCounter, + previousValidationRequestArrayLengths, + previousAccumulatedDataArrayLengths, + ); + // TODO(dbanks12): add previous lengths to enqueued call trace + const enqueuedCallTrace = new PublicEnqueuedCallSideEffectTrace(startSideEffectCounter); + const trace = new DualSideEffectTrace(innerCallTrace, enqueuedCallTrace); const avmPersistableState = AvmPersistableStateManager.newWithPendingSiloedNullifiers( this.worldStateDB, trace, @@ -64,7 +86,7 @@ export class PublicExecutor { const avmExecutionEnv = createAvmExecutionEnvironment( executionRequest, this.header, - globalVariables, + constants.globalVariables, transactionFee, ); @@ -107,7 +129,15 @@ export class PublicExecutor { this.metrics.recordFunctionSimulation(bytecode.length, timer.ms()); } - return publicExecutionResult; + const vmCircuitPublicInputs = enqueuedCallTrace.toVMCircuitPublicInputs( + constants, + avmExecutionEnv, + /*startGasLeft=*/ availableGas, + /*endGasLeft=*/ Gas.from(avmContext.machineState.gasLeft), + avmResult, + ); + + return [publicExecutionResult, vmCircuitPublicInputs]; } } diff --git a/yarn-project/simulator/src/public/public_processor.test.ts b/yarn-project/simulator/src/public/public_processor.test.ts index 8ebde5516a70..db5695173992 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/simulator/src/public/side_effect_trace.test.ts b/yarn-project/simulator/src/public/side_effect_trace.test.ts index a0b4f16f7091..fb8791086c36 100644 --- a/yarn-project/simulator/src/public/side_effect_trace.test.ts +++ b/yarn-project/simulator/src/public/side_effect_trace.test.ts @@ -14,6 +14,8 @@ import { MAX_PUBLIC_DATA_READS_PER_TX, MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, MAX_UNENCRYPTED_LOGS_PER_TX, + PublicAccumulatedDataArrayLengths, + PublicValidationRequestArrayLengths, } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { SerializableContractInstance } from '@aztec/types/contracts'; @@ -367,6 +369,61 @@ describe('Side Effect Trace', () => { SideEffectLimitReachedError, ); }); + + it('PreviousValidationRequestArrayLengths and PreviousAccumulatedDataArrayLengths contribute to limits', () => { + trace = new PublicSideEffectTrace( + 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, + ); + }); }); it('Should trace nested calls', () => { diff --git a/yarn-project/simulator/src/public/side_effect_trace.ts b/yarn-project/simulator/src/public/side_effect_trace.ts index 5e0f8d51cf76..570ade7b67ce 100644 --- a/yarn-project/simulator/src/public/side_effect_trace.ts +++ b/yarn-project/simulator/src/public/side_effect_trace.ts @@ -24,7 +24,9 @@ import { MAX_UNENCRYPTED_LOGS_PER_TX, NoteHash, Nullifier, + PublicAccumulatedDataArrayLengths, type PublicInnerCallRequest, + PublicValidationRequestArrayLengths, ReadRequest, TreeLeafReadRequest, } from '@aztec/circuits.js'; @@ -73,28 +75,57 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { 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 PublicSideEffectTrace(this.sideEffectCounter); + return new PublicSideEffectTrace( + 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.newL2ToL1Messages.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() { + // TODO(dbanks12): make private when dual trace is removed + public incrementSideEffectCounter() { this.sideEffectCounter++; } - // TODO(dbanks12): checks against tx-wide limit need access to parent trace's length + // TODO(dbanks12): checks against tx-wide limit don't take into account side effects in child/nested traces 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 >= MAX_PUBLIC_DATA_READS_PER_TX) { + 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( @@ -108,7 +139,10 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { } public tracePublicStorageWrite(storageAddress: Fr, slot: Fr, value: Fr) { - if (this.contractStorageUpdateRequests.length >= MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX) { + 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( @@ -121,7 +155,10 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { // 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 but will be important when an AVM circuit processes an entire enqueued call - if (this.noteHashReadRequests.length >= MAX_NOTE_HASH_READ_REQUESTS_PER_TX) { + 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)); @@ -132,7 +169,7 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { } public traceNewNoteHash(_storageAddress: Fr, noteHash: Fr) { - if (this.noteHashes.length >= MAX_NOTE_HASHES_PER_TX) { + 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)); @@ -161,7 +198,7 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { public traceNewNullifier(_storageAddress: Fr, nullifier: Fr) { // NOTE: storageAddress is unused but will be important when an AVM circuit processes an entire enqueued call - if (this.nullifiers.length >= MAX_NULLIFIERS_PER_TX) { + 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)); @@ -172,7 +209,10 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { // 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 but will be important when an AVM circuit processes an entire enqueued call - if (this.l1ToL2MsgReadRequests.length >= MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX) { + 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); } this.l1ToL2MsgReadRequests.push(new TreeLeafReadRequest(msgHash, msgLeafIndex)); @@ -183,7 +223,10 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { } public traceNewL2ToL1Message(_contractAddress: Fr, recipient: Fr, content: Fr) { - if (this.newL2ToL1Messages.length >= MAX_L2_TO_L1_MSGS_PER_TX) { + if ( + this.newL2ToL1Messages.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); @@ -193,7 +236,10 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { } public traceUnencryptedLog(contractAddress: Fr, log: Fr[]) { - if (this.unencryptedLogs.length >= MAX_UNENCRYPTED_LOGS_PER_TX) { + if ( + this.allUnencryptedLogs.length + this.previousAccumulatedDataArrayLengths.unencryptedLogsHashes >= + MAX_UNENCRYPTED_LOGS_PER_TX + ) { throw new SideEffectLimitReachedError('unencrypted log', MAX_UNENCRYPTED_LOGS_PER_TX); } const ulog = new UnencryptedL2Log( @@ -264,6 +310,7 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { ); this.sideEffectCounter = result.endSideEffectCounter.toNumber(); // when a nested call returns, caller accepts its updated counter + // TODO(dbanks12): should not accept logs if nested call reverted this.allUnencryptedLogs.push(...result.allUnencryptedLogs.logs); // NOTE: eventually if the AVM circuit processes an entire enqueued call, // this function will accept all of the nested's side effects into this instance @@ -348,13 +395,20 @@ export class PublicSideEffectTrace implements PublicSideEffectTraceInterface { // 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 >= MAX_NULLIFIER_READ_REQUESTS_PER_TX) { + 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 >= MAX_NULLIFIER_NON_EXISTENT_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,