diff --git a/yarn-project/acir-simulator/src/acvm/acvm.ts b/yarn-project/acir-simulator/src/acvm/acvm.ts index 2a9db8fb761..85287d2a1c3 100644 --- a/yarn-project/acir-simulator/src/acvm/acvm.ts +++ b/yarn-project/acir-simulator/src/acvm/acvm.ts @@ -1,7 +1,4 @@ import { FunctionDebugMetadata, OpcodeLocation } from '@aztec/foundation/abi'; -import { AztecAddress } from '@aztec/foundation/aztec-address'; -import { EthAddress } from '@aztec/foundation/eth-address'; -import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { NoirCallStack, SourceCodeLocation } from '@aztec/types'; @@ -15,55 +12,25 @@ import { } from '@noir-lang/acvm_js'; import { traverseCauseChain } from '../common/errors.js'; +import { ORACLE_NAMES } from './oracle/index.js'; /** * The format for fields on the ACVM. */ export type ACVMField = string; + /** * The format for witnesses of the ACVM. */ export type ACVMWitness = WitnessMap; -export const ZERO_ACVM_FIELD: ACVMField = `0x${'00'.repeat(Fr.SIZE_IN_BYTES)}`; -export const ONE_ACVM_FIELD: ACVMField = `0x${'00'.repeat(Fr.SIZE_IN_BYTES - 1)}01`; - -/** - * The supported oracle names. - */ -type ORACLE_NAMES = - | 'computeSelector' - | 'packArguments' - | 'getAuthWitness' - | 'getSecretKey' - | 'getNote' - | 'getNotes' - | 'checkNoteHashExists' - | 'getRandomField' - | 'notifyCreatedNote' - | 'notifyNullifiedNote' - | 'callPrivateFunction' - | 'callPublicFunction' - | 'enqueuePublicFunctionCall' - | 'storageRead' - | 'storageWrite' - | 'getL1ToL2Message' - | 'getPortalContractAddress' - | 'emitEncryptedLog' - | 'emitUnencryptedLog' - | 'getPublicKey' - | 'debugLog' - | 'debugLogWithPrefix'; - -/** - * A type that does not require all keys to be present. - */ -type PartialRecord = Partial>; - /** * The callback interface for the ACIR. */ -export type ACIRCallback = PartialRecord Promise>; +type ACIRCallback = Record< + ORACLE_NAMES, + (...args: ForeignCallInput[]) => ForeignCallOutput | Promise +>; /** * The result of executing an ACIR. @@ -193,61 +160,3 @@ export function extractCallStack( return resolveOpcodeLocations(callStack, debug); } - -/** - * Adapts the buffer to the field size. - * @param originalBuf - The buffer to adapt. - * @returns The adapted buffer. - */ -function adaptBufferSize(originalBuf: Buffer) { - const buffer = Buffer.alloc(Fr.SIZE_IN_BYTES); - if (originalBuf.length > buffer.length) { - throw new Error('Buffer does not fit in field'); - } - originalBuf.copy(buffer, buffer.length - originalBuf.length); - return buffer; -} - -/** - * Converts a value to an ACVM field. - * @param value - The value to convert. - * @returns The ACVM field. - */ -export function toACVMField(value: AztecAddress | EthAddress | Fr | Buffer | boolean | number | bigint): ACVMField { - if (typeof value === 'boolean') { - return value ? ONE_ACVM_FIELD : ZERO_ACVM_FIELD; - } - - let buffer; - - if (Buffer.isBuffer(value)) { - buffer = value; - } else if (typeof value === 'number') { - buffer = Buffer.alloc(Fr.SIZE_IN_BYTES); - buffer.writeUInt32BE(value, Fr.SIZE_IN_BYTES - 4); - } else if (typeof value === 'bigint') { - buffer = new Fr(value).toBuffer(); - } else { - buffer = value.toBuffer(); - } - - return `0x${adaptBufferSize(buffer).toString('hex')}`; -} - -/** - * Converts an ACVM field to a Buffer. - * @param field - The ACVM field to convert. - * @returns The Buffer. - */ -export function convertACVMFieldToBuffer(field: ACVMField): Buffer { - return Buffer.from(field.slice(2), 'hex'); -} - -/** - * Converts an ACVM field to a Fr. - * @param field - The ACVM field to convert. - * @returns The Fr. - */ -export function fromACVMField(field: ACVMField): Fr { - return Fr.fromBuffer(convertACVMFieldToBuffer(field)); -} diff --git a/yarn-project/acir-simulator/src/acvm/acvm_fields_reader.ts b/yarn-project/acir-simulator/src/acvm/acvm_fields_reader.ts deleted file mode 100644 index 60d95829fa1..00000000000 --- a/yarn-project/acir-simulator/src/acvm/acvm_fields_reader.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Fr } from '@aztec/foundation/fields'; - -import { ACVMField, fromACVMField } from './acvm.js'; - -/** - * A utility for reading an array of fields. - */ -export class ACVMFieldsReader { - private offset = 0; - - constructor(private fields: ACVMField[]) {} - - /** - * Reads a field. - * @returns The field. - */ - public readField(): Fr { - const acvmField = this.fields[this.offset]; - if (!acvmField) throw new Error('Not enough fields.'); - this.offset += 1; - return fromACVMField(acvmField); - } - - /** - * Reads an array of fields. - * @param length - The length of the array. - * @returns The array of fields. - */ - public readFieldArray(length: number): Fr[] { - const arr: Fr[] = []; - for (let i = 0; i < length; i++) { - arr.push(this.readField()); - } - return arr; - } - - /** - * Reads a number. - * @returns The number. - */ - public readNumber(): number { - const num = +this.fields[this.offset]; - this.offset += 1; - return num; - } - - /** - * Reads a number array. - * @param length - The length of the array. - * @returns The number. - */ - public readNumberArray(length: number): number[] { - const arr: number[] = []; - for (let i = 0; i < length; i++) { - arr.push(this.readNumber()); - } - return arr; - } -} diff --git a/yarn-project/acir-simulator/src/acvm/deserialize.ts b/yarn-project/acir-simulator/src/acvm/deserialize.ts index c9d0f540526..a7977beabfc 100644 --- a/yarn-project/acir-simulator/src/acvm/deserialize.ts +++ b/yarn-project/acir-simulator/src/acvm/deserialize.ts @@ -24,7 +24,25 @@ import { Tuple } from '@aztec/foundation/serialize'; import { getReturnWitness } from '@noir-lang/acvm_js'; -import { ACVMField, ACVMWitness, fromACVMField } from './acvm.js'; +import { ACVMField, ACVMWitness } from './acvm.js'; + +/** + * Converts an ACVM field to a Buffer. + * @param field - The ACVM field to convert. + * @returns The Buffer. + */ +export function convertACVMFieldToBuffer(field: ACVMField): Buffer { + return Buffer.from(field.slice(2), 'hex'); +} + +/** + * Converts an ACVM field to a Fr. + * @param field - The ACVM field to convert. + * @returns The Fr. + */ +export function fromACVMField(field: ACVMField): Fr { + return Fr.fromBuffer(convertACVMFieldToBuffer(field)); +} // Utilities to read TS classes from ACVM Field arrays // In the order that the ACVM provides them diff --git a/yarn-project/acir-simulator/src/acvm/index.ts b/yarn-project/acir-simulator/src/acvm/index.ts index 39757a4035c..8845721ded2 100644 --- a/yarn-project/acir-simulator/src/acvm/index.ts +++ b/yarn-project/acir-simulator/src/acvm/index.ts @@ -1,4 +1,4 @@ -export * from './acvm_fields_reader.js'; -export * from './serialize.js'; export * from './acvm.js'; export * from './deserialize.js'; +export * from './oracle/index.js'; +export * from './serialize.js'; diff --git a/yarn-project/acir-simulator/src/client/debug.ts b/yarn-project/acir-simulator/src/acvm/oracle/debug.ts similarity index 98% rename from yarn-project/acir-simulator/src/client/debug.ts rename to yarn-project/acir-simulator/src/acvm/oracle/debug.ts index a6e7d97d075..abe82e084ef 100644 --- a/yarn-project/acir-simulator/src/client/debug.ts +++ b/yarn-project/acir-simulator/src/acvm/oracle/debug.ts @@ -1,6 +1,6 @@ import { ForeignCallInput } from '@noir-lang/acvm_js'; -import { ACVMField } from '../acvm/index.js'; +import { ACVMField } from '../acvm.js'; /** * Convert an array of ACVMFields to a string. diff --git a/yarn-project/acir-simulator/src/acvm/oracle/index.ts b/yarn-project/acir-simulator/src/acvm/oracle/index.ts new file mode 100644 index 00000000000..56f45c81f70 --- /dev/null +++ b/yarn-project/acir-simulator/src/acvm/oracle/index.ts @@ -0,0 +1,17 @@ +import { Oracle } from './oracle.js'; + +export * from './debug.js'; +export * from './oracle.js'; +export * from './typed_oracle.js'; + +/** + * A conditional type that takes a type `T` and returns a union of its method names. + */ +type MethodNames = { + [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; +}[keyof T]; + +/** + * Available oracle function names. + */ +export type ORACLE_NAMES = MethodNames; diff --git a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts new file mode 100644 index 00000000000..e587bb7db6c --- /dev/null +++ b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts @@ -0,0 +1,221 @@ +import { RETURN_VALUES_LENGTH } from '@aztec/circuits.js'; +import { FunctionSelector } from '@aztec/foundation/abi'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { padArrayEnd } from '@aztec/foundation/collection'; +import { Fr, Point } from '@aztec/foundation/fields'; +import { createDebugLogger } from '@aztec/foundation/log'; + +import { ACVMField } from '../acvm.js'; +import { convertACVMFieldToBuffer, fromACVMField } from '../deserialize.js'; +import { + toACVMField, + toAcvmCallPrivateStackItem, + toAcvmEnqueuePublicFunctionResult, + toAcvmL1ToL2MessageLoadOracleInputs, +} from '../serialize.js'; +import { acvmFieldMessageToString, oracleDebugCallToFormattedStr } from './debug.js'; +import { TypedOracle } from './typed_oracle.js'; + +/** + * A data source that has all the apis required by Aztec.nr. + */ +export class Oracle { + constructor(private typedOracle: TypedOracle, private log = createDebugLogger('aztec:simulator:oracle')) {} + + computeSelector(...args: ACVMField[][]): ACVMField { + const signature = oracleDebugCallToFormattedStr(args); + const selector = this.typedOracle.computeSelector(signature); + return toACVMField(selector); + } + + getRandomField(): ACVMField { + const val = this.typedOracle.getRandomField(); + return toACVMField(val); + } + + async packArguments(args: ACVMField[]): Promise { + const packed = await this.typedOracle.packArguments(args.map(fromACVMField)); + return toACVMField(packed); + } + + async getSecretKey([publicKeyX]: ACVMField[], [publicKeyY]: ACVMField[]): Promise { + const publicKey = new Point(fromACVMField(publicKeyX), fromACVMField(publicKeyY)); + const secretKey = await this.typedOracle.getSecretKey(publicKey); + return [toACVMField(secretKey.low), toACVMField(secretKey.high)]; + } + + async getPublicKey([address]: ACVMField[]) { + const { publicKey, partialAddress } = await this.typedOracle.getPublicKey( + AztecAddress.fromField(fromACVMField(address)), + ); + return [publicKey.x, publicKey.y, partialAddress].map(toACVMField); + } + + async getAuthWitness([messageHash]: ACVMField[]): Promise { + const messageHashField = fromACVMField(messageHash); + const witness = await this.typedOracle.getAuthWitness(messageHashField); + if (!witness) throw new Error(`Authorization not found for message hash ${messageHashField}`); + return witness.map(toACVMField); + } + + async getNotes( + [storageSlot]: ACVMField[], + [numSelects]: ACVMField[], + selectBy: ACVMField[], + selectValues: ACVMField[], + sortBy: ACVMField[], + sortOrder: ACVMField[], + [limit]: ACVMField[], + [offset]: ACVMField[], + [returnSize]: ACVMField[], + ): Promise { + const notes = await this.typedOracle.getNotes( + fromACVMField(storageSlot), + +numSelects, + selectBy.map(s => +s), + selectValues.map(fromACVMField), + sortBy.map(s => +s), + sortOrder.map(s => +s), + +limit, + +offset, + ); + + const contractAddress = notes[0]?.contractAddress ?? Fr.ZERO; + + const isSome = new Fr(1); // Boolean. Indicates whether the Noir Option::is_some(); + const realNotePreimages = notes.flatMap(({ nonce, preimage }) => [nonce, isSome, ...preimage]); + const preimageLength = notes[0]?.preimage.length ?? 0; + const returnHeaderLength = 2; // is for the header values: `notes.length` and `contractAddress`. + const extraPreimageLength = 2; // is for the nonce and isSome fields. + const extendedPreimageLength = preimageLength + extraPreimageLength; + const numRealNotes = notes.length; + const numReturnNotes = Math.floor((+returnSize - returnHeaderLength) / extendedPreimageLength); + const numDummyNotes = numReturnNotes - numRealNotes; + + const dummyNotePreimage = Array(extendedPreimageLength).fill(Fr.ZERO); + const dummyNotePreimages = Array(numDummyNotes) + .fill(dummyNotePreimage) + .flatMap(note => note); + + const paddedZeros = Array( + Math.max(0, +returnSize - returnHeaderLength - realNotePreimages.length - dummyNotePreimages.length), + ).fill(Fr.ZERO); + + return [notes.length, contractAddress, ...realNotePreimages, ...dummyNotePreimages, ...paddedZeros].map(v => + toACVMField(v), + ); + } + + async checkNoteHashExists([nonce]: ACVMField[], [innerNoteHash]: ACVMField[]): Promise { + const exists = await this.typedOracle.checkNoteHashExists(fromACVMField(nonce), fromACVMField(innerNoteHash)); + return toACVMField(exists); + } + + notifyCreatedNote([storageSlot]: ACVMField[], preimage: ACVMField[], [innerNoteHash]: ACVMField[]): ACVMField { + this.typedOracle.notifyCreatedNote( + fromACVMField(storageSlot), + preimage.map(fromACVMField), + fromACVMField(innerNoteHash), + ); + return toACVMField(0); + } + + async notifyNullifiedNote([innerNullifier]: ACVMField[], [innerNoteHash]: ACVMField[]): Promise { + await this.typedOracle.notifyNullifiedNote(fromACVMField(innerNullifier), fromACVMField(innerNoteHash)); + return toACVMField(0); + } + + async getL1ToL2Message([msgKey]: ACVMField[]): Promise { + const { root, ...message } = await this.typedOracle.getL1ToL2Message(fromACVMField(msgKey)); + return toAcvmL1ToL2MessageLoadOracleInputs(message, root); + } + + async getPortalContractAddress([aztecAddress]: ACVMField[]): Promise { + const contractAddress = AztecAddress.fromString(aztecAddress); + const portalContactAddress = await this.typedOracle.getPortalContractAddress(contractAddress); + return toACVMField(portalContactAddress); + } + + async storageRead([startStorageSlot]: ACVMField[], [numberOfElements]: ACVMField[]): Promise { + const values = await this.typedOracle.storageRead(fromACVMField(startStorageSlot), +numberOfElements); + return values.map(toACVMField); + } + + async storageWrite([startStorageSlot]: ACVMField[], values: ACVMField[]): Promise { + const newValues = await this.typedOracle.storageWrite(fromACVMField(startStorageSlot), values.map(fromACVMField)); + return newValues.map(toACVMField); + } + + emitEncryptedLog( + [contractAddress]: ACVMField[], + [storageSlot]: ACVMField[], + [publicKeyX]: ACVMField[], + [publicKeyY]: ACVMField[], + preimage: ACVMField[], + ): ACVMField { + const publicKey = new Point(fromACVMField(publicKeyX), fromACVMField(publicKeyY)); + this.typedOracle.emitEncryptedLog( + AztecAddress.fromString(contractAddress), + Fr.fromString(storageSlot), + publicKey, + preimage.map(fromACVMField), + ); + return toACVMField(0); + } + + emitUnencryptedLog(message: ACVMField[]): ACVMField { + // https://github.com/AztecProtocol/aztec-packages/issues/885 + const log = Buffer.concat(message.map(charBuffer => convertACVMFieldToBuffer(charBuffer).subarray(-1))); + this.typedOracle.emitUnencryptedLog(log); + return toACVMField(0); + } + + debugLog(...args: ACVMField[][]): ACVMField { + this.log(oracleDebugCallToFormattedStr(args)); + return toACVMField(0); + } + + debugLogWithPrefix(arg0: ACVMField[], ...args: ACVMField[][]): ACVMField { + this.log(`${acvmFieldMessageToString(arg0)}: ${oracleDebugCallToFormattedStr(args)}`); + return toACVMField(0); + } + + async callPrivateFunction( + [contractAddress]: ACVMField[], + [functionSelector]: ACVMField[], + [argsHash]: ACVMField[], + ): Promise { + const callStackItem = await this.typedOracle.callPrivateFunction( + AztecAddress.fromField(fromACVMField(contractAddress)), + FunctionSelector.fromField(fromACVMField(functionSelector)), + fromACVMField(argsHash), + ); + return toAcvmCallPrivateStackItem(callStackItem); + } + + async callPublicFunction( + [contractAddress]: ACVMField[], + [functionSelector]: ACVMField[], + [argsHash]: ACVMField[], + ): Promise { + const returnValues = await this.typedOracle.callPublicFunction( + AztecAddress.fromField(fromACVMField(contractAddress)), + FunctionSelector.fromField(fromACVMField(functionSelector)), + fromACVMField(argsHash), + ); + return padArrayEnd(returnValues, Fr.ZERO, RETURN_VALUES_LENGTH).map(toACVMField); + } + + async enqueuePublicFunctionCall( + [contractAddress]: ACVMField[], + [functionSelector]: ACVMField[], + [argsHash]: ACVMField[], + ) { + const enqueuedRequest = await this.typedOracle.enqueuePublicFunctionCall( + AztecAddress.fromString(contractAddress), + FunctionSelector.fromField(fromACVMField(functionSelector)), + fromACVMField(argsHash), + ); + return toAcvmEnqueuePublicFunctionResult(enqueuedRequest); + } +} diff --git a/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts new file mode 100644 index 00000000000..a972474b420 --- /dev/null +++ b/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts @@ -0,0 +1,159 @@ +import { PrivateCallStackItem, PublicCallRequest } from '@aztec/circuits.js'; +import { FunctionSelector } from '@aztec/foundation/abi'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { Fr, GrumpkinScalar } from '@aztec/foundation/fields'; +import { CompleteAddress, PublicKey } from '@aztec/types'; + +/** + * Information about a note needed during execution. + */ +export interface NoteData { + /** The contract address of the note. */ + contractAddress: AztecAddress; + /** The storage slot of the note. */ + storageSlot: Fr; + /** The nonce of the note. */ + nonce: Fr; + /** The preimage of the note */ + preimage: Fr[]; + /** The inner note hash of the note. */ + innerNoteHash: Fr; + /** The corresponding nullifier of the note. Undefined for pending notes. */ + siloedNullifier?: Fr; + /** The note's leaf index in the private data tree. Undefined for pending notes. */ + index?: bigint; +} + +/** + * The partial data for L1 to L2 Messages provided by other data sources. + */ +export interface MessageLoadOracleInputs { + /** + * An collapsed array of fields containing all of the l1 to l2 message components. + * `l1ToL2Message.toFieldArray()` -\> [sender, chainId, recipient, version, content, secretHash, deadline, fee] + */ + message: Fr[]; + /** + * The path in the merkle tree to the message. + */ + siblingPath: Fr[]; + /** + * The index of the message commitment in the merkle tree. + */ + index: bigint; +} + +/** + * The data required by Aztec.nr to validate L1 to L2 Messages. + */ +export interface L1ToL2MessageOracleReturnData extends MessageLoadOracleInputs { + /** + * The current root of the l1 to l2 message tree. + */ + root: Fr; +} + +/** + * Oracle with typed parameters and typed return values. + * Methods that require read and/or write will have to be implemented based on the context (public, private, or view) + * and are unavailable by default. + */ +export abstract class TypedOracle { + computeSelector(signature: string): Fr { + return FunctionSelector.fromSignature(signature).toField(); + } + + getRandomField(): Fr { + return Fr.random(); + } + + packArguments(_args: Fr[]): Promise { + throw new Error('Not available.'); + } + + getSecretKey(_owner: PublicKey): Promise { + throw new Error('Not available.'); + } + + getPublicKey(_address: AztecAddress): Promise { + throw new Error('Not available.'); + } + + getAuthWitness(_messageHash: Fr): Promise { + throw new Error('Not available.'); + } + + getNotes( + _storageSlot: Fr, + _numSelects: number, + _selectBy: number[], + _selectValues: Fr[], + _sortBy: number[], + _sortOrder: number[], + _limit: number, + _offset: number, + ): Promise { + throw new Error('Not available.'); + } + + checkNoteHashExists(_nonce: Fr, _innerNoteHash: Fr): Promise { + throw new Error('Not available.'); + } + + notifyCreatedNote(_storageSlot: Fr, _preimage: Fr[], _innerNoteHash: Fr): void { + throw new Error('Not available.'); + } + + notifyNullifiedNote(_innerNullifier: Fr, _innerNoteHash: Fr): Promise { + throw new Error('Not available.'); + } + + getL1ToL2Message(_msgKey: Fr): Promise { + throw new Error('Not available.'); + } + + getPortalContractAddress(_contractAddress: AztecAddress): Promise { + throw new Error('Not available.'); + } + + storageRead(_startStorageSlot: Fr, _numberOfElements: number): Promise { + throw new Error('Not available.'); + } + + storageWrite(_startStorageSlot: Fr, _values: Fr[]): Promise { + throw new Error('Not available.'); + } + + emitEncryptedLog(_contractAddress: AztecAddress, _storageSlot: Fr, _publicKey: PublicKey, _preimage: Fr[]): void { + throw new Error('Not available.'); + } + + emitUnencryptedLog(_log: Buffer): void { + throw new Error('Not available.'); + } + + callPrivateFunction( + _targetContractAddress: AztecAddress, + _functionSelector: FunctionSelector, + _argsHash: Fr, + ): Promise { + throw new Error('Not available.'); + } + + callPublicFunction( + _targetContractAddress: AztecAddress, + _functionSelector: FunctionSelector, + _argsHash: Fr, + ): Promise { + throw new Error('Not available.'); + } + + enqueuePublicFunctionCall( + _targetContractAddress: AztecAddress, + _functionSelector: FunctionSelector, + _argsHash: Fr, + ): Promise { + throw new Error('Not available.'); + } +} diff --git a/yarn-project/acir-simulator/src/acvm/serialize.ts b/yarn-project/acir-simulator/src/acvm/serialize.ts index 00ca0d9de48..5a91956ed42 100644 --- a/yarn-project/acir-simulator/src/acvm/serialize.ts +++ b/yarn-project/acir-simulator/src/acvm/serialize.ts @@ -7,10 +7,43 @@ import { PrivateCircuitPublicInputs, PublicCallRequest, } from '@aztec/circuits.js'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; -import { MessageLoadOracleInputs } from '../client/db_oracle.js'; -import { ACVMField, toACVMField } from './acvm.js'; +import { ACVMField } from './acvm.js'; +import { MessageLoadOracleInputs } from './oracle/index.js'; + +/** + * Adapts the buffer to the field size. + * @param originalBuf - The buffer to adapt. + * @returns The adapted buffer. + */ +function adaptBufferSize(originalBuf: Buffer) { + const buffer = Buffer.alloc(Fr.SIZE_IN_BYTES); + if (originalBuf.length > buffer.length) { + throw new Error('Buffer does not fit in field'); + } + originalBuf.copy(buffer, buffer.length - originalBuf.length); + return buffer; +} + +/** + * Converts a value to an ACVM field. + * @param value - The value to convert. + * @returns The ACVM field. + */ +export function toACVMField(value: AztecAddress | EthAddress | Fr | Buffer | boolean | number | bigint): ACVMField { + let buffer; + if (Buffer.isBuffer(value)) { + buffer = value; + } else if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'bigint') { + buffer = new Fr(value).toBuffer(); + } else { + buffer = value.toBuffer(); + } + return `0x${adaptBufferSize(buffer).toString('hex')}`; +} // Utilities to write TS classes to ACVM Field arrays // In the order that the ACVM expects them diff --git a/yarn-project/acir-simulator/src/client/client_execution_context.ts b/yarn-project/acir-simulator/src/client/client_execution_context.ts index 63c720a7497..0ee5001e9b6 100644 --- a/yarn-project/acir-simulator/src/client/client_execution_context.ts +++ b/yarn-project/acir-simulator/src/client/client_execution_context.ts @@ -1,28 +1,34 @@ -import { CircuitsWasm, HistoricBlockData, ReadRequestMembershipWitness, TxContext } from '@aztec/circuits.js'; +import { + CallContext, + CircuitsWasm, + ContractDeploymentData, + FunctionData, + FunctionSelector, + HistoricBlockData, + PublicCallRequest, + ReadRequestMembershipWitness, + TxContext, +} from '@aztec/circuits.js'; import { computeUniqueCommitment, siloCommitment } from '@aztec/circuits.js/abis'; +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr, Point } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; -import { AuthWitness } from '@aztec/types'; +import { AuthWitness, FunctionL2Logs, NotePreimage, NoteSpendingInfo } from '@aztec/types'; -import { - ACVMField, - ONE_ACVM_FIELD, - ZERO_ACVM_FIELD, - fromACVMField, - toACVMField, - toAcvmL1ToL2MessageLoadOracleInputs, -} from '../acvm/index.js'; +import { NoteData, toACVMWitness } from '../acvm/index.js'; import { PackedArgsCache } from '../common/packed_args_cache.js'; import { DBOracle } from './db_oracle.js'; import { ExecutionNoteCache } from './execution_note_cache.js'; -import { NewNoteData } from './execution_result.js'; +import { ExecutionResult, NewNoteData } from './execution_result.js'; import { pickNotes } from './pick_notes.js'; +import { executePrivateFunction } from './private_execution.js'; +import { ViewDataOracle } from './view_data_oracle.js'; /** * The execution context for a client tx simulation. */ -export class ClientTxExecutionContext { +export class ClientExecutionContext extends ViewDataOracle { /** * New notes created during this execution. * It's possible that a note in this list has been nullified (in the same or other executions) and doen't exist in the ExecutionNoteCache and the final proof data. @@ -41,50 +47,63 @@ export class ClientTxExecutionContext { * They should act as references for the read requests output by an app circuit via public inputs. */ private gotNotes: Map = new Map(); - - /** Logger instance */ - private logger = createDebugLogger('aztec:simulator:execution_context'); + private encryptedLogs: Buffer[] = []; + private unencryptedLogs: Buffer[] = []; + private nestedExecutions: ExecutionResult[] = []; + private enqueuedPublicFunctionCalls: PublicCallRequest[] = []; constructor( - /** The database oracle. */ - public db: DBOracle, - /** The tx context. */ - public txContext: TxContext, + protected readonly contractAddress: AztecAddress, + private readonly argsHash: Fr, + private readonly txContext: TxContext, + private readonly callContext: CallContext, /** Data required to reconstruct the block hash, it contains historic roots. */ - public historicBlockData: HistoricBlockData, - /** The cache of packed arguments */ - public packedArgsCache: PackedArgsCache, - private noteCache: ExecutionNoteCache, + protected readonly historicBlockData: HistoricBlockData, /** List of transient auth witnesses to be used during this simulation */ - private authWitnesses: AuthWitness[], - private log = createDebugLogger('aztec:simulator:client_execution_context'), - ) {} - - /** - * Create context for nested executions. - * @returns ClientTxExecutionContext - */ - public extend() { - return new ClientTxExecutionContext( - this.db, - this.txContext, - this.historicBlockData, - this.packedArgsCache, - this.noteCache, - this.authWitnesses, - ); + protected readonly authWitnesses: AuthWitness[], + private sideEffectCounter: number, + private readonly packedArgsCache: PackedArgsCache, + private readonly noteCache: ExecutionNoteCache, + protected readonly db: DBOracle, + private readonly curve: Grumpkin, + protected log = createDebugLogger('aztec:simulator:client_execution_context'), + ) { + super(contractAddress, historicBlockData, authWitnesses, db, undefined, log); } + // We still need this function until we can get user-defined ordering of structs for fn arguments + // TODO When that is sorted out on noir side, we can use instead the utilities in serialize.ts /** - * Returns an auth witness for the given message hash. Checks on the list of transient witnesses - * for this transaction first, and falls back to the local database if not found. - * @param messageHash - Hash of the message to authenticate. - * @returns Authentication witness for the requested message hash. + * Writes the function inputs to the initial witness. + * @returns The initial witness. */ - public getAuthWitness(messageHash: Fr): Promise { - return Promise.resolve( - this.authWitnesses.find(w => w.requestHash.equals(messageHash))?.witness ?? this.db.getAuthWitness(messageHash), - ); + public getInitialWitness() { + const contractDeploymentData = this.txContext.contractDeploymentData; + + const fields = [ + this.callContext.msgSender, + this.callContext.storageContractAddress, + this.callContext.portalContractAddress, + this.callContext.isDelegateCall, + this.callContext.isStaticCall, + this.callContext.isContractDeployment, + + ...this.historicBlockData.toArray(), + + contractDeploymentData.deployerPublicKey.x, + contractDeploymentData.deployerPublicKey.y, + contractDeploymentData.constructorVkHash, + contractDeploymentData.functionTreeRoot, + contractDeploymentData.contractAddressSalt, + contractDeploymentData.portalContractAddress, + + this.txContext.chainId, + this.txContext.version, + + ...this.packedArgsCache.unpack(this.argsHash), + ]; + + return toACVMWitness(1, fields); } /** @@ -116,18 +135,39 @@ export class ClientTxExecutionContext { } /** - * For getting secret key. - * @param contractAddress - The contract address. - * @param ownerX - The x coordinate of the owner's public key. - * @param ownerY - The y coordinate of the owner's public key. - * @returns The secret key of the owner as a pair of ACVM fields. + * Return the encrypted logs emitted during this execution. */ - public async getSecretKey(contractAddress: AztecAddress, ownerX: ACVMField, ownerY: ACVMField) { - const secretKey = await this.db.getSecretKey( - contractAddress, - new Point(fromACVMField(ownerX), fromACVMField(ownerY)), - ); - return [toACVMField(secretKey.low), toACVMField(secretKey.high)]; + public getEncryptedLogs() { + return new FunctionL2Logs(this.encryptedLogs); + } + + /** + * Return the encrypted logs emitted during this execution. + */ + public getUnencryptedLogs() { + return new FunctionL2Logs(this.unencryptedLogs); + } + + /** + * Return the nested execution results during this execution. + */ + public getNestedExecutions() { + return this.nestedExecutions; + } + + /** + * Return the enqueued public function calls during this execution. + */ + public getEnqueuedPublicFunctionCalls() { + return this.enqueuedPublicFunctionCalls; + } + + /** + * Pack the given arguments. + * @param args - Arguments to pack + */ + public packArguments(args: Fr[]): Promise { + return this.packedArgsCache.pack(args); } /** @@ -157,50 +197,35 @@ export class ClientTxExecutionContext { * paddedZeros - zeros to ensure an array with length returnSize expected by Noir circuit */ public async getNotes( - contractAddress: AztecAddress, - storageSlot: ACVMField, + storageSlot: Fr, numSelects: number, - selectBy: ACVMField[], - selectValues: ACVMField[], - sortBy: ACVMField[], - sortOrder: ACVMField[], + selectBy: number[], + selectValues: Fr[], + sortBy: number[], + sortOrder: number[], limit: number, offset: number, - returnSize: number, - ): Promise { - const storageSlotField = fromACVMField(storageSlot); - + ): Promise { // Nullified pending notes are already removed from the list. - const pendingNotes = this.noteCache.getNotes(contractAddress, storageSlotField); + const pendingNotes = this.noteCache.getNotes(this.contractAddress, storageSlot); - const pendingNullifiers = this.noteCache.getNullifiers(contractAddress); - const dbNotes = await this.db.getNotes(contractAddress, storageSlotField); + const pendingNullifiers = this.noteCache.getNullifiers(this.contractAddress); + const dbNotes = await this.db.getNotes(this.contractAddress, storageSlot); const dbNotesFiltered = dbNotes.filter(n => !pendingNullifiers.has((n.siloedNullifier as Fr).value)); - const notes = pickNotes([...dbNotesFiltered, ...pendingNotes], { - selects: selectBy - .slice(0, numSelects) - .map((fieldIndex, i) => ({ index: +fieldIndex, value: fromACVMField(selectValues[i]) })), - sorts: sortBy.map((fieldIndex, i) => ({ index: +fieldIndex, order: +sortOrder[i] })), + const notes = pickNotes([...dbNotesFiltered, ...pendingNotes], { + selects: selectBy.slice(0, numSelects).map((index, i) => ({ index, value: selectValues[i] })), + sorts: sortBy.map((index, i) => ({ index, order: sortOrder[i] })), limit, offset, }); - this.logger( - `Returning ${notes.length} notes for ${contractAddress} at ${storageSlotField}: ${notes + this.log( + `Returning ${notes.length} notes for ${this.contractAddress} at ${storageSlot}: ${notes .map(n => `${n.nonce.toString()}:[${n.preimage.map(i => i.toString()).join(',')}]`) .join(', ')}`, ); - const wasm = await CircuitsWasm.get(); - notes.forEach(n => { - if (n.index !== undefined) { - const siloedNoteHash = siloCommitment(wasm, n.contractAddress, n.innerNoteHash); - const uniqueSiloedNoteHash = computeUniqueCommitment(wasm, n.nonce, siloedNoteHash); - this.gotNotes.set(uniqueSiloedNoteHash.value, n.index); - } - }); - // TODO: notice, that if we don't have a note in our DB, we don't know how big the preimage needs to be, and so we don't actually know how many dummy notes to return, or big to make those dummy notes, or where to position `is_some` booleans to inform the noir program that _all_ the notes should be dummies. // By a happy coincidence, a `0` field is interpreted as `is_none`, and since in this case (of an empty db) we'll return all zeros (paddedZeros), the noir program will treat the returned data as all dummies, but this is luck. Perhaps a preimage size should be conveyed by the get_notes Aztec.nr oracle? const preimageLength = notes?.[0]?.preimage.length ?? 0; @@ -208,33 +233,53 @@ export class ClientTxExecutionContext { !notes.every(({ preimage }) => { return preimageLength === preimage.length; }) - ) + ) { throw new Error('Preimages for a particular note type should all be the same length'); + } - // Combine pending and db preimages into a single flattened array. - const isSome = new Fr(1); // Boolean. Indicates whether the Noir Option::is_some(); - - const realNotePreimages = notes.flatMap(({ nonce, preimage }) => [nonce, isSome, ...preimage]); + const wasm = await CircuitsWasm.get(); + notes.forEach(n => { + if (n.index !== undefined) { + const siloedNoteHash = siloCommitment(wasm, n.contractAddress, n.innerNoteHash); + const uniqueSiloedNoteHash = computeUniqueCommitment(wasm, n.nonce, siloedNoteHash); + this.gotNotes.set(uniqueSiloedNoteHash.value, n.index); + } + }); - const returnHeaderLength = 2; // is for the header values: `notes.length` and `contractAddress`. - const extraPreimageLength = 2; // is for the nonce and isSome fields. - const extendedPreimageLength = preimageLength + extraPreimageLength; - const numRealNotes = notes.length; - const numReturnNotes = Math.floor((returnSize - returnHeaderLength) / extendedPreimageLength); - const numDummyNotes = numReturnNotes - numRealNotes; + return notes; + } - const dummyNotePreimage = Array(extendedPreimageLength).fill(Fr.ZERO); - const dummyNotePreimages = Array(numDummyNotes) - .fill(dummyNotePreimage) - .flatMap(note => note); + /** + * Fetches a path to prove existence of a commitment in the db, given its contract side commitment (before silo). + * @param nonce - The nonce of the note. + * @param innerNoteHash - The inner note hash of the note. + * @returns 1 if (persistent or transient) note hash exists, 0 otherwise. Value is in ACVMField form. + */ + public async checkNoteHashExists(nonce: Fr, innerNoteHash: Fr): Promise { + if (nonce.isZero()) { + // If nonce is 0, we are looking for a new note created in this transaction. + const exists = this.noteCache.checkNoteExists(this.contractAddress, innerNoteHash); + if (exists) { + return true; + } + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/1386) + // Currently it can also be a note created from public if nonce is 0. + // If we can't find a matching new note, keep looking for the match from the notes created in previous transactions. + } - const paddedZeros = Array( - Math.max(0, returnSize - returnHeaderLength - realNotePreimages.length - dummyNotePreimages.length), - ).fill(Fr.ZERO); + // If nonce is zero, SHOULD only be able to reach this point if note was publicly created + const wasm = await CircuitsWasm.get(); + let noteHashToLookUp = siloCommitment(wasm, this.contractAddress, innerNoteHash); + if (!nonce.isZero()) { + noteHashToLookUp = computeUniqueCommitment(wasm, nonce, noteHashToLookUp); + } - return [notes.length, contractAddress, ...realNotePreimages, ...dummyNotePreimages, ...paddedZeros].map(v => - toACVMField(v), - ); + const index = await this.db.getCommitmentIndex(noteHashToLookUp); + const exists = index !== undefined; + if (exists) { + this.gotNotes.set(noteHashToLookUp.value, index); + } + return exists; } /** @@ -246,84 +291,162 @@ export class ClientTxExecutionContext { * @param innerNoteHash - The inner note hash of the new note. * @returns */ - public handleNewNote( - contractAddress: AztecAddress, - storageSlot: ACVMField, - preimage: ACVMField[], - innerNoteHash: ACVMField, - ) { + public notifyCreatedNote(storageSlot: Fr, preimage: Fr[], innerNoteHash: Fr) { this.noteCache.addNewNote({ - contractAddress, - storageSlot: fromACVMField(storageSlot), + contractAddress: this.contractAddress, + storageSlot, nonce: Fr.ZERO, // Nonce cannot be known during private execution. - preimage: preimage.map(f => fromACVMField(f)), + preimage, siloedNullifier: undefined, // Siloed nullifier cannot be known for newly created note. - innerNoteHash: fromACVMField(innerNoteHash), + innerNoteHash, }); this.newNotes.push({ - storageSlot: fromACVMField(storageSlot), - preimage: preimage.map(f => fromACVMField(f)), + storageSlot, + preimage, }); } /** * Adding a siloed nullifier into the current set of all pending nullifiers created * within the current transaction/execution. - * @param contractAddress - The contract address. * @param innerNullifier - The pending nullifier to add in the list (not yet siloed by contract address). * @param innerNoteHash - The inner note hash of the new note. - * @param contractAddress - The contract address */ - public async handleNullifiedNote(contractAddress: AztecAddress, innerNullifier: ACVMField, innerNoteHash: ACVMField) { - await this.noteCache.nullifyNote(contractAddress, fromACVMField(innerNullifier), fromACVMField(innerNoteHash)); + public async notifyNullifiedNote(innerNullifier: Fr, innerNoteHash: Fr) { + await this.noteCache.nullifyNote(this.contractAddress, innerNullifier, innerNoteHash); } /** - * Fetches the a message from the db, given its key. - * @param msgKey - A buffer representing the message key. - * @returns The l1 to l2 message data + * Encrypt a note and emit it as a log. + * @param contractAddress - The contract address of the note. + * @param storageSlot - The storage slot the note is at. + * @param publicKey - The public key of the account that can decrypt the log. + * @param preimage - The preimage of the note. */ - public async getL1ToL2Message(msgKey: Fr): Promise { - const messageInputs = await this.db.getL1ToL2Message(msgKey); - return toAcvmL1ToL2MessageLoadOracleInputs(messageInputs, this.historicBlockData.l1ToL2MessagesTreeRoot); + public emitEncryptedLog(contractAddress: AztecAddress, storageSlot: Fr, publicKey: Point, preimage: Fr[]) { + const notePreimage = new NotePreimage(preimage); + const noteSpendingInfo = new NoteSpendingInfo(notePreimage, contractAddress, storageSlot); + const encryptedNotePreimage = noteSpendingInfo.toEncryptedBuffer(publicKey, this.curve); + this.encryptedLogs.push(encryptedNotePreimage); } /** - * Fetches a path to prove existence of a commitment in the db, given its contract side commitment (before silo). - * @param contractAddress - The contract address. - * @param nonce - The nonce of the note. - * @param innerNoteHash - The inner note hash of the note. - * @returns 1 if (persistent or transient) note hash exists, 0 otherwise. Value is in ACVMField form. + * Emit an unencrypted log. + * @param log - The unencrypted log to be emitted. */ - public async checkNoteHashExists( - contractAddress: AztecAddress, - nonce: ACVMField, - innerNoteHash: ACVMField, - ): Promise { - const nonceField = fromACVMField(nonce); - const innerNoteHashField = fromACVMField(innerNoteHash); - if (nonceField.isZero()) { - // If nonce is 0, we are looking for a new note created in this transaction. - const exists = this.noteCache.checkNoteExists(contractAddress, innerNoteHashField); - if (exists) { - return ONE_ACVM_FIELD; - } - // TODO(https://github.com/AztecProtocol/aztec-packages/issues/1386) - // Currently it can also be a note created from public if nonce is 0. - // If we can't find a matching new note, keep looking for the match from the notes created in previous transactions. - } + public emitUnencryptedLog(log: Buffer) { + this.unencryptedLogs.push(log); + this.log(`Emitted unencrypted log: "${log.toString('ascii')}"`); + } - // If nonce is zero, SHOULD only be able to reach this point if note was publicly created - const wasm = await CircuitsWasm.get(); - let noteHashToLookUp = siloCommitment(wasm, contractAddress, innerNoteHashField); - if (!nonceField.isZero()) { - noteHashToLookUp = computeUniqueCommitment(wasm, nonceField, noteHashToLookUp); - } + /** + * Calls a private function as a nested execution. + * @param targetContractAddress - The address of the contract to call. + * @param functionSelector - The function selector of the function to call. + * @param argsHash - The packed arguments to pass to the function. + * @returns The execution result. + */ + async callPrivateFunction(targetContractAddress: AztecAddress, functionSelector: FunctionSelector, argsHash: Fr) { + this.log( + `Calling private function ${this.contractAddress}:${functionSelector} from ${this.callContext.storageContractAddress}`, + ); - const index = await this.db.getCommitmentIndex(noteHashToLookUp); - if (index !== undefined) { - this.gotNotes.set(noteHashToLookUp.value, index); - } - return index !== undefined ? ONE_ACVM_FIELD : ZERO_ACVM_FIELD; + const targetAbi = await this.db.getFunctionABI(targetContractAddress, functionSelector); + const targetFunctionData = FunctionData.fromAbi(targetAbi); + + const derivedTxContext = new TxContext( + false, + false, + false, + ContractDeploymentData.empty(), + this.txContext.chainId, + this.txContext.version, + ); + + const derivedCallContext = await this.deriveCallContext(targetContractAddress, false, false); + + const context = new ClientExecutionContext( + targetContractAddress, + argsHash, + derivedTxContext, + derivedCallContext, + this.historicBlockData, + this.authWitnesses, + this.sideEffectCounter, + this.packedArgsCache, + this.noteCache, + this.db, + this.curve, + ); + + const childExecutionResult = await executePrivateFunction( + context, + targetAbi, + targetContractAddress, + targetFunctionData, + this.log, + ); + + this.nestedExecutions.push(childExecutionResult); + + return childExecutionResult.callStackItem; + } + + /** + * Creates a PublicCallStackItem object representing the request to call a public function. No function + * is actually called, since that must happen on the sequencer side. All the fields related to the result + * of the execution are empty. + * @param targetContractAddress - The address of the contract to call. + * @param functionSelector - The function selector of the function to call. + * @param argsHash - The packed arguments to pass to the function. + * @returns The public call stack item with the request information. + */ + public async enqueuePublicFunctionCall( + targetContractAddress: AztecAddress, + functionSelector: FunctionSelector, + argsHash: Fr, + ): Promise { + const targetAbi = await this.db.getFunctionABI(targetContractAddress, functionSelector); + const derivedCallContext = await this.deriveCallContext(targetContractAddress, false, false); + const args = this.packedArgsCache.unpack(argsHash); + const enqueuedRequest = PublicCallRequest.from({ + args, + callContext: derivedCallContext, + functionData: FunctionData.fromAbi(targetAbi), + contractAddress: targetContractAddress, + sideEffectCounter: this.sideEffectCounter++, // update after assigning current value to call + }); + + // TODO($846): if enqueued public calls are associated with global + // side-effect counter, that will leak info about how many other private + // side-effects occurred in the TX. Ultimately the private kernel should + // just output everything in the proper order without any counters. + this.log( + `Enqueued call to public function (with side-effect counter #${enqueuedRequest.sideEffectCounter}) ${targetContractAddress}:${functionSelector}`, + ); + + this.enqueuedPublicFunctionCalls.push(enqueuedRequest); + + return enqueuedRequest; + } + + /** + * Derives the call context for a nested execution. + * @param parentContext - The parent call context. + * @param targetContractAddress - The address of the contract being called. + * @param isDelegateCall - Whether the call is a delegate call. + * @param isStaticCall - Whether the call is a static call. + * @returns The derived call context. + */ + private async deriveCallContext(targetContractAddress: AztecAddress, isDelegateCall = false, isStaticCall = false) { + const portalContractAddress = await this.db.getPortalContractAddress(targetContractAddress); + return new CallContext( + this.contractAddress, + targetContractAddress, + portalContractAddress, + isDelegateCall, + isStaticCall, + false, + ); } } diff --git a/yarn-project/acir-simulator/src/client/db_oracle.ts b/yarn-project/acir-simulator/src/client/db_oracle.ts index 81ff7cbb67d..a2fb45431ae 100644 --- a/yarn-project/acir-simulator/src/client/db_oracle.ts +++ b/yarn-project/acir-simulator/src/client/db_oracle.ts @@ -4,46 +4,8 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; -import { CommitmentsDB } from '../index.js'; - -/** - * Information about a note needed during execution. - */ -export interface NoteData { - /** The contract address of the note. */ - contractAddress: AztecAddress; - /** The storage slot of the note. */ - storageSlot: Fr; - /** The nonce of the note. */ - nonce: Fr; - /** The preimage of the note */ - preimage: Fr[]; - /** The inner note hash of the note. */ - innerNoteHash: Fr; - /** The corresponding nullifier of the note. Undefined for pending notes. */ - siloedNullifier?: Fr; - /** The note's leaf index in the private data tree. Undefined for pending notes. */ - index?: bigint; -} - -/** - * The format that Aztec.nr uses to get L1 to L2 Messages. - */ -export interface MessageLoadOracleInputs { - /** - * An collapsed array of fields containing all of the l1 to l2 message components. - * `l1ToL2Message.toFieldArray()` -\> [sender, chainId, recipient, version, content, secretHash, deadline, fee] - */ - message: Fr[]; - /** - * The path in the merkle tree to the message. - */ - siblingPath: Fr[]; - /** - * The index of the message commitment in the merkle tree. - */ - index: bigint; -} +import { NoteData } from '../acvm/index.js'; +import { CommitmentsDB } from '../public/index.js'; /** * A function ABI with optional debug metadata diff --git a/yarn-project/acir-simulator/src/client/execution_note_cache.ts b/yarn-project/acir-simulator/src/client/execution_note_cache.ts index 45e883dda6f..51be35a4232 100644 --- a/yarn-project/acir-simulator/src/client/execution_note_cache.ts +++ b/yarn-project/acir-simulator/src/client/execution_note_cache.ts @@ -3,7 +3,7 @@ import { siloNullifier } from '@aztec/circuits.js/abis'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; -import { NoteData } from './db_oracle.js'; +import { NoteData } from '../acvm/index.js'; /** * Data that's accessible by all the function calls in an execution. diff --git a/yarn-project/acir-simulator/src/client/index.ts b/yarn-project/acir-simulator/src/client/index.ts index 5088bcf5d12..60fcf15d4a9 100644 --- a/yarn-project/acir-simulator/src/client/index.ts +++ b/yarn-project/acir-simulator/src/client/index.ts @@ -1,4 +1,3 @@ -export * from './private_execution.js'; export * from './simulator.js'; export * from './db_oracle.js'; export * from './execution_result.js'; diff --git a/yarn-project/acir-simulator/src/client/pick_notes.ts b/yarn-project/acir-simulator/src/client/pick_notes.ts index db62f1e8ed1..8f9fbddbff5 100644 --- a/yarn-project/acir-simulator/src/client/pick_notes.ts +++ b/yarn-project/acir-simulator/src/client/pick_notes.ts @@ -96,7 +96,7 @@ const sortNotes = (a: Fr[], b: Fr[], sorts: Sort[], level = 0): number => { export function pickNotes( notes: T[], { selects = [], sorts = [], limit = 0, offset = 0 }: GetOptions, -) { +): T[] { return selectNotes(notes, selects) .sort((a, b) => sortNotes(a.preimage, b.preimage, sorts)) .slice(offset, limit ? offset + limit : undefined); diff --git a/yarn-project/acir-simulator/src/client/private_execution.ts b/yarn-project/acir-simulator/src/client/private_execution.ts index 64516e8fde9..3de7e625b5f 100644 --- a/yarn-project/acir-simulator/src/client/private_execution.ts +++ b/yarn-project/acir-simulator/src/client/private_execution.ts @@ -1,355 +1,78 @@ -import { - CallContext, - ContractDeploymentData, - FunctionData, - PrivateCallStackItem, - PublicCallRequest, -} from '@aztec/circuits.js'; -import { Grumpkin } from '@aztec/circuits.js/barretenberg'; -import { FunctionSelector, decodeReturnValues } from '@aztec/foundation/abi'; +import { FunctionData, PrivateCallStackItem } from '@aztec/circuits.js'; +import { decodeReturnValues } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; -import { Fr, Point } from '@aztec/foundation/fields'; +import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { to2Fields } from '@aztec/foundation/serialize'; -import { FunctionL2Logs, NotePreimage, NoteSpendingInfo } from '@aztec/types'; -import { extractPrivateCircuitPublicInputs, frToAztecAddress } from '../acvm/deserialize.js'; -import { - ZERO_ACVM_FIELD, - acvm, - convertACVMFieldToBuffer, - extractCallStack, - fromACVMField, - toACVMField, - toACVMWitness, - toAcvmCallPrivateStackItem, - toAcvmEnqueuePublicFunctionResult, -} from '../acvm/index.js'; +import { extractPrivateCircuitPublicInputs } from '../acvm/deserialize.js'; +import { Oracle, acvm, extractCallStack } from '../acvm/index.js'; import { ExecutionError } from '../common/errors.js'; -import { AcirSimulator, ExecutionResult, FunctionAbiWithDebugMetadata } from '../index.js'; -import { ClientTxExecutionContext } from './client_execution_context.js'; -import { acvmFieldMessageToString, oracleDebugCallToFormattedStr } from './debug.js'; +import { ClientExecutionContext } from './client_execution_context.js'; +import { FunctionAbiWithDebugMetadata } from './db_oracle.js'; +import { ExecutionResult } from './execution_result.js'; +import { AcirSimulator } from './simulator.js'; /** - * The private function execution class. + * Execute a private function and return the execution result. */ -export class PrivateFunctionExecution { - constructor( - private context: ClientTxExecutionContext, - private abi: FunctionAbiWithDebugMetadata, - private contractAddress: AztecAddress, - private functionData: FunctionData, - private argsHash: Fr, - private callContext: CallContext, - private curve: Grumpkin, - private sideEffectCounter: number = 0, - private log = createDebugLogger('aztec:simulator:secret_execution'), - ) {} - - /** - * Executes the function. - * @returns The execution result. - */ - public async run(): Promise { - const selector = this.functionData.selector; - this.log(`Executing external function ${this.contractAddress}:${selector}`); - - const acir = Buffer.from(this.abi.bytecode, 'base64'); - const initialWitness = this.getInitialWitness(); - - const nestedExecutionContexts: ExecutionResult[] = []; - const enqueuedPublicFunctionCalls: PublicCallRequest[] = []; - // TODO: Move to ClientTxExecutionContext. - const encryptedLogs = new FunctionL2Logs([]); - const unencryptedLogs = new FunctionL2Logs([]); - - const { partialWitness } = await acvm(await AcirSimulator.getSolver(), acir, initialWitness, { - computeSelector: (...args) => { - const signature = oracleDebugCallToFormattedStr(args); - const returnValue = toACVMField(FunctionSelector.fromSignature(signature).toField()); - return Promise.resolve(returnValue); - }, - packArguments: async args => { - return toACVMField(await this.context.packedArgsCache.pack(args.map(fromACVMField))); - }, - getAuthWitness: async ([messageHash]) => { - const witness = await this.context.getAuthWitness(fromACVMField(messageHash)); - if (!witness) throw new Error(`Authorization not found for message hash ${fromACVMField(messageHash)}`); - return witness.map(toACVMField); - }, - getSecretKey: ([ownerX], [ownerY]) => this.context.getSecretKey(this.contractAddress, ownerX, ownerY), - getPublicKey: async ([acvmAddress]) => { - const address = frToAztecAddress(fromACVMField(acvmAddress)); - const { publicKey, partialAddress } = await this.context.db.getCompleteAddress(address); - return [publicKey.x, publicKey.y, partialAddress].map(toACVMField); - }, - getNotes: ([slot], [numSelects], selectBy, selectValues, sortBy, sortOrder, [limit], [offset], [returnSize]) => - this.context.getNotes( - this.contractAddress, - slot, - +numSelects, - selectBy, - selectValues, - sortBy, - sortOrder, - +limit, - +offset, - +returnSize, - ), - getRandomField: () => Promise.resolve(toACVMField(Fr.random())), - notifyCreatedNote: ([storageSlot], preimage, [innerNoteHash]) => { - this.context.handleNewNote(this.contractAddress, storageSlot, preimage, innerNoteHash); - return Promise.resolve(ZERO_ACVM_FIELD); - }, - notifyNullifiedNote: async ([innerNullifier], [innerNoteHash]) => { - await this.context.handleNullifiedNote(this.contractAddress, innerNullifier, innerNoteHash); - return Promise.resolve(ZERO_ACVM_FIELD); - }, - checkNoteHashExists: ([nonce], [innerNoteHash]) => - this.context.checkNoteHashExists(this.contractAddress, nonce, innerNoteHash), - callPrivateFunction: async ([acvmContractAddress], [acvmFunctionSelector], [acvmArgsHash]) => { - const contractAddress = fromACVMField(acvmContractAddress); - const functionSelector = fromACVMField(acvmFunctionSelector); - this.log( - `Calling private function ${contractAddress.toString()}:${functionSelector} from ${this.callContext.storageContractAddress.toString()}`, - ); - - const childExecutionResult = await this.callPrivateFunction( - frToAztecAddress(contractAddress), - FunctionSelector.fromField(functionSelector), - fromACVMField(acvmArgsHash), - this.callContext, - this.curve, - ); - - nestedExecutionContexts.push(childExecutionResult); - - return toAcvmCallPrivateStackItem(childExecutionResult.callStackItem); - }, - getL1ToL2Message: ([msgKey]) => { - return this.context.getL1ToL2Message(fromACVMField(msgKey)); - }, - debugLog: (...args) => { - this.log(oracleDebugCallToFormattedStr(args)); - return Promise.resolve(ZERO_ACVM_FIELD); - }, - debugLogWithPrefix: (arg0, ...args) => { - this.log(`${acvmFieldMessageToString(arg0)}: ${oracleDebugCallToFormattedStr(args)}`); - return Promise.resolve(ZERO_ACVM_FIELD); - }, - enqueuePublicFunctionCall: async ([acvmContractAddress], [acvmFunctionSelector], [acvmArgsHash]) => { - const selector = FunctionSelector.fromField(fromACVMField(acvmFunctionSelector)); - const enqueuedRequest = await this.enqueuePublicFunctionCall( - frToAztecAddress(fromACVMField(acvmContractAddress)), - selector, - this.context.packedArgsCache.unpack(fromACVMField(acvmArgsHash)), - this.callContext, - ); - - this.log( - `Enqueued call to public function (with side-effect counter #${enqueuedRequest.sideEffectCounter}) ${acvmContractAddress}:${selector}`, - ); - enqueuedPublicFunctionCalls.push(enqueuedRequest); - return toAcvmEnqueuePublicFunctionResult(enqueuedRequest); - }, - emitUnencryptedLog: message => { - // https://github.com/AztecProtocol/aztec-packages/issues/885 - const log = Buffer.concat(message.map(charBuffer => convertACVMFieldToBuffer(charBuffer).subarray(-1))); - unencryptedLogs.logs.push(log); - this.log(`Emitted unencrypted log: "${log.toString('ascii')}"`); - return Promise.resolve(ZERO_ACVM_FIELD); - }, - emitEncryptedLog: ([acvmContractAddress], [acvmStorageSlot], [encPubKeyX], [encPubKeyY], acvmPreimage) => { - const contractAddress = AztecAddress.fromBuffer(convertACVMFieldToBuffer(acvmContractAddress)); - const storageSlot = fromACVMField(acvmStorageSlot); - const preimage = acvmPreimage.map(f => fromACVMField(f)); - - const notePreimage = new NotePreimage(preimage); - const noteSpendingInfo = new NoteSpendingInfo(notePreimage, contractAddress, storageSlot); - const ownerPublicKey = new Point(fromACVMField(encPubKeyX), fromACVMField(encPubKeyY)); - - const encryptedNotePreimage = noteSpendingInfo.toEncryptedBuffer(ownerPublicKey, this.curve); - - encryptedLogs.logs.push(encryptedNotePreimage); - - return Promise.resolve(ZERO_ACVM_FIELD); - }, - getPortalContractAddress: async ([aztecAddress]) => { - const contractAddress = AztecAddress.fromString(aztecAddress); - const portalContactAddress = await this.context.db.getPortalContractAddress(contractAddress); - return Promise.resolve(toACVMField(portalContactAddress)); - }, - }).catch((err: Error) => { +export async function executePrivateFunction( + context: ClientExecutionContext, + abi: FunctionAbiWithDebugMetadata, + contractAddress: AztecAddress, + functionData: FunctionData, + log = createDebugLogger('aztec:simulator:secret_execution'), +): Promise { + const functionSelector = functionData.selector; + log(`Executing external function ${contractAddress}:${functionSelector}`); + + const acir = Buffer.from(abi.bytecode, 'base64'); + const initialWitness = context.getInitialWitness(); + const acvmCallback = new Oracle(context); + const { partialWitness } = await acvm(await AcirSimulator.getSolver(), acir, initialWitness, acvmCallback).catch( + (err: Error) => { throw new ExecutionError( err.message, { - contractAddress: this.contractAddress, - functionSelector: selector, + contractAddress, + functionSelector, }, - extractCallStack(err, this.abi.debug), + extractCallStack(err, abi.debug), { cause: err }, ); - }); - - const publicInputs = extractPrivateCircuitPublicInputs(partialWitness, acir); - - // TODO(https://github.com/AztecProtocol/aztec-packages/issues/1165) --> set this in Noir - publicInputs.encryptedLogsHash = to2Fields(encryptedLogs.hash()); - publicInputs.encryptedLogPreimagesLength = new Fr(encryptedLogs.getSerializedLength()); - publicInputs.unencryptedLogsHash = to2Fields(unencryptedLogs.hash()); - publicInputs.unencryptedLogPreimagesLength = new Fr(unencryptedLogs.getSerializedLength()); - - const callStackItem = new PrivateCallStackItem(this.contractAddress, this.functionData, publicInputs, false); - const returnValues = decodeReturnValues(this.abi, publicInputs.returnValues); - - this.log(`Returning from call to ${this.contractAddress.toString()}:${selector}`); - - const readRequestPartialWitnesses = this.context.getReadRequestPartialWitnesses(publicInputs.readRequests); - const newNotes = this.context.getNewNotes(); - - return { - acir, - partialWitness, - callStackItem, - returnValues, - readRequestPartialWitnesses, - newNotes, - vk: Buffer.from(this.abi.verificationKey!, 'hex'), - nestedExecutions: nestedExecutionContexts, - enqueuedPublicFunctionCalls, - encryptedLogs, - unencryptedLogs, - }; - } - - // We still need this function until we can get user-defined ordering of structs for fn arguments - // TODO When that is sorted out on noir side, we can use instead the utilities in serialize.ts - /** - * Writes the function inputs to the initial witness. - * @returns The initial witness. - */ - private getInitialWitness() { - const contractDeploymentData = this.context.txContext.contractDeploymentData ?? ContractDeploymentData.empty(); - - const blockData = this.context.historicBlockData; - - const fields = [ - this.callContext.msgSender, - this.callContext.storageContractAddress, - this.callContext.portalContractAddress, - this.callContext.isDelegateCall, - this.callContext.isStaticCall, - this.callContext.isContractDeployment, - - ...blockData.toArray(), - - contractDeploymentData.deployerPublicKey.x, - contractDeploymentData.deployerPublicKey.y, - contractDeploymentData.constructorVkHash, - contractDeploymentData.functionTreeRoot, - contractDeploymentData.contractAddressSalt, - contractDeploymentData.portalContractAddress, - - this.context.txContext.chainId, - this.context.txContext.version, - - ...this.context.packedArgsCache.unpack(this.argsHash), - ]; - - return toACVMWitness(1, fields); - } - - /** - * Calls a private function as a nested execution. - * @param targetContractAddress - The address of the contract to call. - * @param targetFunctionSelector - The function selector of the function to call. - * @param targetArgsHash - The packed arguments to pass to the function. - * @param callerContext - The call context of the caller. - * @param curve - The curve instance to use for elliptic curve operations. - * @returns The execution result. - */ - private async callPrivateFunction( - targetContractAddress: AztecAddress, - targetFunctionSelector: FunctionSelector, - targetArgsHash: Fr, - callerContext: CallContext, - curve: Grumpkin, - ) { - const targetAbi = await this.context.db.getFunctionABI(targetContractAddress, targetFunctionSelector); - const targetFunctionData = FunctionData.fromAbi(targetAbi); - const derivedCallContext = await this.deriveCallContext(callerContext, targetContractAddress, false, false); - const context = this.context.extend(); - - const nestedExecution = new PrivateFunctionExecution( - context, - targetAbi, - targetContractAddress, - targetFunctionData, - targetArgsHash, - derivedCallContext, - curve, - this.sideEffectCounter, - this.log, - ); - - return nestedExecution.run(); - } - - /** - * Creates a PublicCallStackItem object representing the request to call a public function. No function - * is actually called, since that must happen on the sequencer side. All the fields related to the result - * of the execution are empty. - * @param targetContractAddress - The address of the contract to call. - * @param targetFunctionSelector - The function selector of the function to call. - * @param targetArgs - The arguments to pass to the function. - * @param callerContext - The call context of the caller. - * @returns The public call stack item with the request information. - */ - private async enqueuePublicFunctionCall( - targetContractAddress: AztecAddress, - targetFunctionSelector: FunctionSelector, - targetArgs: Fr[], - callerContext: CallContext, - ): Promise { - const targetAbi = await this.context.db.getFunctionABI(targetContractAddress, targetFunctionSelector); - const derivedCallContext = await this.deriveCallContext(callerContext, targetContractAddress, false, false); - - return PublicCallRequest.from({ - args: targetArgs, - callContext: derivedCallContext, - functionData: FunctionData.fromAbi(targetAbi), - contractAddress: targetContractAddress, - sideEffectCounter: this.sideEffectCounter++, // update after assigning current value to call - }); - - // TODO($846): if enqueued public calls are associated with global - // side-effect counter, that will leak info about how many other private - // side-effects occurred in the TX. Ultimately the private kernel should - // just output everything in the proper order without any counters. - } - - /** - * Derives the call context for a nested execution. - * @param parentContext - The parent call context. - * @param targetContractAddress - The address of the contract being called. - * @param isDelegateCall - Whether the call is a delegate call. - * @param isStaticCall - Whether the call is a static call. - * @returns The derived call context. - */ - private async deriveCallContext( - parentContext: CallContext, - targetContractAddress: AztecAddress, - isDelegateCall = false, - isStaticCall = false, - ) { - const portalContractAddress = await this.context.db.getPortalContractAddress(targetContractAddress); - return new CallContext( - parentContext.storageContractAddress, - targetContractAddress, - portalContractAddress, - isDelegateCall, - isStaticCall, - false, - ); - } + }, + ); + + const publicInputs = extractPrivateCircuitPublicInputs(partialWitness, acir); + + const encryptedLogs = context.getEncryptedLogs(); + const unencryptedLogs = context.getUnencryptedLogs(); + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/1165) --> set this in Noir + publicInputs.encryptedLogsHash = to2Fields(encryptedLogs.hash()); + publicInputs.encryptedLogPreimagesLength = new Fr(encryptedLogs.getSerializedLength()); + publicInputs.unencryptedLogsHash = to2Fields(unencryptedLogs.hash()); + publicInputs.unencryptedLogPreimagesLength = new Fr(unencryptedLogs.getSerializedLength()); + + const callStackItem = new PrivateCallStackItem(contractAddress, functionData, publicInputs, false); + const returnValues = decodeReturnValues(abi, publicInputs.returnValues); + const readRequestPartialWitnesses = context.getReadRequestPartialWitnesses(publicInputs.readRequests); + const newNotes = context.getNewNotes(); + const nestedExecutions = context.getNestedExecutions(); + const enqueuedPublicFunctionCalls = context.getEnqueuedPublicFunctionCalls(); + + log(`Returning from call to ${contractAddress.toString()}:${functionSelector}`); + + return { + acir, + partialWitness, + callStackItem, + returnValues, + readRequestPartialWitnesses, + newNotes, + vk: Buffer.from(abi.verificationKey!, 'hex'), + nestedExecutions, + enqueuedPublicFunctionCalls, + encryptedLogs, + unencryptedLogs, + }; } diff --git a/yarn-project/acir-simulator/src/client/simulator.ts b/yarn-project/acir-simulator/src/client/simulator.ts index f00be81213e..e9d1d9bfc38 100644 --- a/yarn-project/acir-simulator/src/client/simulator.ts +++ b/yarn-project/acir-simulator/src/client/simulator.ts @@ -1,4 +1,4 @@ -import { CallContext, FunctionData, MAX_NOTE_FIELDS_LENGTH, TxContext } from '@aztec/circuits.js'; +import { CallContext, FunctionData, MAX_NOTE_FIELDS_LENGTH } from '@aztec/circuits.js'; import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { ArrayType, FunctionSelector, FunctionType, encodeArguments } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; @@ -11,12 +11,13 @@ import { WasmBlackBoxFunctionSolver, createBlackBoxSolver } from '@noir-lang/acv import { createSimulationError } from '../common/errors.js'; import { PackedArgsCache } from '../common/packed_args_cache.js'; -import { ClientTxExecutionContext } from './client_execution_context.js'; +import { ClientExecutionContext } from './client_execution_context.js'; import { DBOracle, FunctionAbiWithDebugMetadata } from './db_oracle.js'; import { ExecutionNoteCache } from './execution_note_cache.js'; import { ExecutionResult } from './execution_result.js'; -import { PrivateFunctionExecution } from './private_execution.js'; -import { UnconstrainedFunctionExecution } from './unconstrained_execution.js'; +import { executePrivateFunction } from './private_execution.js'; +import { executeUnconstrainedFunction } from './unconstrained_execution.js'; +import { ViewDataOracle } from './view_data_oracle.js'; /** * The ACIR simulator. @@ -84,26 +85,28 @@ export class AcirSimulator { false, request.functionData.isConstructor, ); - - const execution = new PrivateFunctionExecution( - new ClientTxExecutionContext( - this.db, - request.txContext, - historicBlockData, - await PackedArgsCache.create(request.packedArguments), - new ExecutionNoteCache(), - request.authWitnesses, - ), - entryPointABI, + const context = new ClientExecutionContext( contractAddress, - request.functionData, request.argsHash, + request.txContext, callContext, + historicBlockData, + request.authWitnesses, + 0, + await PackedArgsCache.create(request.packedArguments), + new ExecutionNoteCache(), + this.db, curve, ); try { - return await execution.run(); + const executionResult = await executePrivateFunction( + context, + entryPointABI, + contractAddress, + request.functionData, + ); + return executionResult; } catch (err) { throw createSimulationError(err instanceof Error ? err : new Error('Unknown error during private execution')); } @@ -112,19 +115,14 @@ export class AcirSimulator { /** * Runs an unconstrained function. * @param request - The transaction request. - * @param origin - The sender of the request. * @param entryPointABI - The ABI of the entry point function. * @param contractAddress - The address of the contract. - * @param portalContractAddress - The address of the portal contract. - * @param historicBlockData - Block data containing historic roots. * @param aztecNode - The AztecNode instance. */ public async runUnconstrained( request: FunctionCall, - origin: AztecAddress, entryPointABI: FunctionAbiWithDebugMetadata, contractAddress: AztecAddress, - portalContractAddress: EthAddress, aztecNode?: AztecNode, ) { if (entryPointABI.functionType !== FunctionType.UNCONSTRAINED) { @@ -132,33 +130,16 @@ export class AcirSimulator { } const historicBlockData = await this.db.getHistoricBlockData(); - const callContext = new CallContext( - origin, - contractAddress, - portalContractAddress, - false, - false, - request.functionData.isConstructor, - ); - - const execution = new UnconstrainedFunctionExecution( - new ClientTxExecutionContext( - this.db, - TxContext.empty(), - historicBlockData, - await PackedArgsCache.create([]), - new ExecutionNoteCache(), - [], - ), - entryPointABI, - contractAddress, - request.functionData, - request.args, - callContext, - ); + const context = new ViewDataOracle(contractAddress, historicBlockData, [], this.db, aztecNode); try { - return await execution.run(aztecNode); + return await executeUnconstrainedFunction( + context, + entryPointABI, + contractAddress, + request.functionData, + request.args, + ); } catch (err) { throw createSimulationError(err instanceof Error ? err : new Error('Unknown error during private execution')); } @@ -209,10 +190,8 @@ export class AcirSimulator { const [innerNoteHash, siloedNoteHash, uniqueSiloedNoteHash, innerNullifier] = (await this.runUnconstrained( execRequest, - AztecAddress.ZERO, abi, AztecAddress.ZERO, - EthAddress.ZERO, )) as bigint[]; return { diff --git a/yarn-project/acir-simulator/src/client/unconstrained_execution.test.ts b/yarn-project/acir-simulator/src/client/unconstrained_execution.test.ts index 3e38c3999b4..2cb8fa654ac 100644 --- a/yarn-project/acir-simulator/src/client/unconstrained_execution.test.ts +++ b/yarn-project/acir-simulator/src/client/unconstrained_execution.test.ts @@ -1,7 +1,6 @@ import { CompleteAddress, FunctionData, HistoricBlockData } from '@aztec/circuits.js'; import { FunctionSelector, encodeArguments } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; -import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr, GrumpkinScalar } from '@aztec/foundation/fields'; import { StatefulTestContractAbi } from '@aztec/noir-contracts/artifacts'; import { FunctionCall } from '@aztec/types'; @@ -65,13 +64,7 @@ describe('Unconstrained Execution test suite', () => { args: encodeArguments(abi, [owner]), }; - const result = await acirSimulator.runUnconstrained( - execRequest, - AztecAddress.random(), - abi, - AztecAddress.random(), - EthAddress.ZERO, - ); + const result = await acirSimulator.runUnconstrained(execRequest, abi, AztecAddress.random()); expect(result).toEqual(9n); }, 30_000); diff --git a/yarn-project/acir-simulator/src/client/unconstrained_execution.ts b/yarn-project/acir-simulator/src/client/unconstrained_execution.ts index 4a1d8fc89c3..85504664c83 100644 --- a/yarn-project/acir-simulator/src/client/unconstrained_execution.ts +++ b/yarn-project/acir-simulator/src/client/unconstrained_execution.ts @@ -1,131 +1,49 @@ -import { CallContext, FunctionData } from '@aztec/circuits.js'; -import { DecodedReturn, FunctionSelector, decodeReturnValues } from '@aztec/foundation/abi'; +import { FunctionData } from '@aztec/circuits.js'; +import { DecodedReturn, decodeReturnValues } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; -import { AztecNode } from '@aztec/types'; -import { extractReturnWitness, frToAztecAddress } from '../acvm/deserialize.js'; -import { - ACVMField, - ZERO_ACVM_FIELD, - acvm, - extractCallStack, - fromACVMField, - toACVMField, - toACVMWitness, -} from '../acvm/index.js'; +import { extractReturnWitness } from '../acvm/deserialize.js'; +import { ACVMField, Oracle, acvm, extractCallStack, fromACVMField, toACVMWitness } from '../acvm/index.js'; import { ExecutionError } from '../common/errors.js'; import { AcirSimulator } from '../index.js'; -import { ClientTxExecutionContext } from './client_execution_context.js'; import { FunctionAbiWithDebugMetadata } from './db_oracle.js'; -import { oracleDebugCallToFormattedStr } from './debug.js'; +import { ViewDataOracle } from './view_data_oracle.js'; /** - * The unconstrained function execution class. + * Execute an unconstrained function and return the decoded values. */ -export class UnconstrainedFunctionExecution { - constructor( - private context: ClientTxExecutionContext, - private abi: FunctionAbiWithDebugMetadata, - private contractAddress: AztecAddress, - private functionData: FunctionData, - private args: Fr[], - _: CallContext, // not used ATM - - private log = createDebugLogger('aztec:simulator:unconstrained_execution'), - ) {} - - /** - * Executes the unconstrained function. - * @param aztecNode - The aztec node. - * @returns The return values of the executed function. - */ - public async run(aztecNode?: AztecNode): Promise { - this.log(`Executing unconstrained function ${this.contractAddress.toShortString()}:${this.functionData.selector}`); - - const acir = Buffer.from(this.abi.bytecode, 'base64'); - const initialWitness = toACVMWitness(1, this.args); - - const { partialWitness } = await acvm(await AcirSimulator.getSolver(), acir, initialWitness, { - computeSelector: (...args) => { - const signature = oracleDebugCallToFormattedStr(args); - const returnValue = toACVMField(FunctionSelector.fromSignature(signature).toField()); - return Promise.resolve(returnValue); - }, - getSecretKey: ([ownerX], [ownerY]) => this.context.getSecretKey(this.contractAddress, ownerX, ownerY), - getPublicKey: async ([acvmAddress]) => { - const address = frToAztecAddress(fromACVMField(acvmAddress)); - const { publicKey, partialAddress } = await this.context.db.getCompleteAddress(address); - return [publicKey.x, publicKey.y, partialAddress].map(toACVMField); - }, - getNotes: ([slot], [numSelects], selectBy, selectValues, sortBy, sortOrder, [limit], [offset], [returnSize]) => - this.context.getNotes( - this.contractAddress, - slot, - +numSelects, - selectBy, - selectValues, - sortBy, - sortOrder, - +limit, - +offset, - +returnSize, - ), - checkNoteHashExists: ([nonce], [innerNoteHash]) => - this.context.checkNoteHashExists(this.contractAddress, nonce, innerNoteHash), - getRandomField: () => Promise.resolve(toACVMField(Fr.random())), - debugLog: (...params) => { - this.log(oracleDebugCallToFormattedStr(params)); - return Promise.resolve(ZERO_ACVM_FIELD); +export async function executeUnconstrainedFunction( + oracle: ViewDataOracle, + abi: FunctionAbiWithDebugMetadata, + contractAddress: AztecAddress, + functionData: FunctionData, + args: Fr[], + log = createDebugLogger('aztec:simulator:unconstrained_execution'), +): Promise { + const functionSelector = functionData.selector; + log(`Executing unconstrained function ${contractAddress}:${functionSelector}`); + + const acir = Buffer.from(abi.bytecode, 'base64'); + const initialWitness = toACVMWitness(1, args); + const { partialWitness } = await acvm( + await AcirSimulator.getSolver(), + acir, + initialWitness, + new Oracle(oracle), + ).catch((err: Error) => { + throw new ExecutionError( + err.message, + { + contractAddress, + functionSelector, }, - getL1ToL2Message: ([msgKey]) => this.context.getL1ToL2Message(fromACVMField(msgKey)), - storageRead: async ([slot], [numberOfElements]) => { - if (!aztecNode) { - const errMsg = `Aztec node is undefined, cannot read storage`; - this.log.error(errMsg); - throw new Error(errMsg); - } - - const makeLogMsg = (slot: bigint, value: string) => - `Oracle storage read: slot=${slot.toString(16)} value=${value}`; - - const startStorageSlot = fromACVMField(slot); - const values = []; - for (let i = 0; i < Number(numberOfElements); i++) { - const storageSlot = startStorageSlot.value + BigInt(i); - const value = await aztecNode.getPublicStorageAt(this.contractAddress, storageSlot); - if (value === undefined) { - const logMsg = makeLogMsg(storageSlot, 'undefined'); - this.log(logMsg); - throw new Error(logMsg); - } - const frValue = Fr.fromBuffer(value); - const logMsg = makeLogMsg(storageSlot, frValue.toString()); - this.log(logMsg); - values.push(frValue); - } - return values.map(v => toACVMField(v)); - }, - getPortalContractAddress: async ([aztecAddress]) => { - const contractAddress = AztecAddress.fromString(aztecAddress); - const portalContactAddress = await this.context.db.getPortalContractAddress(contractAddress); - return Promise.resolve(toACVMField(portalContactAddress)); - }, - }).catch((err: Error) => { - throw new ExecutionError( - err.message, - { - contractAddress: this.contractAddress, - functionSelector: this.functionData.selector, - }, - extractCallStack(err, this.abi.debug), - { cause: err }, - ); - }); - - const returnValues: ACVMField[] = extractReturnWitness(acir, partialWitness); + extractCallStack(err, abi.debug), + { cause: err }, + ); + }); - return decodeReturnValues(this.abi, returnValues.map(fromACVMField)); - } + const returnValues: ACVMField[] = extractReturnWitness(acir, partialWitness); + return decodeReturnValues(abi, returnValues.map(fromACVMField)); } diff --git a/yarn-project/acir-simulator/src/client/view_data_oracle.ts b/yarn-project/acir-simulator/src/client/view_data_oracle.ts new file mode 100644 index 00000000000..48a7466494e --- /dev/null +++ b/yarn-project/acir-simulator/src/client/view_data_oracle.ts @@ -0,0 +1,165 @@ +import { CircuitsWasm, HistoricBlockData, PublicKey } from '@aztec/circuits.js'; +import { computeUniqueCommitment, siloCommitment } from '@aztec/circuits.js/abis'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { Fr } from '@aztec/foundation/fields'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { AuthWitness, AztecNode, CompleteAddress } from '@aztec/types'; + +import { NoteData, TypedOracle } from '../acvm/index.js'; +import { DBOracle } from './db_oracle.js'; +import { pickNotes } from './pick_notes.js'; + +/** + * The execution context for a client view tx simulation. + * It only reads data from data sources. Nothing will be updated or created during this simulation. + */ +export class ViewDataOracle extends TypedOracle { + constructor( + protected readonly contractAddress: AztecAddress, + /** Data required to reconstruct the block hash, it contains historic roots. */ + protected readonly historicBlockData: HistoricBlockData, + /** List of transient auth witnesses to be used during this simulation */ + protected readonly authWitnesses: AuthWitness[], + protected readonly db: DBOracle, + protected readonly aztecNode: AztecNode | undefined, + protected log = createDebugLogger('aztec:simulator:client_view_context'), + ) { + super(); + } + + /** + * Return the secret key of a owner to use in a specific contract. + * @param owner - The owner of the secret key. + */ + public getSecretKey(owner: PublicKey) { + return this.db.getSecretKey(this.contractAddress, owner); + } + + /** + * Retrieve the complete address associated to a given address. + * @param address - Address to fetch the complete address for. + * @returns A complete address associated with the input address. + */ + public getPublicKey(address: AztecAddress): Promise { + return this.db.getCompleteAddress(address); + } + + /** + * Returns an auth witness for the given message hash. Checks on the list of transient witnesses + * for this transaction first, and falls back to the local database if not found. + * @param messageHash - Hash of the message to authenticate. + * @returns Authentication witness for the requested message hash. + */ + public getAuthWitness(messageHash: Fr): Promise { + return Promise.resolve( + this.authWitnesses.find(w => w.requestHash.equals(messageHash))?.witness ?? this.db.getAuthWitness(messageHash), + ); + } + + /** + * Gets some notes for a contract address and storage slot. + * Returns a flattened array containing real-note-count and note preimages. + * + * @remarks + * + * Check for pending notes with matching address/slot. + * Real notes coming from DB will have a leafIndex which + * represents their index in the private data tree. + * + * @param contractAddress - The contract address. + * @param storageSlot - The storage slot. + * @param numSelects - The number of valid selects in selectBy and selectValues. + * @param selectBy - An array of indices of the fields to selects. + * @param selectValues - The values to match. + * @param sortBy - An array of indices of the fields to sort. + * @param sortOrder - The order of the corresponding index in sortBy. (1: DESC, 2: ASC, 0: Do nothing) + * @param limit - The number of notes to retrieve per query. + * @param offset - The starting index for pagination. + * @returns Flattened array of ACVMFields (format expected by Noir/ACVM) containing: + * count - number of real (non-padding) notes retrieved, + * contractAddress - the contract address, + * preimages - the real note preimages retrieved, and + * paddedZeros - zeros to ensure an array with length returnSize expected by Noir circuit + */ + public async getNotes( + storageSlot: Fr, + numSelects: number, + selectBy: number[], + selectValues: Fr[], + sortBy: number[], + sortOrder: number[], + limit: number, + offset: number, + ): Promise { + const dbNotes = await this.db.getNotes(this.contractAddress, storageSlot); + return pickNotes(dbNotes, { + selects: selectBy.slice(0, numSelects).map((index, i) => ({ index, value: selectValues[i] })), + sorts: sortBy.map((index, i) => ({ index, order: sortOrder[i] })), + limit, + offset, + }); + } + + /** + * Fetches a path to prove existence of a commitment in the db, given its contract side commitment (before silo). + * @param nonce - The nonce of the note. + * @param innerNoteHash - The inner note hash of the note. + * @returns 1 if (persistent or transient) note hash exists, 0 otherwise. Value is in ACVMField form. + */ + public async checkNoteHashExists(nonce: Fr, innerNoteHash: Fr): Promise { + // If nonce is zero, SHOULD only be able to reach this point if note was publicly created + const wasm = await CircuitsWasm.get(); + let noteHashToLookUp = siloCommitment(wasm, this.contractAddress, innerNoteHash); + if (!nonce.isZero()) { + noteHashToLookUp = computeUniqueCommitment(wasm, nonce, noteHashToLookUp); + } + + const index = await this.db.getCommitmentIndex(noteHashToLookUp); + return index !== undefined; + } + + /** + * Fetches the a message from the db, given its key. + * @param msgKey - A buffer representing the message key. + * @returns The l1 to l2 message data + */ + public async getL1ToL2Message(msgKey: Fr) { + const message = await this.db.getL1ToL2Message(msgKey); + return { ...message, root: this.historicBlockData.l1ToL2MessagesTreeRoot }; + } + + /** + * Retrieves the portal contract address associated with the given contract address. + * Throws an error if the input contract address is not found or invalid. + * @param contractAddress - The address of the contract whose portal address is to be fetched. + * @returns The portal contract address. + */ + public getPortalContractAddress(contractAddress: AztecAddress) { + return this.db.getPortalContractAddress(contractAddress); + } + + /** + * Read the public storage data. + * @param startStorageSlot - The starting storage slot. + * @param numberOfElements - Number of elements to read from the starting storage slot. + */ + public async storageRead(startStorageSlot: Fr, numberOfElements: number) { + if (!this.aztecNode) { + throw new Error('Aztec node is undefined, cannot read storage.'); + } + + const values = []; + for (let i = 0; i < Number(numberOfElements); i++) { + const storageSlot = startStorageSlot.value + BigInt(i); + const value = await this.aztecNode.getPublicStorageAt(this.contractAddress, storageSlot); + if (value === undefined) { + throw new Error(`Oracle storage read undefined: slot=${storageSlot.toString(16)}`); + } + + const frValue = Fr.fromBuffer(value); + this.log(`Oracle storage read: slot=${storageSlot.toString(16)} value=${frValue}`); + values.push(frValue); + } + return values; + } +} diff --git a/yarn-project/acir-simulator/src/index.ts b/yarn-project/acir-simulator/src/index.ts index b1da6dc55c3..d51150943c6 100644 --- a/yarn-project/acir-simulator/src/index.ts +++ b/yarn-project/acir-simulator/src/index.ts @@ -1,4 +1,4 @@ export * from './client/index.js'; -export * from './acvm/acvm.js'; +export * from './acvm/index.js'; export * from './public/index.js'; export { computeSlotForMapping } from './utils.js'; diff --git a/yarn-project/acir-simulator/src/public/db.ts b/yarn-project/acir-simulator/src/public/db.ts index 245c3f103c7..1cf022f93f5 100644 --- a/yarn-project/acir-simulator/src/public/db.ts +++ b/yarn-project/acir-simulator/src/public/db.ts @@ -2,7 +2,7 @@ import { EthAddress, FunctionSelector } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; -import { MessageLoadOracleInputs } from '../index.js'; +import { MessageLoadOracleInputs } from '../acvm/index.js'; /** * Database interface for providing access to public state. diff --git a/yarn-project/acir-simulator/src/public/executor.ts b/yarn-project/acir-simulator/src/public/executor.ts index ca1451771dd..0f9738efff4 100644 --- a/yarn-project/acir-simulator/src/public/executor.ts +++ b/yarn-project/acir-simulator/src/public/executor.ts @@ -1,40 +1,76 @@ -import { - AztecAddress, - CallContext, - CircuitsWasm, - EthAddress, - Fr, - FunctionData, - FunctionSelector, - GlobalVariables, - HistoricBlockData, - RETURN_VALUES_LENGTH, -} from '@aztec/circuits.js'; -import { siloCommitment } from '@aztec/circuits.js/abis'; -import { padArrayEnd } from '@aztec/foundation/collection'; +import { GlobalVariables, HistoricBlockData } from '@aztec/circuits.js'; import { createDebugLogger } from '@aztec/foundation/log'; -import { FunctionL2Logs } from '@aztec/types'; -import { - ONE_ACVM_FIELD, - ZERO_ACVM_FIELD, - acvm, - convertACVMFieldToBuffer, - extractCallStack, - extractPublicCircuitPublicInputs, - frToAztecAddress, - fromACVMField, - toACVMField, - toACVMWitness, - toAcvmL1ToL2MessageLoadOracleInputs, -} from '../acvm/index.js'; -import { oracleDebugCallToFormattedStr } from '../client/debug.js'; +import { Oracle, acvm, extractCallStack, extractPublicCircuitPublicInputs } from '../acvm/index.js'; import { ExecutionError, createSimulationError } from '../common/errors.js'; import { PackedArgsCache } from '../common/packed_args_cache.js'; import { AcirSimulator } from '../index.js'; import { CommitmentsDB, PublicContractsDB, PublicStateDB } from './db.js'; import { PublicExecution, PublicExecutionResult } from './execution.js'; -import { ContractStorageActionsCollector } from './state_actions.js'; +import { PublicExecutionContext } from './public_execution_context.js'; + +/** + * Execute a public function and return the execution result. + */ +export async function executePublicFunction( + context: PublicExecutionContext, + acir: Buffer, + log = createDebugLogger('aztec:simulator:public_execution'), +): Promise { + const execution = context.execution; + const { contractAddress, functionData } = execution; + const selector = functionData.selector; + log(`Executing public external function ${contractAddress.toString()}:${selector}`); + + const initialWitness = context.getInitialWitness(); + const acvmCallback = new Oracle(context); + const { partialWitness } = await acvm(await AcirSimulator.getSolver(), acir, initialWitness, acvmCallback).catch( + (err: Error) => { + throw new ExecutionError( + err.message, + { + contractAddress, + functionSelector: selector, + }, + extractCallStack(err), + { cause: err }, + ); + }, + ); + + const { + returnValues, + newL2ToL1Msgs, + newCommitments: newCommitmentsPadded, + newNullifiers: newNullifiersPadded, + } = extractPublicCircuitPublicInputs(partialWitness, acir); + + const newL2ToL1Messages = newL2ToL1Msgs.filter(v => !v.isZero()); + const newCommitments = newCommitmentsPadded.filter(v => !v.isZero()); + const newNullifiers = newNullifiersPadded.filter(v => !v.isZero()); + + const { contractStorageReads, contractStorageUpdateRequests } = context.getStorageActionData(); + log( + `Contract storage reads: ${contractStorageReads + .map(r => r.toFriendlyJSON() + ` - sec: ${r.sideEffectCounter}`) + .join(', ')}`, + ); + + const nestedExecutions = context.getNestedExecutions(); + const unencryptedLogs = context.getUnencryptedLogs(); + + return { + execution, + newCommitments, + newL2ToL1Messages, + newNullifiers, + contractStorageReads, + contractStorageUpdateRequests, + returnValues, + nestedExecutions, + unencryptedLogs, + }; +} /** * Handles execution of public functions. @@ -45,7 +81,6 @@ export class PublicExecutor { private readonly contractsDb: PublicContractsDB, private readonly commitmentsDb: CommitmentsDB, private readonly blockData: HistoricBlockData, - private sideEffectCounter: number = 0, private log = createDebugLogger('aztec:simulator:public-executor'), ) {} @@ -56,216 +91,28 @@ export class PublicExecutor { * @returns The result of the run plus all nested runs. */ public async simulate(execution: PublicExecution, globalVariables: GlobalVariables): Promise { - try { - return await this.execute(execution, globalVariables); - } catch (err) { - throw createSimulationError(err instanceof Error ? err : new Error('Unknown error during public execution')); - } - } - - private async execute(execution: PublicExecution, globalVariables: GlobalVariables): Promise { const selector = execution.functionData.selector; - this.log(`Executing public external function ${execution.contractAddress.toString()}:${selector}`); - const acir = await this.contractsDb.getBytecode(execution.contractAddress, selector); - if (!acir) throw new Error(`Bytecode not found for ${execution.contractAddress.toString()}:${selector}`); + if (!acir) throw new Error(`Bytecode not found for ${execution.contractAddress}:${selector}`); - const initialWitness = getInitialWitness(execution.args, execution.callContext, this.blockData, globalVariables); - const storageActions = new ContractStorageActionsCollector(this.stateDb, execution.contractAddress); - const nestedExecutions: PublicExecutionResult[] = []; - const unencryptedLogs = new FunctionL2Logs([]); // Functions can request to pack arguments before calling other functions. // We use this cache to hold the packed arguments. const packedArgs = await PackedArgsCache.create([]); - const { partialWitness } = await acvm(await AcirSimulator.getSolver(), acir, initialWitness, { - computeSelector: (...args) => { - const signature = oracleDebugCallToFormattedStr(args); - return Promise.resolve(toACVMField(FunctionSelector.fromSignature(signature).toField())); - }, - packArguments: async args => { - return toACVMField(await packedArgs.pack(args.map(fromACVMField))); - }, - debugLog: (...args) => { - this.log(oracleDebugCallToFormattedStr(args)); - return Promise.resolve(ZERO_ACVM_FIELD); - }, - getL1ToL2Message: async ([msgKey]) => { - const messageInputs = await this.commitmentsDb.getL1ToL2Message(fromACVMField(msgKey)); - return toAcvmL1ToL2MessageLoadOracleInputs(messageInputs, this.blockData.l1ToL2MessagesTreeRoot); - }, // l1 to l2 messages in public contexts TODO: https://github.com/AztecProtocol/aztec-packages/issues/616 - checkNoteHashExists: async ([_nonce], [innerNoteHash]) => { - // TODO(https://github.com/AztecProtocol/aztec-packages/issues/1386) - // Once public kernel or base rollup circuit injects nonces, this can be updated to use uniqueSiloedCommitment. - const wasm = await CircuitsWasm.get(); - const siloedNoteHash = siloCommitment(wasm, execution.contractAddress, fromACVMField(innerNoteHash)); - const index = await this.commitmentsDb.getCommitmentIndex(siloedNoteHash); - // return 0 or 1 for whether note hash exists - return index === undefined ? ZERO_ACVM_FIELD : ONE_ACVM_FIELD; - }, - storageRead: async ([slot], [numberOfElements]) => { - const startStorageSlot = fromACVMField(slot); - const values = []; - for (let i = 0; i < Number(numberOfElements); i++) { - const storageSlot = new Fr(startStorageSlot.value + BigInt(i)); - const value = await storageActions.read(storageSlot, this.sideEffectCounter++); // update the sideEffectCounter after assigning its current value to storage action - this.log(`Oracle storage read: slot=${storageSlot.toString()} value=${value.toString()}`); - values.push(value); - } - return values.map(v => toACVMField(v)); - }, - storageWrite: async ([slot], values) => { - const startStorageSlot = fromACVMField(slot); - const newValues = []; - for (let i = 0; i < values.length; i++) { - const storageSlot = new Fr(startStorageSlot.value + BigInt(i)); - const newValue = fromACVMField(values[i]); - await storageActions.write(storageSlot, newValue, this.sideEffectCounter++); // update the sideEffectCounter after assigning its current value to storage action - await this.stateDb.storageWrite(execution.contractAddress, storageSlot, newValue); - this.log(`Oracle storage write: slot=${storageSlot.toString()} value=${newValue.toString()}`); - newValues.push(newValue); - } - return newValues.map(v => toACVMField(v)); - }, - callPublicFunction: async ([address], [functionSelector], [argsHash]) => { - const args = packedArgs.unpack(fromACVMField(argsHash)); - const selector = FunctionSelector.fromField(fromACVMField(functionSelector)); - this.log(`Public function call: addr=${address} selector=${selector} args=${args.join(',')}`); - const childExecutionResult = await this.callPublicFunction( - frToAztecAddress(fromACVMField(address)), - selector, - args, - execution.callContext, - globalVariables, - ); - nestedExecutions.push(childExecutionResult); - this.log(`Returning from nested call: ret=${childExecutionResult.returnValues.join(', ')}`); - return padArrayEnd(childExecutionResult.returnValues, Fr.ZERO, RETURN_VALUES_LENGTH).map(toACVMField); - }, - emitUnencryptedLog: args => { - // https://github.com/AztecProtocol/aztec-packages/issues/885 - const log = Buffer.concat(args.map((charBuffer: any) => convertACVMFieldToBuffer(charBuffer).subarray(-1))); - unencryptedLogs.logs.push(log); - this.log(`Emitted unencrypted log: "${log.toString('ascii')}"`); - return Promise.resolve(ZERO_ACVM_FIELD); - }, - getPortalContractAddress: async ([aztecAddress]) => { - const contractAddress = AztecAddress.fromString(aztecAddress); - const portalContactAddress = - (await this.contractsDb.getPortalContractAddress(contractAddress)) ?? EthAddress.ZERO; - return Promise.resolve(toACVMField(portalContactAddress)); - }, - }).catch((err: Error) => { - throw new ExecutionError( - err.message, - { - contractAddress: execution.contractAddress, - functionSelector: selector, - }, - extractCallStack(err), - { cause: err }, - ); - }); - - const { - returnValues, - newL2ToL1Msgs, - newCommitments: newCommitmentsPadded, - newNullifiers: newNullifiersPadded, - } = extractPublicCircuitPublicInputs(partialWitness, acir); - - const newL2ToL1Messages = newL2ToL1Msgs.filter(v => !v.isZero()); - const newCommitments = newCommitmentsPadded.filter(v => !v.isZero()); - const newNullifiers = newNullifiersPadded.filter(v => !v.isZero()); - - const [contractStorageReads, contractStorageUpdateRequests] = storageActions.collect(); - this.log( - `Contract storage reads: ${contractStorageReads - .map(r => r.toFriendlyJSON() + ` - sec: ${r.sideEffectCounter}`) - .join(', ')}`, - ); - - return { + const context = new PublicExecutionContext( execution, - newCommitments, - newL2ToL1Messages, - newNullifiers, - contractStorageReads, - contractStorageUpdateRequests, - returnValues, - nestedExecutions, - unencryptedLogs, - }; - } + this.blockData, + globalVariables, + packedArgs, + this.stateDb, + this.contractsDb, + this.commitmentsDb, + ); - private async callPublicFunction( - targetContractAddress: AztecAddress, - targetFunctionSelector: FunctionSelector, - targetArgs: Fr[], - callerContext: CallContext, - globalVariables: GlobalVariables, - ) { - const portalAddress = (await this.contractsDb.getPortalContractAddress(targetContractAddress)) ?? EthAddress.ZERO; - const isInternal = await this.contractsDb.getIsInternal(targetContractAddress, targetFunctionSelector); - if (isInternal === undefined) { - throw new Error( - `ERR: ContractsDb don't contain isInternal for ${targetContractAddress.toString()}:${targetFunctionSelector.toString()}. Defaulting to false.`, - ); + try { + return await executePublicFunction(context, acir, this.log); + } catch (err) { + throw createSimulationError(err instanceof Error ? err : new Error('Unknown error during public execution')); } - - const functionData = new FunctionData(targetFunctionSelector, isInternal, false, false); - - const callContext = CallContext.from({ - msgSender: callerContext.storageContractAddress, - portalContractAddress: portalAddress, - storageContractAddress: targetContractAddress, - isContractDeployment: false, - isDelegateCall: false, - isStaticCall: false, - }); - - const nestedExecution: PublicExecution = { - args: targetArgs, - contractAddress: targetContractAddress, - functionData, - callContext, - }; - - return this.execute(nestedExecution, globalVariables); } } - -/** - * Generates the initial witness for a public function. - * @param args - The arguments to the function. - * @param callContext - The call context of the function. - * @param historicBlockData - Historic Trees roots and data required to reconstruct block hash. - * @param globalVariables - The global variables. - * @param witnessStartIndex - The index where to start inserting the parameters. - * @returns The initial witness. - */ -function getInitialWitness( - args: Fr[], - callContext: CallContext, - historicBlockData: HistoricBlockData, - globalVariables: GlobalVariables, - witnessStartIndex = 1, -) { - return toACVMWitness(witnessStartIndex, [ - callContext.msgSender, - callContext.storageContractAddress, - callContext.portalContractAddress, - callContext.isDelegateCall, - callContext.isStaticCall, - callContext.isContractDeployment, - - ...historicBlockData.toArray(), - - globalVariables.chainId, - globalVariables.version, - globalVariables.blockNumber, - globalVariables.timestamp, - - ...args, - ]); -} diff --git a/yarn-project/acir-simulator/src/public/public_execution_context.ts b/yarn-project/acir-simulator/src/public/public_execution_context.ts new file mode 100644 index 00000000000..8c3446bd030 --- /dev/null +++ b/yarn-project/acir-simulator/src/public/public_execution_context.ts @@ -0,0 +1,254 @@ +import { + CallContext, + CircuitsWasm, + FunctionData, + FunctionSelector, + GlobalVariables, + HistoricBlockData, +} from '@aztec/circuits.js'; +import { siloCommitment } from '@aztec/circuits.js/abis'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { Fr } from '@aztec/foundation/fields'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { FunctionL2Logs } from '@aztec/types'; + +import { TypedOracle, toACVMWitness } from '../acvm/index.js'; +import { PackedArgsCache } from '../common/index.js'; +import { CommitmentsDB, PublicContractsDB, PublicStateDB } from './db.js'; +import { PublicExecution, PublicExecutionResult } from './execution.js'; +import { executePublicFunction } from './executor.js'; +import { ContractStorageActionsCollector } from './state_actions.js'; + +/** + * The execution context for a public tx simulation. + */ +export class PublicExecutionContext extends TypedOracle { + private storageActions: ContractStorageActionsCollector; + private nestedExecutions: PublicExecutionResult[] = []; + private unencryptedLogs: Buffer[] = []; + + constructor( + /** + * Data for this execution. + */ + public readonly execution: PublicExecution, + private readonly historicBlockData: HistoricBlockData, + private readonly globalVariables: GlobalVariables, + private readonly packedArgsCache: PackedArgsCache, + private readonly stateDb: PublicStateDB, + private readonly contractsDb: PublicContractsDB, + private readonly commitmentsDb: CommitmentsDB, + private sideEffectCounter: number = 0, + private log = createDebugLogger('aztec:simulator:public-execution'), + ) { + super(); + this.storageActions = new ContractStorageActionsCollector(stateDb, execution.contractAddress); + } + + /** + * Generates the initial witness for a public function. + * @param args - The arguments to the function. + * @param callContext - The call context of the function. + * @param historicBlockData - Historic Trees roots and data required to reconstruct block hash. + * @param globalVariables - The global variables. + * @param witnessStartIndex - The index where to start inserting the parameters. + * @returns The initial witness. + */ + public getInitialWitness(witnessStartIndex = 1) { + const { callContext, args } = this.execution; + const fields = [ + callContext.msgSender, + callContext.storageContractAddress, + callContext.portalContractAddress, + callContext.isDelegateCall, + callContext.isStaticCall, + callContext.isContractDeployment, + + ...this.historicBlockData.toArray(), + + this.globalVariables.chainId, + this.globalVariables.version, + this.globalVariables.blockNumber, + this.globalVariables.timestamp, + + ...args, + ]; + + return toACVMWitness(witnessStartIndex, fields); + } + + /** + * Return the nested execution results during this execution. + */ + public getNestedExecutions() { + return this.nestedExecutions; + } + + /** + * Return the encrypted logs emitted during this execution. + */ + public getUnencryptedLogs() { + return new FunctionL2Logs(this.unencryptedLogs); + } + + /** + * Return the data read and updated during this execution. + */ + public getStorageActionData() { + const [contractStorageReads, contractStorageUpdateRequests] = this.storageActions.collect(); + return { contractStorageReads, contractStorageUpdateRequests }; + } + + /** + * Pack the given arguments. + * @param args - Arguments to pack + */ + public packArguments(args: Fr[]): Promise { + return this.packedArgsCache.pack(args); + } + + /** + * Fetches the a message from the db, given its key. + * @param msgKey - A buffer representing the message key. + * @returns The l1 to l2 message data + */ + public async getL1ToL2Message(msgKey: Fr) { + // l1 to l2 messages in public contexts TODO: https://github.com/AztecProtocol/aztec-packages/issues/616 + const message = await this.commitmentsDb.getL1ToL2Message(msgKey); + return { ...message, root: this.historicBlockData.l1ToL2MessagesTreeRoot }; + } + + /** + * Fetches a path to prove existence of a commitment in the db, given its contract side commitment (before silo). + * @param nonce - The nonce of the note. + * @param innerNoteHash - The inner note hash of the note. + * @returns 1 if (persistent or transient) note hash exists, 0 otherwise. Value is in ACVMField form. + */ + public async checkNoteHashExists(nonce: Fr, innerNoteHash: Fr): Promise { + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/1386) + // Once public kernel or base rollup circuit injects nonces, this can be updated to use uniqueSiloedCommitment. + const wasm = await CircuitsWasm.get(); + const siloedNoteHash = siloCommitment(wasm, this.execution.contractAddress, innerNoteHash); + const index = await this.commitmentsDb.getCommitmentIndex(siloedNoteHash); + return index !== undefined; + } + + /** + * Emit an unencrypted log. + * @param log - The unencrypted log to be emitted. + */ + public emitUnencryptedLog(log: Buffer) { + // https://github.com/AztecProtocol/aztec-packages/issues/885 + this.unencryptedLogs.push(log); + this.log(`Emitted unencrypted log: "${log.toString('ascii')}"`); + } + + /** + * Retrieves the portal contract address associated with the given contract address. + * Returns zero address if the input contract address is not found or invalid. + * @param contractAddress - The address of the contract whose portal address is to be fetched. + * @returns The portal contract address. + */ + public async getPortalContractAddress(contractAddress: AztecAddress) { + return (await this.contractsDb.getPortalContractAddress(contractAddress)) ?? EthAddress.ZERO; + } + + /** + * Read the public storage data. + * @param startStorageSlot - The starting storage slot. + * @param numberOfElements - Number of elements to read from the starting storage slot. + */ + public async storageRead(startStorageSlot: Fr, numberOfElements: number) { + const values = []; + for (let i = 0; i < Number(numberOfElements); i++) { + const storageSlot = new Fr(startStorageSlot.value + BigInt(i)); + const value = await this.storageActions.read(storageSlot, this.sideEffectCounter++); // update the sideEffectCounter after assigning its current value to storage action + this.log(`Oracle storage read: slot=${storageSlot.toString()} value=${value.toString()}`); + values.push(value); + } + return values; + } + + /** + * Write some values to the public storage. + * @param startStorageSlot - The starting storage slot. + * @param values - The values to be written. + */ + public async storageWrite(startStorageSlot: Fr, values: Fr[]) { + const newValues = []; + for (let i = 0; i < values.length; i++) { + const storageSlot = new Fr(startStorageSlot.value + BigInt(i)); + const newValue = values[i]; + await this.storageActions.write(storageSlot, newValue, this.sideEffectCounter++); // update the sideEffectCounter after assigning its current value to storage action + await this.stateDb.storageWrite(this.execution.contractAddress, storageSlot, newValue); + this.log(`Oracle storage write: slot=${storageSlot.toString()} value=${newValue.toString()}`); + newValues.push(newValue); + } + return newValues; + } + + /** + * Calls a public function as a nested execution. + * @param targetContractAddress - The address of the contract to call. + * @param functionSelector - The function selector of the function to call. + * @param argsHash - The packed arguments to pass to the function. + * @returns The return values of the public function. + */ + public async callPublicFunction( + targetContractAddress: AztecAddress, + functionSelector: FunctionSelector, + argsHash: Fr, + ) { + const args = this.packedArgsCache.unpack(argsHash); + this.log(`Public function call: addr=${targetContractAddress} selector=${functionSelector} args=${args.join(',')}`); + + const portalAddress = (await this.contractsDb.getPortalContractAddress(targetContractAddress)) ?? EthAddress.ZERO; + const isInternal = await this.contractsDb.getIsInternal(targetContractAddress, functionSelector); + if (isInternal === undefined) { + throw new Error( + `ERR: ContractsDb don't contain isInternal for ${targetContractAddress.toString()}:${functionSelector.toString()}. Defaulting to false.`, + ); + } + + const acir = await this.contractsDb.getBytecode(targetContractAddress, functionSelector); + if (!acir) throw new Error(`Bytecode not found for ${targetContractAddress}:${functionSelector}`); + + const functionData = new FunctionData(functionSelector, isInternal, false, false); + + const callContext = CallContext.from({ + msgSender: this.execution.contractAddress, + portalContractAddress: portalAddress, + storageContractAddress: targetContractAddress, + isContractDeployment: false, + isDelegateCall: false, + isStaticCall: false, + }); + + const nestedExecution: PublicExecution = { + args, + contractAddress: targetContractAddress, + functionData, + callContext, + }; + + const context = new PublicExecutionContext( + nestedExecution, + this.historicBlockData, + this.globalVariables, + this.packedArgsCache, + this.stateDb, + this.contractsDb, + this.commitmentsDb, + this.sideEffectCounter, + this.log, + ); + + const childExecutionResult = await executePublicFunction(context, acir); + + this.nestedExecutions.push(childExecutionResult); + this.log(`Returning from nested call: ret=${childExecutionResult.returnValues.join(', ')}`); + + return childExecutionResult.returnValues; + } +} diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts index 72f42355be8..36d98202b92 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts @@ -309,9 +309,10 @@ export class AztecRPCServer implements AztecRPC { return txHash; } - public async viewTx(functionName: string, args: any[], to: AztecAddress, from?: AztecAddress) { + public async viewTx(functionName: string, args: any[], to: AztecAddress, _from?: AztecAddress) { + // TODO - Should check if `from` has the permission to call the view function. const functionCall = await this.#getFunctionCall(functionName, args, to); - const executionResult = await this.#simulateUnconstrained(functionCall, from); + const executionResult = await this.#simulateUnconstrained(functionCall); // TODO - Return typed result based on the function abi. return executionResult; @@ -451,22 +452,14 @@ export class AztecRPCServer implements AztecRPC { * Returns the simulation result containing the outputs of the unconstrained function. * * @param execRequest - The transaction request object containing the target contract and function data. - * @param from - The origin of the request. * @returns The simulation result containing the outputs of the unconstrained function. */ - async #simulateUnconstrained(execRequest: FunctionCall, from?: AztecAddress) { - const { contractAddress, functionAbi, portalContract } = await this.#getSimulationParameters(execRequest); + async #simulateUnconstrained(execRequest: FunctionCall) { + const { contractAddress, functionAbi } = await this.#getSimulationParameters(execRequest); this.log('Executing unconstrained simulator...'); try { - const result = await this.simulator.runUnconstrained( - execRequest, - from ?? AztecAddress.ZERO, - functionAbi, - contractAddress, - portalContract, - this.node, - ); + const result = await this.simulator.runUnconstrained(execRequest, functionAbi, contractAddress, this.node); this.log('Unconstrained simulation completed!'); return result;