diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts index 8bbe59e00fd0..bc897a246849 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts @@ -9,6 +9,7 @@ import { createPXEClient, getContractClassFromArtifact, makeFetch, + sleep, } from '@aztec/aztec.js'; import { CounterContract } from '@aztec/noir-contracts.js/Counter'; import { DocsExampleContract } from '@aztec/noir-contracts.js/DocsExample'; @@ -140,9 +141,34 @@ describe('e2e_deploy_contract deploy method', () => { await new BatchCall(wallet, [...deploy.calls, init]).send().wait(); }, 300_000); - it.skip('publicly deploys and calls a public function in a tx in the same block', async () => { - // TODO(@spalladino): Requires being able to read a nullifier on the same block it was emitted. - }); + it.skip('publicly deploys a contract in one tx and calls a public function on it later in the same block', async () => { + await t.aztecNode.setConfig({ minTxsPerBlock: 2 }); + + const owner = wallet.getAddress(); + logger.debug('Initializing deploy method'); + const deployMethod = StatefulTestContract.deploy(wallet, owner, owner, 42); + logger.debug('Creating request/calls to register and deploy contract'); + const deploy = await deployMethod.request(); + const deployTx = new BatchCall(wallet, deploy.calls); + logger.debug('Getting an instance of the not-yet-deployed contract to batch calls to'); + const contract = await StatefulTestContract.at((await deployMethod.getInstance()).address, wallet); + + logger.debug('Creating public call to run in same block as deployment'); + const publicCall = contract.methods.increment_public_value(owner, 84); + + // First send the deploy transaction + const deployTxPromise = deployTx.send({ skipPublicSimulation: true }).wait({ timeout: 600 }); + + // Wait a bit to ensure the deployment transaction gets processed first + await sleep(5000); + + // Then send the public call transaction + const publicCallTxPromise = publicCall.send({ skipPublicSimulation: true }).wait({ timeout: 600 }); + + logger.debug('Deploying a contract and calling a public function in the same block'); + const [deployTxReceipt, publicCallTxReceipt] = await Promise.all([deployTxPromise, publicCallTxPromise]); + expect(deployTxReceipt.blockNumber).toEqual(publicCallTxReceipt.blockNumber); + }, 300_000); describe('regressions', () => { it('fails properly when trying to deploy a contract with a failing constructor with a pxe client with retries', async () => { diff --git a/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts b/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts index 2925104fa0dd..ce997c819004 100644 --- a/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts +++ b/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts @@ -46,7 +46,7 @@ export class PhasesTxValidator implements TxValidator { return { result: 'valid' }; } finally { - await this.contractDataSource.removeNewContracts(tx); + this.contractDataSource.clearContractsForTx(); } } diff --git a/yarn-project/simulator/src/public/avm/fixtures/base_avm_simulation_tester.ts b/yarn-project/simulator/src/public/avm/fixtures/base_avm_simulation_tester.ts index 544fdd84a3e1..25c7f0c6680f 100644 --- a/yarn-project/simulator/src/public/avm/fixtures/base_avm_simulation_tester.ts +++ b/yarn-project/simulator/src/public/avm/fixtures/base_avm_simulation_tester.ts @@ -2,7 +2,7 @@ import { DEPLOYER_CONTRACT_ADDRESS } from '@aztec/constants'; import { Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; -import { computeFeePayerBalanceStorageSlot } from '@aztec/protocol-contracts/fee-juice'; +import { computeFeePayerBalanceStorageSlot, getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; import type { ContractArtifact } from '@aztec/stdlib/abi'; import { PublicDataWrite } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -11,6 +11,7 @@ import { computePublicDataTreeLeafSlot, siloNullifier } from '@aztec/stdlib/hash import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server'; import { MerkleTreeId } from '@aztec/stdlib/trees'; +import { createContractClassAndInstance } from './index.js'; import type { SimpleContractDataSource } from './simple_contract_data_source.js'; /** @@ -58,13 +59,16 @@ export abstract class BaseAvmSimulationTester { seed = 0, originalContractClassId?: Fr, // if previously upgraded ): Promise { - const contractInstance = await this.contractDataSource.registerAndDeployContract( + const { contractClass, contractInstance } = await createContractClassAndInstance( constructorArgs, deployer, contractArtifact, seed, originalContractClassId, ); + + await this.contractDataSource.addNewContract(contractArtifact, contractClass, contractInstance); + if (!skipNullifierInsertion) { await this.insertContractAddressNullifier(contractInstance.address); } @@ -72,11 +76,14 @@ export abstract class BaseAvmSimulationTester { } async registerFeeJuiceContract(): Promise { - return await this.contractDataSource.registerFeeJuiceContract(); - } - - getFirstContractInstance(): ContractInstanceWithAddress { - return this.contractDataSource.getFirstContractInstance(); + const feeJuice = await getCanonicalFeeJuice(); + const feeJuiceContractClassPublic = { + ...feeJuice.contractClass, + privateFunctions: [], + unconstrainedFunctions: [], + }; + await this.contractDataSource.addNewContract(feeJuice.artifact, feeJuiceContractClassPublic, feeJuice.instance); + return feeJuice.instance; } addContractClass(contractClass: ContractClassPublic, contractArtifact: ContractArtifact): Promise { diff --git a/yarn-project/simulator/src/public/avm/fixtures/index.ts b/yarn-project/simulator/src/public/avm/fixtures/index.ts index 1426ab76e64c..d957267f59e6 100644 --- a/yarn-project/simulator/src/public/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/public/avm/fixtures/index.ts @@ -1,13 +1,24 @@ -import { MAX_L2_GAS_PER_TX_PUBLIC_PORTION } from '@aztec/constants'; +import { + DEPLOYER_CONTRACT_ADDRESS, + MAX_L2_GAS_PER_TX_PUBLIC_PORTION, + PUBLIC_DISPATCH_SELECTOR, +} from '@aztec/constants'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { AvmGadgetsTestContractArtifact } from '@aztec/noir-contracts.js/AvmGadgetsTest'; import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; import { type ContractArtifact, type FunctionArtifact, FunctionSelector } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { + type ContractClassPublic, + type ContractInstanceWithAddress, + computeInitializationHash, +} from '@aztec/stdlib/contract'; import { isNoirCallStackUnresolved } from '@aztec/stdlib/errors'; import { GasFees } from '@aztec/stdlib/gas'; +import { siloNullifier } from '@aztec/stdlib/hash'; import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server'; +import { makeContractClassPublic, makeContractInstanceFromClassId } from '@aztec/stdlib/testing'; import { GlobalVariables } from '@aztec/stdlib/tx'; import { strict as assert } from 'assert'; @@ -234,3 +245,52 @@ export function resolveAvmGadgetsTestContractAssertionMessage( return resolveAssertionMessageFromRevertData(output, functionArtifact); } + +/** + * Create a contract class and instance given constructor args, artifact, etc. + * NOTE: This is useful for testing real-ish contract class registration and instance deployment TXs (via logs) + * @param constructorArgs - The constructor arguments for the contract. + * @param deployer - The deployer of the contract. + * @param contractArtifact - The contract artifact for the contract. + * @param seed - The seed for the contract. + * @param originalContractClassId - The original contract class ID (if upgraded) + * @returns The contract class, instance, and contract address nullifier. + */ +export async function createContractClassAndInstance( + constructorArgs: any[], + deployer: AztecAddress, + contractArtifact: ContractArtifact, + seed = 0, + originalContractClassId?: Fr, // if previously upgraded +): Promise<{ + contractClass: ContractClassPublic; + contractInstance: ContractInstanceWithAddress; + contractAddressNullifier: Fr; +}> { + const bytecode = getContractFunctionArtifact(PUBLIC_DISPATCH_FN_NAME, contractArtifact)!.bytecode; + const contractClass = await makeContractClassPublic( + seed, + /*publicDispatchFunction=*/ { bytecode, selector: new FunctionSelector(PUBLIC_DISPATCH_SELECTOR) }, + ); + + const constructorAbi = getContractFunctionArtifact('constructor', contractArtifact); + const initializationHash = await computeInitializationHash(constructorAbi, constructorArgs); + const contractInstance = + originalContractClassId === undefined + ? await makeContractInstanceFromClassId(contractClass.id, seed, { + deployer, + initializationHash, + }) + : await makeContractInstanceFromClassId(originalContractClassId, seed, { + deployer, + initializationHash, + currentClassId: contractClass.id, + }); + + const contractAddressNullifier = await siloNullifier( + AztecAddress.fromNumber(DEPLOYER_CONTRACT_ADDRESS), + contractInstance.address.toField(), + ); + + return { contractClass, contractInstance, contractAddressNullifier }; +} diff --git a/yarn-project/simulator/src/public/avm/fixtures/simple_contract_data_source.ts b/yarn-project/simulator/src/public/avm/fixtures/simple_contract_data_source.ts index 25595e9bf0df..3fc0adc6fc6d 100644 --- a/yarn-project/simulator/src/public/avm/fixtures/simple_contract_data_source.ts +++ b/yarn-project/simulator/src/public/avm/fixtures/simple_contract_data_source.ts @@ -1,20 +1,15 @@ -import { PUBLIC_DISPATCH_SELECTOR } from '@aztec/constants'; import type { Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; -import { getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; import { type ContractArtifact, FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { - type ContractClassPublic, - type ContractDataSource, - type ContractInstanceWithAddress, - type PublicFunction, - computeInitializationHash, - computePublicBytecodeCommitment, +import type { + ContractClassPublic, + ContractDataSource, + ContractInstanceWithAddress, + PublicFunction, } from '@aztec/stdlib/contract'; -import { makeContractClassPublic, makeContractInstanceFromClassId } from '@aztec/stdlib/testing'; -import { PUBLIC_DISPATCH_FN_NAME, getContractFunctionArtifact } from './index.js'; +import { PUBLIC_DISPATCH_FN_NAME } from './index.js'; /** * This class is used during public/avm testing to function as a database of @@ -39,56 +34,14 @@ export class SimpleContractDataSource implements ContractDataSource { * Derive the contract class and instance with some seed. * Add both to the contract data source along with the contract artifact. */ - async registerAndDeployContract( - constructorArgs: any[], - deployer: AztecAddress, + async addNewContract( contractArtifact: ContractArtifact, - seed = 0, - originalContractClassId?: Fr, // if previously upgraded - ): Promise { - const bytecode = getContractFunctionArtifact(PUBLIC_DISPATCH_FN_NAME, contractArtifact)!.bytecode; - const contractClass = await makeContractClassPublic( - seed, - /*publicDispatchFunction=*/ { bytecode, selector: new FunctionSelector(PUBLIC_DISPATCH_SELECTOR) }, - ); - - const constructorAbi = getContractFunctionArtifact('constructor', contractArtifact); - const initializationHash = await computeInitializationHash(constructorAbi, constructorArgs); - this.logger.trace(`Initialization hash for contract class ${contractClass.id}: ${initializationHash.toString()}`); - const contractInstance = - originalContractClassId === undefined - ? await makeContractInstanceFromClassId(contractClass.id, seed, { - deployer, - initializationHash, - }) - : await makeContractInstanceFromClassId(originalContractClassId, seed, { - deployer, - initializationHash, - currentClassId: contractClass.id, - }); - + contractClass: ContractClassPublic, + contractInstance: ContractInstanceWithAddress, + ) { this.addContractArtifact(contractClass.id, contractArtifact); await this.addContractClass(contractClass); await this.addContractInstance(contractInstance); - return contractInstance; - } - - async registerFeeJuiceContract(): Promise { - const feeJuice = await getCanonicalFeeJuice(); - const feeJuiceContractClassPublic = { - ...feeJuice.contractClass, - privateFunctions: [], - unconstrainedFunctions: [], - }; - - this.addContractArtifact(feeJuiceContractClassPublic.id, feeJuice.artifact); - await this.addContractClass(feeJuiceContractClassPublic); - await this.addContractInstance(feeJuice.instance); - return feeJuice.instance; - } - - getFirstContractInstance(): ContractInstanceWithAddress { - return this.contractInstances.values().next().value; } addContractArtifact(classId: Fr, artifact: ContractArtifact): void { @@ -96,7 +49,7 @@ export class SimpleContractDataSource implements ContractDataSource { } ///////////////////////////////////////////////////////////// - // ContractDataSource function impelementations + // ContractDataSource function implementations getPublicFunction(_address: AztecAddress, _selector: FunctionSelector): Promise { throw new Error('Method not implemented.'); } @@ -109,9 +62,8 @@ export class SimpleContractDataSource implements ContractDataSource { return Promise.resolve(this.contractClasses.get(id.toString())); } - async getBytecodeCommitment(id: Fr): Promise { - const contractClass = await this.getContractClass(id); - return Promise.resolve(computePublicBytecodeCommitment(contractClass!.packedBytecode)); + getBytecodeCommitment(_id: Fr): Promise { + return Promise.resolve(undefined); } getContract(address: AztecAddress): Promise { diff --git a/yarn-project/simulator/src/public/avm/journal/journal.ts b/yarn-project/simulator/src/public/avm/journal/journal.ts index 74742b2d0e59..a15b651a6b48 100644 --- a/yarn-project/simulator/src/public/avm/journal/journal.ts +++ b/yarn-project/simulator/src/public/avm/journal/journal.ts @@ -193,17 +193,15 @@ export class AvmPersistableStateManager { `Value mismatch when performing public data write (got value: ${value}, value in tree: ${newLeafPreimage.value})`, ); } else { - this.log.debug(`insertion witness data length: ${result.insertionWitnessData.length}`); // The new leaf preimage should have the new value and slot newLeafPreimage.slot = leafSlot; newLeafPreimage.value = value; // TODO: is this necessary?! Why doesn't sequentialInsert return the newLeafPreimage via // result.insertionWitnessData[0].leafPreimage? - this.log.debug( - `newLeafPreimage.slot: ${newLeafPreimage.slot}, newLeafPreimage.value: ${newLeafPreimage.value}`, + this.log.trace( + `newLeafPreimage.slot: ${newLeafPreimage.slot}, newLeafPreimage.value: ${newLeafPreimage.value}, insertionIndex: ${result.insertionWitnessData[0].index}`, ); - this.log.debug(`insertion index: ${result.insertionWitnessData[0].index}`); insertionPath = result.insertionWitnessData[0].siblingPath.toFields(); } diff --git a/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts index 652f689a8ec6..2785a4536d8b 100644 --- a/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts +++ b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts @@ -1,6 +1,5 @@ import { PUBLIC_DISPATCH_SELECTOR } from '@aztec/constants'; import { Fr } from '@aztec/foundation/fields'; -import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; import { type ContractArtifact, FunctionSelector, encodeArguments } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { GasFees } from '@aztec/stdlib/gas'; @@ -13,7 +12,7 @@ import { BaseAvmSimulationTester } from '../avm/fixtures/base_avm_simulation_tes import { getContractFunctionArtifact, getFunctionSelector } from '../avm/fixtures/index.js'; import { SimpleContractDataSource } from '../avm/fixtures/simple_contract_data_source.js'; import { WorldStateDB } from '../public_db_sources.js'; -import { type PublicTxResult, PublicTxSimulator } from '../public_tx_simulator.js'; +import { type PublicTxResult, PublicTxSimulator } from '../public_tx_simulator/public_tx_simulator.js'; import { createTxForPublicCalls } from './index.js'; const TIMESTAMP = new Fr(99833); @@ -25,6 +24,7 @@ export type TestEnqueuedCall = { fnName: string; args: any[]; isStaticCall?: boolean; + contractArtifact?: ContractArtifact; }; /** @@ -62,28 +62,36 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { const setupExecutionRequests: PublicExecutionRequest[] = []; for (let i = 0; i < setupCalls.length; i++) { const address = setupCalls[i].address; - const contractArtifact = await this.contractDataSource.getContractArtifact(address); + const contractArtifact = + setupCalls[i].contractArtifact || (await this.contractDataSource.getContractArtifact(address)); + if (!contractArtifact) { + throw new Error(`Contract artifact not found for address: ${address}`); + } const req = await executionRequestForCall( + contractArtifact, sender, address, setupCalls[i].fnName, setupCalls[i].args, setupCalls[i].isStaticCall, - contractArtifact, ); setupExecutionRequests.push(req); } const appExecutionRequests: PublicExecutionRequest[] = []; for (let i = 0; i < appCalls.length; i++) { const address = appCalls[i].address; - const contractArtifact = await this.contractDataSource.getContractArtifact(address); + const contractArtifact = + appCalls[i].contractArtifact || (await this.contractDataSource.getContractArtifact(address)); + if (!contractArtifact) { + throw new Error(`Contract artifact not found for address: ${address}`); + } const req = await executionRequestForCall( + contractArtifact, sender, address, appCalls[i].fnName, appCalls[i].args, appCalls[i].isStaticCall, - contractArtifact, ); appExecutionRequests.push(req); } @@ -91,14 +99,18 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { let teardownExecutionRequest: PublicExecutionRequest | undefined = undefined; if (teardownCall) { const address = teardownCall.address; - const contractArtifact = await this.contractDataSource.getContractArtifact(address); + const contractArtifact = + teardownCall.contractArtifact || (await this.contractDataSource.getContractArtifact(address)); + if (!contractArtifact) { + throw new Error(`Contract artifact not found for address: ${address}`); + } teardownExecutionRequest = await executionRequestForCall( + contractArtifact, sender, address, teardownCall.fnName, teardownCall.args, teardownCall.isStaticCall, - contractArtifact, ); } @@ -137,12 +149,12 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { } async function executionRequestForCall( + contractArtifact: ContractArtifact, sender: AztecAddress, address: AztecAddress, fnName: string, args: Fr[] = [], isStaticCall: boolean = false, - contractArtifact: ContractArtifact = AvmTestContractArtifact, ): Promise { const fnSelector = await getFunctionSelector(fnName, contractArtifact); const fnAbi = getContractFunctionArtifact(fnName, contractArtifact); diff --git a/yarn-project/simulator/src/public/fixtures/utils.ts b/yarn-project/simulator/src/public/fixtures/utils.ts index 647853e463af..7e4a28e04985 100644 --- a/yarn-project/simulator/src/public/fixtures/utils.ts +++ b/yarn-project/simulator/src/public/fixtures/utils.ts @@ -1,12 +1,29 @@ -import { DEFAULT_GAS_LIMIT, MAX_L2_GAS_PER_TX_PUBLIC_PORTION } from '@aztec/constants'; +import { + CONTRACT_CLASS_LOG_DATA_SIZE_IN_FIELDS, + DEFAULT_GAS_LIMIT, + DEPLOYER_CONTRACT_ADDRESS, + MAX_L2_GAS_PER_TX_PUBLIC_PORTION, + PRIVATE_LOG_SIZE_IN_FIELDS, + REGISTERER_CONTRACT_ADDRESS, + REGISTERER_CONTRACT_CLASS_REGISTERED_MAGIC_VALUE, +} from '@aztec/constants'; import { Fr } from '@aztec/foundation/fields'; +import { assertLength } from '@aztec/foundation/serialize'; +import { DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_TAG } from '@aztec/protocol-contracts'; +import { bufferAsFields } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; +import { siloNullifier } from '@aztec/stdlib/hash'; import { PartialPrivateTailPublicInputsForPublic, + PartialPrivateTailPublicInputsForRollup, PrivateKernelTailCircuitPublicInputs, RollupValidationRequests, + ScopedLogHash, + countAccumulatedItems, } from '@aztec/stdlib/kernel'; +import { ContractClassLog, PrivateLog } from '@aztec/stdlib/logs'; import { type PublicExecutionRequest, Tx } from '@aztec/stdlib/tx'; import { BlockHeader, TxConstantData, TxContext } from '@aztec/stdlib/tx'; @@ -73,3 +90,110 @@ export async function createTxForPublicCalls( return tx; } + +export function createTxForPrivateOnly(feePayer = AztecAddress.zero(), gasUsedByPrivate: Gas = new Gas(10, 10)): Tx { + // use max limits + const gasLimits = new Gas(DEFAULT_GAS_LIMIT, MAX_L2_GAS_PER_TX_PUBLIC_PORTION); + + const forRollup = PartialPrivateTailPublicInputsForRollup.empty(); + + const maxFeesPerGas = feePayer.isZero() ? GasFees.empty() : new GasFees(10, 10); + const gasSettings = new GasSettings(gasLimits, Gas.empty(), maxFeesPerGas, GasFees.empty()); + const txContext = new TxContext(Fr.zero(), Fr.zero(), gasSettings); + const constantData = new TxConstantData(BlockHeader.empty(), txContext, Fr.zero(), Fr.zero()); + + const txData = new PrivateKernelTailCircuitPublicInputs( + constantData, + RollupValidationRequests.empty(), + /*gasUsed=*/ gasUsedByPrivate, + feePayer, + /*forPublic=*/ undefined, + forRollup, + ); + return Tx.newWithTxData(txData); +} + +export async function addNewContractClassToTx( + tx: Tx, + contractClass: ContractClassPublic, + skipNullifierInsertion = false, +) { + const contractClassLogFields = [ + new Fr(REGISTERER_CONTRACT_CLASS_REGISTERED_MAGIC_VALUE), + contractClass.id, + new Fr(contractClass.version), + new Fr(contractClass.artifactHash), + new Fr(contractClass.privateFunctionsRoot), + ...bufferAsFields(contractClass.packedBytecode, Math.ceil(contractClass.packedBytecode.length / 31) + 1), + ]; + const contractClassLog = ContractClassLog.fromFields([ + new Fr(REGISTERER_CONTRACT_ADDRESS), + ...contractClassLogFields.concat( + new Array(CONTRACT_CLASS_LOG_DATA_SIZE_IN_FIELDS - contractClassLogFields.length).fill(Fr.ZERO), + ), + ]); + const contractClassLogHash = ScopedLogHash.fromFields([ + await contractClassLog.hash(), + new Fr(7), + new Fr(contractClassLog.getEmittedLength()), + new Fr(REGISTERER_CONTRACT_ADDRESS), + ]); + + const accumulatedData = tx.data.forPublic ? tx.data.forPublic!.revertibleAccumulatedData : tx.data.forRollup!.end; + if (!skipNullifierInsertion) { + const nextNullifierIndex = countAccumulatedItems(accumulatedData.nullifiers); + accumulatedData.nullifiers[nextNullifierIndex] = contractClass.id; + } + + const nextLogIndex = countAccumulatedItems(accumulatedData.contractClassLogsHashes); + accumulatedData.contractClassLogsHashes[nextLogIndex] = contractClassLogHash; + + tx.contractClassLogs.push(contractClassLog); +} + +export async function addNewContractInstanceToTx( + tx: Tx, + contractInstance: ContractInstanceWithAddress, + skipNullifierInsertion = false, +) { + // can't use publicKeys.toFields() because it includes isInfinite which + // is not broadcast in such private logs + const publicKeysAsFields = [ + contractInstance.publicKeys.masterNullifierPublicKey.x, + contractInstance.publicKeys.masterNullifierPublicKey.y, + contractInstance.publicKeys.masterIncomingViewingPublicKey.x, + contractInstance.publicKeys.masterIncomingViewingPublicKey.y, + contractInstance.publicKeys.masterOutgoingViewingPublicKey.x, + contractInstance.publicKeys.masterOutgoingViewingPublicKey.y, + contractInstance.publicKeys.masterTaggingPublicKey.x, + contractInstance.publicKeys.masterTaggingPublicKey.y, + ]; + const fields = [ + DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_TAG, + contractInstance.address.toField(), + new Fr(contractInstance.version), + new Fr(contractInstance.salt), + contractInstance.currentContractClassId, + contractInstance.initializationHash, + ...publicKeysAsFields, + contractInstance.deployer.toField(), + new Fr(0), + new Fr(0), + new Fr(0), + ]; + const contractInstanceLog = new PrivateLog(assertLength(fields, PRIVATE_LOG_SIZE_IN_FIELDS)); + + const contractAddressNullifier = await siloNullifier( + AztecAddress.fromNumber(DEPLOYER_CONTRACT_ADDRESS), + contractInstance.address.toField(), + ); + + const accumulatedData = tx.data.forPublic ? tx.data.forPublic!.revertibleAccumulatedData : tx.data.forRollup!.end; + if (!skipNullifierInsertion) { + const nextNullifierIndex = countAccumulatedItems(accumulatedData.nullifiers); + accumulatedData.nullifiers[nextNullifierIndex] = contractAddressNullifier; + } + + const nextLogIndex = countAccumulatedItems(accumulatedData.privateLogs); + accumulatedData.privateLogs[nextLogIndex] = contractInstanceLog; +} diff --git a/yarn-project/simulator/src/public/index.ts b/yarn-project/simulator/src/public/index.ts index 2fab51c1dc86..bf389ec528a6 100644 --- a/yarn-project/simulator/src/public/index.ts +++ b/yarn-project/simulator/src/public/index.ts @@ -1,8 +1,8 @@ export * from '../common/db_interfaces.js'; -export * from './public_tx_simulator.js'; +export * from './public_tx_simulator/public_tx_simulator.js'; export { type EnqueuedPublicCallExecutionResult, type PublicFunctionCallResult } from './execution.js'; export * from './public_db_sources.js'; -export { PublicProcessor, PublicProcessorFactory } from './public_processor.js'; +export { PublicProcessor, PublicProcessorFactory } from './public_processor/public_processor.js'; export { SideEffectTrace } from './side_effect_trace.js'; export { getExecutionRequestsByPhase } from './utils.js'; export { PublicTxSimulationTester } from './fixtures/index.js'; diff --git a/yarn-project/simulator/src/public/public_db_sources.ts b/yarn-project/simulator/src/public/public_db_sources.ts index 0730ad9b3a8a..0bfc2dec034d 100644 --- a/yarn-project/simulator/src/public/public_db_sources.ts +++ b/yarn-project/simulator/src/public/public_db_sources.ts @@ -18,89 +18,216 @@ import type { MerkleTreeReadOperations, MerkleTreeWriteOperations, } from '@aztec/stdlib/interfaces/server'; +import { ContractClassLog, PrivateLog } from '@aztec/stdlib/logs'; import type { PublicDBAccessStats } from '@aztec/stdlib/stats'; import { MerkleTreeId, type PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees'; import type { Tx } from '@aztec/stdlib/tx'; import type { PublicContractsDB, PublicStateDB } from '../common/db_interfaces.js'; +import { TxContractCache } from './tx_contract_cache.js'; /** * Implements the PublicContractsDB using a ContractDataSource. * Progressively records contracts in transaction as they are processed in a block. + * Separates block-level contract information (from processed/included txs) from the + * current tx's contract information (which may be cleared on tx revert/death). */ export class ContractsDataSourcePublicDB implements PublicContractsDB { - private instanceCache = new Map(); - private classCache = new Map(); + // Two caching layers for contract classes and instances. + // Tx-level cache: + // - The current tx's new contract information is cached + // in currentTxNonRevertibleCache and currentTxRevertibleCache. + // Block-level cache: + // - Contract information from earlier in the block, usable by later txs. + // When a tx succeeds, that tx's caches are merged into the block cache and cleared. + private currentTxNonRevertibleCache = new TxContractCache(); + private currentTxRevertibleCache = new TxContractCache(); + private blockCache = new TxContractCache(); + // Separate flat cache for bytecode commitments. private bytecodeCommitmentCache = new Map(); private log = createLogger('simulator:contracts-data-source'); constructor(private dataSource: ContractDataSource) {} + /** * Add new contracts from a transaction * @param tx - The transaction to add contracts from. */ public async addNewContracts(tx: Tx): Promise { - // Extract contract class and instance data from logs and add to cache for this block - const siloedLogs = await tx.filterContractClassLogs(tx.data.getNonEmptyContractClassLogsHashes(), true); - const contractClassRegisteredEvents = siloedLogs - .filter(log => ContractClassRegisteredEvent.isContractClassRegisteredEvent(log)) - .map(log => ContractClassRegisteredEvent.fromLog(log)); + await this.addNonRevertibleContractClasses(tx); + await this.addRevertibleContractClasses(tx); + this.addNonRevertibleContractInstances(tx); + this.addRevertibleContractInstances(tx); + } + + /** + * Add non revertible contracts from a transaction + * @param tx - The transaction to add non revertible contracts from. + */ + public async addNewNonRevertibleContracts(tx: Tx) { + await this.addNonRevertibleContractClasses(tx); + this.addNonRevertibleContractInstances(tx); + } + + /** + * Add revertible contracts from a transaction + * @param tx - The transaction to add revertible contracts from. + */ + public async addNewRevertibleContracts(tx: Tx) { + await this.addRevertibleContractClasses(tx); + this.addRevertibleContractInstances(tx); + } + + /** + * Add non-revertible contract classes from a transaction + * For private-only txs, this will be all contract classes (found in tx.data.forPublic) + * @param tx - The transaction to add non-revertible contract classes from. + */ + private async addNonRevertibleContractClasses(tx: Tx) { + const siloedContractClassLogs = tx.data.forPublic + ? await tx.filterContractClassLogs( + tx.data.forPublic!.nonRevertibleAccumulatedData.contractClassLogsHashes, + /*siloed=*/ true, + ) + : await tx.filterContractClassLogs(tx.data.forRollup!.end.contractClassLogsHashes, /*siloed=*/ true); + + await this.addContractClassesFromLogs(siloedContractClassLogs, this.currentTxNonRevertibleCache, 'non-revertible'); + } + + /** + * Add revertible contract classes from a transaction + * None for private-only txs. + * @param tx - The transaction to add revertible contract classes from. + */ + private async addRevertibleContractClasses(tx: Tx) { + const siloedContractClassLogs = tx.data.forPublic + ? await tx.filterContractClassLogs( + tx.data.forPublic!.revertibleAccumulatedData.contractClassLogsHashes, + /*siloed=*/ true, + ) + : []; + + await this.addContractClassesFromLogs(siloedContractClassLogs, this.currentTxRevertibleCache, 'revertible'); + } + + /** + * Add non-revertible contract instances from a transaction + * For private-only txs, this will be all contract instances (found in tx.data.forRollup) + * @param tx - The transaction to add non-revertible contract instances from. + */ + private addNonRevertibleContractInstances(tx: Tx) { + const contractInstanceLogs = tx.data.forPublic + ? tx.data.forPublic!.nonRevertibleAccumulatedData.privateLogs.filter(l => !l.isEmpty()) + : tx.data.forRollup!.end.privateLogs.filter(l => !l.isEmpty()); + + this.addContractInstancesFromLogs(contractInstanceLogs, this.currentTxNonRevertibleCache, 'non-revertible'); + } + + /** + * Add revertible contract instances from a transaction + * None for private-only txs. + * @param tx - The transaction to add revertible contract instances from. + */ + private addRevertibleContractInstances(tx: Tx) { + const contractInstanceLogs = tx.data.forPublic + ? tx.data.forPublic!.revertibleAccumulatedData.privateLogs.filter(l => !l.isEmpty()) + : []; + + this.addContractInstancesFromLogs(contractInstanceLogs, this.currentTxRevertibleCache, 'revertible'); + } + + /** + * Given a tx's siloed contract class logs, add the contract classes to the cache + * @param siloedContractClassLogs - Contract class logs to process + * @param cache - The cache to store the contract classes in + * @param cacheType - Type of cache (for logging) + */ + private async addContractClassesFromLogs( + siloedContractClassLogs: ContractClassLog[], + cache: TxContractCache, + cacheType: string, + ) { + const contractClassEvents = siloedContractClassLogs + .filter((log: ContractClassLog) => ContractClassRegisteredEvent.isContractClassRegisteredEvent(log)) + .map((log: ContractClassLog) => ContractClassRegisteredEvent.fromLog(log)); + + // Cache contract classes await Promise.all( - contractClassRegisteredEvents.map(async event => { - this.log.debug(`Adding class ${event.contractClassId.toString()} to public execution contract cache`); - this.classCache.set(event.contractClassId.toString(), await event.toContractClassPublic()); + contractClassEvents.map(async (event: ContractClassRegisteredEvent) => { + this.log.debug(`Adding class ${event.contractClassId.toString()} to contract's ${cacheType} tx cache`); + const contractClass = await event.toContractClassPublic(); + + cache.addClass(event.contractClassId, contractClass); }), ); + } - // We store the contract instance deployed event log in private logs, contract_instance_deployer_contract/src/main.nr - const contractInstanceEvents = tx.data - .getNonEmptyPrivateLogs() + /** + * Given a tx's contract instance logs, add the contract instances to the cache + * @param contractInstanceLogs - Contract instance logs to process + * @param cache - The cache to store the contract instances in + * @param cacheType - Type of cache (for logging) + */ + private addContractInstancesFromLogs(contractInstanceLogs: PrivateLog[], cache: TxContractCache, cacheType: string) { + const contractInstanceEvents = contractInstanceLogs .filter(log => ContractInstanceDeployedEvent.isContractInstanceDeployedEvent(log)) - .map(ContractInstanceDeployedEvent.fromLog); + .map(log => ContractInstanceDeployedEvent.fromLog(log)); + + // Cache contract instances contractInstanceEvents.forEach(e => { this.log.debug( - `Adding instance ${e.address.toString()} with class ${e.contractClassId.toString()} to public execution contract cache`, + `Adding instance ${e.address.toString()} with class ${e.contractClassId.toString()} to ${cacheType} tx contract cache`, ); - this.instanceCache.set(e.address.toString(), e.toContractInstance()); + cache.addInstance(e.address, e.toContractInstance()); }); } /** - * Removes new contracts added from transactions - * @param tx - The tx's contracts to be removed - * @param onlyRevertible - Whether to only remove contracts added from revertible contract class logs + * Clear new contracts from the current tx's cache */ - public async removeNewContracts(tx: Tx, onlyRevertible: boolean = false): Promise { - // TODO(@spalladino): Can this inadvertently delete a valid contract added by another tx? - // Let's say we have two txs adding the same contract on the same block. If the 2nd one reverts, - // wouldn't that accidentally remove the contract added on the first one? - const contractClassLogs = onlyRevertible ? await tx.getSplitContractClassLogs(true, true) : tx.contractClassLogs; - contractClassLogs - .filter(log => ContractClassRegisteredEvent.isContractClassRegisteredEvent(log)) - .forEach(log => { - const event = ContractClassRegisteredEvent.fromLog(log); - this.classCache.delete(event.contractClassId.toString()); - }); + public clearContractsForTx() { + this.currentTxRevertibleCache.clear(); + this.currentTxRevertibleCache.clear(); + this.currentTxNonRevertibleCache.clear(); + } - // We store the contract instance deployed event log in private logs, contract_instance_deployer_contract/src/main.nr - const privateLogs = onlyRevertible - ? tx.data.forPublic!.revertibleAccumulatedData.privateLogs.filter(l => !l.isEmpty()) - : tx.data.getNonEmptyPrivateLogs(); - const contractInstanceEvents = privateLogs - .filter(log => ContractInstanceDeployedEvent.isContractInstanceDeployedEvent(log)) - .map(ContractInstanceDeployedEvent.fromLog); - contractInstanceEvents.forEach(e => this.instanceCache.delete(e.address.toString())); + /** + * Commits the current transaction's cached contracts to the block-level cache. + * Then, clears the tx cache. + */ + public commitContractsForTx(onlyNonRevertibles: boolean = false) { + // Merge non-revertible tx cache into block cache + this.blockCache.mergeFrom(this.currentTxNonRevertibleCache); + + if (!onlyNonRevertibles) { + // Merge revertible tx cache into block cache + this.blockCache.mergeFrom(this.currentTxRevertibleCache); + } - return Promise.resolve(); + // Clear the tx's caches + this.currentTxNonRevertibleCache.clear(); + this.currentTxRevertibleCache.clear(); } public async getContractInstance(address: AztecAddress): Promise { - return this.instanceCache.get(address.toString()) ?? (await this.dataSource.getContract(address)); + // Check caches in order: tx revertible -> tx non-revertible -> block -> data source + return ( + this.currentTxRevertibleCache.getInstance(address) ?? + this.currentTxNonRevertibleCache.getInstance(address) ?? + this.blockCache.getInstance(address) ?? + (await this.dataSource.getContract(address)) + ); } public async getContractClass(contractClassId: Fr): Promise { - return this.classCache.get(contractClassId.toString()) ?? (await this.dataSource.getContractClass(contractClassId)); + // Check caches in order: tx revertible -> tx non-revertible -> block -> data source + return ( + this.currentTxRevertibleCache.getClass(contractClassId) ?? + this.currentTxNonRevertibleCache.getClass(contractClassId) ?? + this.blockCache.getClass(contractClassId) ?? + (await this.dataSource.getContractClass(contractClassId)) + ); } public async getBytecodeCommitment(contractClassId: Fr): Promise { diff --git a/yarn-project/simulator/src/public/public_processor/apps_tests/deployments.test.ts b/yarn-project/simulator/src/public/public_processor/apps_tests/deployments.test.ts new file mode 100644 index 000000000000..e66962e94a13 --- /dev/null +++ b/yarn-project/simulator/src/public/public_processor/apps_tests/deployments.test.ts @@ -0,0 +1,233 @@ +import { Fr } from '@aztec/foundation/fields'; +import { TestDateProvider } from '@aztec/foundation/timer'; +import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; +import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; +import { RevertCode } from '@aztec/stdlib/avm'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { GasFees } from '@aztec/stdlib/gas'; +import { GlobalVariables } from '@aztec/stdlib/tx'; +import { getTelemetryClient } from '@aztec/telemetry-client'; +import { NativeWorldStateService } from '@aztec/world-state'; + +import { PublicTxSimulationTester, SimpleContractDataSource } from '../../../server.js'; +import { createContractClassAndInstance } from '../../avm/fixtures/index.js'; +import { addNewContractClassToTx, addNewContractInstanceToTx, createTxForPrivateOnly } from '../../fixtures/utils.js'; +import { WorldStateDB } from '../../public_db_sources.js'; +import { PublicTxSimulator } from '../../public_tx_simulator/public_tx_simulator.js'; +import { PublicProcessor } from '../public_processor.js'; + +describe('Public processor contract registration/deployment tests', () => { + //const logger = createLogger('public-processor-apps-tests-deployments'); + + const admin = AztecAddress.fromNumber(42); + const sender = AztecAddress.fromNumber(111); + + let worldStateDB: WorldStateDB; + let tester: PublicTxSimulationTester; + let processor: PublicProcessor; + + beforeEach(async () => { + const globals = GlobalVariables.empty(); + // apply some nonzero default gas fees + globals.gasFees = new GasFees(2, 3); + + const contractDataSource = new SimpleContractDataSource(); + const merkleTrees = await (await NativeWorldStateService.tmp()).fork(); + worldStateDB = new WorldStateDB(merkleTrees, contractDataSource); + const simulator = new PublicTxSimulator(merkleTrees, worldStateDB, globals, /*doMerkleOperations=*/ true); + + processor = new PublicProcessor( + merkleTrees, + globals, + worldStateDB, + simulator, + new TestDateProvider(), + getTelemetryClient(), + ); + + tester = new PublicTxSimulationTester(worldStateDB, contractDataSource, merkleTrees); + + // make sure tx senders have fee balance + await tester.setFeePayerBalance(admin); + await tester.setFeePayerBalance(sender); + }); + + it('can deploy in a private-only tx and call a public function later in the block', async () => { + const { contractClass, contractInstance } = await createContractClassAndInstance( + /*constructorArgs=*/ [], + admin, + AvmTestContractArtifact, + ); + + // First transaction - deploys and initializes first token contract + const deployTx = createTxForPrivateOnly(/*feePayer=*/ admin); + await addNewContractClassToTx(deployTx, contractClass); + await addNewContractInstanceToTx(deployTx, contractInstance); + + // NOTE: we need to include the contract artifact for each enqueued call, otherwise the tester + // will not know how to construct the TX since we are intentionally not adding the contract to + // the contract data source. + + // Second transaction - makes a simple public call on the deployed contract + const simplePublicTx = await tester.createTx( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: contractInstance.address, + fnName: 'read_storage_single', + args: [], + contractArtifact: AvmTestContractArtifact, + }, + ], + ); + + const results = await processor.process([deployTx, simplePublicTx]); + const processedTxs = results[0]; + const failedTxs = results[1]; + expect(processedTxs.length).toBe(2); + expect(failedTxs.length).toBe(0); + + // First tx should succeed (constructor) + expect(processedTxs[0].revertCode).toEqual(RevertCode.OK); + + // Second tx should succeed (public call) + expect(processedTxs[1].revertCode).toEqual(RevertCode.OK); + }); + + it('can deploy a contract and call its public function in same tx', async () => { + const mintAmount = 1_000_000n; + const constructorArgs = [admin, /*name=*/ 'Token', /*symbol=*/ 'TOK', /*decimals=*/ new Fr(18)]; + const { contractClass, contractInstance } = await createContractClassAndInstance( + constructorArgs, + admin, + TokenContractArtifact, + ); + const token = contractInstance; + + // NOTE: we need to include the contract artifact for each enqueued call, otherwise the tester + // will not know how to construct the TX since we are intentionally not adding the contract to + // the contract data source. + + // Deploys a contract and calls its public constructor and another public call in same tx + const deployAndCallTx = await tester.createTx( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'constructor', + args: constructorArgs, + contractArtifact: TokenContractArtifact, + }, + { + address: token.address, + fnName: 'mint_to_public', + args: [/*to=*/ sender, mintAmount], + contractArtifact: TokenContractArtifact, + }, + ], + ); + await addNewContractClassToTx(deployAndCallTx, contractClass); + await addNewContractInstanceToTx(deployAndCallTx, contractInstance); + + const results = await processor.process([deployAndCallTx]); + const processedTxs = results[0]; + const failedTxs = results[1]; + expect(processedTxs.length).toBe(1); + expect(failedTxs.length).toBe(0); + + // First tx should succeed (constructor) + expect(processedTxs[0].revertCode).toEqual(RevertCode.OK); + }); + + it('new contract cannot get removed from block-level cache by a later failing transaction', async () => { + const mintAmount = 1_000_000n; + const constructorArgs = [admin, /*name=*/ 'Token', /*symbol=*/ 'TOK', /*decimals=*/ new Fr(18)]; + + const { contractClass, contractInstance } = await createContractClassAndInstance( + constructorArgs, + admin, + TokenContractArtifact, + ); + const token = contractInstance; + + // First transaction - deploys and initializes first token contract + const passingConstructorTx = await tester.createTx( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'constructor', + args: constructorArgs, + contractArtifact: TokenContractArtifact, + }, + ], + ); + await addNewContractClassToTx(passingConstructorTx, contractClass); + await addNewContractInstanceToTx(passingConstructorTx, contractInstance); + + // NOTE: we need to include the contract artifact for each enqueued call, otherwise the tester + // will not know how to construct the TX since we are intentionally not adding the contract to + // the contract data source. + + // Second transaction - deploys second token but fails during transfer + const receiver = AztecAddress.fromNumber(222); + const transferAmount = 10n; + const nonce = new Fr(0); + const failingConstructorTx = await tester.createTx( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'constructor', + args: constructorArgs, + contractArtifact: TokenContractArtifact, + }, + // The next enqueued call will fail because sender has no tokens to transfer + { + address: token.address, + fnName: 'transfer_in_public', + args: [/*from=*/ sender, /*to=*/ receiver, transferAmount, nonce], + contractArtifact: TokenContractArtifact, + }, + ], + ); + // FIXME(#12375): should be able to include the nullifier insertions, but at the moment + // tx simulator cannot recover from errors during revertible private insertions. + // Once fixed, this skipNullifierInsertion flag can be removed. + await addNewContractClassToTx(failingConstructorTx, contractClass, /*skipNullifierInsertion=*/ true); + await addNewContractInstanceToTx(failingConstructorTx, contractInstance, /*skipNullifierInsertion=*/ true); + + // Third transaction - verifies first token is still accessible by minting + const mintTx = await tester.createTx( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'mint_to_public', + args: [/*to=*/ sender, mintAmount], + contractArtifact: TokenContractArtifact, + }, + ], + ); + + const results = await processor.process([passingConstructorTx, failingConstructorTx, mintTx]); + const processedTxs = results[0]; + const failedTxs = results[1]; + expect(processedTxs.length).toBe(3); + expect(failedTxs.length).toBe(0); + + // First tx should succeed (constructor) + expect(processedTxs[0].revertCode).toEqual(RevertCode.OK); + + // Second tx should revert in app logic (failed transfer) + expect(processedTxs[1].revertCode).toEqual(RevertCode.APP_LOGIC_REVERTED); + + // Third tx should succeed (mint), proving first contract is still accessible + expect(processedTxs[2].revertCode).toEqual(RevertCode.OK); + }); +}); diff --git a/yarn-project/simulator/src/public/apps_tests/token_public_processor.test.ts b/yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts similarity index 93% rename from yarn-project/simulator/src/public/apps_tests/token_public_processor.test.ts rename to yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts index a44560a08e0f..ad971d355c2b 100644 --- a/yarn-project/simulator/src/public/apps_tests/token_public_processor.test.ts +++ b/yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts @@ -9,10 +9,10 @@ import { GlobalVariables } from '@aztec/stdlib/tx'; import { getTelemetryClient } from '@aztec/telemetry-client'; import { NativeWorldStateService } from '@aztec/world-state'; -import { PublicTxSimulationTester, SimpleContractDataSource } from '../../server.js'; -import { WorldStateDB } from '../public_db_sources.js'; +import { PublicTxSimulationTester, SimpleContractDataSource } from '../../../server.js'; +import { WorldStateDB } from '../../public_db_sources.js'; +import { PublicTxSimulator } from '../../public_tx_simulator/public_tx_simulator.js'; import { PublicProcessor } from '../public_processor.js'; -import { PublicTxSimulator } from '../public_tx_simulator.js'; describe('Public Processor app tests: TokenContract', () => { const logger = createLogger('public-processor-apps-tests-token'); @@ -22,6 +22,7 @@ describe('Public Processor app tests: TokenContract', () => { const sender = AztecAddress.fromNumber(111); let token: ContractInstanceWithAddress; + let worldStateDB: WorldStateDB; let tester: PublicTxSimulationTester; let processor: PublicProcessor; @@ -32,7 +33,7 @@ describe('Public Processor app tests: TokenContract', () => { const contractDataSource = new SimpleContractDataSource(); const merkleTrees = await (await NativeWorldStateService.tmp()).fork(); - const worldStateDB = new WorldStateDB(merkleTrees, contractDataSource); + worldStateDB = new WorldStateDB(merkleTrees, contractDataSource); const simulator = new PublicTxSimulator(merkleTrees, worldStateDB, globals, /*doMerkleOperations=*/ true); processor = new PublicProcessor( diff --git a/yarn-project/simulator/src/public/public_processor.test.ts b/yarn-project/simulator/src/public/public_processor/public_processor.test.ts similarity index 98% rename from yarn-project/simulator/src/public/public_processor.test.ts rename to yarn-project/simulator/src/public/public_processor/public_processor.test.ts index d443c533d97f..fa5d2f30b37b 100644 --- a/yarn-project/simulator/src/public/public_processor.test.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.test.ts @@ -15,9 +15,9 @@ import { getTelemetryClient } from '@aztec/telemetry-client'; import { type MockProxy, mock } from 'jest-mock-extended'; -import type { WorldStateDB } from './public_db_sources.js'; +import { WorldStateDB } from '../public_db_sources.js'; +import type { PublicTxResult, PublicTxSimulator } from '../public_tx_simulator/public_tx_simulator.js'; import { PublicProcessor } from './public_processor.js'; -import type { PublicTxResult, PublicTxSimulator } from './public_tx_simulator.js'; describe('public_processor', () => { let db: MockProxy; diff --git a/yarn-project/simulator/src/public/public_processor.ts b/yarn-project/simulator/src/public/public_processor/public_processor.ts similarity index 96% rename from yarn-project/simulator/src/public/public_processor.ts rename to yarn-project/simulator/src/public/public_processor/public_processor.ts index f00e3c7cf1c5..b2a805c685de 100644 --- a/yarn-project/simulator/src/public/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts @@ -33,9 +33,9 @@ import { } from '@aztec/telemetry-client'; import { ForkCheckpoint } from '@aztec/world-state/native'; -import { WorldStateDB } from './public_db_sources.js'; +import { WorldStateDB } from '../public_db_sources.js'; +import { PublicTxSimulator } from '../public_tx_simulator/public_tx_simulator.js'; import { PublicProcessorMetrics } from './public_processor_metrics.js'; -import { PublicTxSimulator } from './public_tx_simulator.js'; /** * Creates new instances of PublicProcessor given the provided merkle tree db and contract data source. @@ -257,7 +257,7 @@ export class PublicProcessor implements Traceable { await checkpoint.revert(); continue; } else { - this.log.trace(`Tx ${(await tx.getTxHash()).toString()} is valid post processing.`); + this.log.trace(`Tx ${txHash.toString()} is valid post processing.`); } } @@ -265,6 +265,10 @@ export class PublicProcessor implements Traceable { // If there are no public calls, perform all tree insertions for side effects from private // When there are public calls, the PublicTxSimulator & AVM handle tree insertions. await this.doTreeInsertionsForPrivateOnlyTx(processedTx); + // Add any contracts registered/deployed in this private-only tx to the block-level cache + // (add to tx-level cache and then commit to block-level cache) + await this.worldStateDB.addNewContracts(tx); + this.worldStateDB.commitContractsForTx(); } nullifierCache?.addNullifiers(processedTx.txEffect.nullifiers.map(n => n.toBuffer())); @@ -282,13 +286,15 @@ export class PublicProcessor implements Traceable { break; } const errorMessage = err instanceof Error ? err.message : 'Unknown error'; - this.log.warn(`Failed to process tx ${tx.getTxHash()}: ${errorMessage} ${err?.stack}`); + this.log.warn(`Failed to process tx ${txHash.toString()}: ${errorMessage} ${err?.stack}`); failed.push({ tx, error: err instanceof Error ? err : new Error(errorMessage) }); returns.push(new NestedProcessReturnValues([])); } finally { // Base case is we always commit the checkpoint. Using the ForkCheckpoint means this has no effect if the tx was reverted await checkpoint.commit(); + // The tx-level contracts cache should not live on to the next tx + this.worldStateDB.clearContractsForTx(); } } @@ -296,7 +302,7 @@ export class PublicProcessor implements Traceable { const rate = duration > 0 ? totalPublicGas.l2Gas / duration : 0; this.metrics.recordAllTxs(totalPublicGas, rate); - this.log.info(`Processed ${result.length} successful txs and ${failed.length} txs in ${duration}s`, { + this.log.info(`Processed ${result.length} successful txs and ${failed.length} failed txs in ${duration}s`, { duration, rate, totalPublicGas, @@ -478,7 +484,7 @@ export class PublicProcessor implements Traceable { } processedPhases.forEach(phase => { - if (phase.revertReason) { + if (phase.reverted) { this.metrics.recordRevertedPhase(phase.phase); } else { this.metrics.recordPhaseDuration(phase.phase, phase.durationMs); diff --git a/yarn-project/simulator/src/public/public_processor_metrics.ts b/yarn-project/simulator/src/public/public_processor/public_processor_metrics.ts similarity index 100% rename from yarn-project/simulator/src/public/public_processor_metrics.ts rename to yarn-project/simulator/src/public/public_processor/public_processor_metrics.ts diff --git a/yarn-project/simulator/src/public/apps_tests/avm_test.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_test.test.ts similarity index 95% rename from yarn-project/simulator/src/public/apps_tests/avm_test.test.ts rename to yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_test.test.ts index a313e476bec0..f425b34fdb51 100644 --- a/yarn-project/simulator/src/public/apps_tests/avm_test.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_test.test.ts @@ -3,7 +3,7 @@ import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { PublicTxSimulationTester } from '../fixtures/public_tx_simulation_tester.js'; +import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; describe('Public TX simulator apps tests: AvmTestContract', () => { const deployer = AztecAddress.fromNumber(42); diff --git a/yarn-project/simulator/src/public/apps_tests/token.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts similarity index 97% rename from yarn-project/simulator/src/public/apps_tests/token.test.ts rename to yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts index bb0bc15a1abd..861aaadfd36f 100644 --- a/yarn-project/simulator/src/public/apps_tests/token.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts @@ -4,7 +4,7 @@ import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { PublicTxSimulationTester } from '../fixtures/public_tx_simulation_tester.js'; +import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; import type { PublicTxResult } from '../public_tx_simulator.js'; describe('Public TX simulator apps tests: TokenContract', () => { diff --git a/yarn-project/simulator/src/public/public_tx_context.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_context.ts similarity index 98% rename from yarn-project/simulator/src/public/public_tx_context.ts rename to yarn-project/simulator/src/public/public_tx_simulator/public_tx_context.ts index 2b8c38f38916..04bcc0b367ab 100644 --- a/yarn-project/simulator/src/public/public_tx_context.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_context.ts @@ -45,10 +45,10 @@ import { import { strict as assert } from 'assert'; import { inspect } from 'util'; -import { AvmPersistableStateManager } from './avm/index.js'; -import type { WorldStateDB } from './public_db_sources.js'; -import { SideEffectArrayLengths, SideEffectTrace } from './side_effect_trace.js'; -import { getCallRequestsByPhase, getExecutionRequestsByPhase } from './utils.js'; +import { AvmPersistableStateManager } from '../avm/index.js'; +import type { WorldStateDB } from '../public_db_sources.js'; +import { SideEffectArrayLengths, SideEffectTrace } from '../side_effect_trace.js'; +import { getCallRequestsByPhase, getExecutionRequestsByPhase } from '../utils.js'; /** * The transaction-level context for public execution. diff --git a/yarn-project/simulator/src/public/public_tx_simulator.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.test.ts similarity index 99% rename from yarn-project/simulator/src/public/public_tx_simulator.test.ts rename to yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.test.ts index 22e1f0b8772d..940b10f3d877 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.test.ts @@ -36,10 +36,10 @@ import { NativeWorldStateService } from '@aztec/world-state'; import { jest } from '@jest/globals'; import { mock } from 'jest-mock-extended'; -import { AvmFinalizedCallResult } from './avm/avm_contract_call_result.js'; -import type { AvmPersistableStateManager } from './avm/journal/journal.js'; -import type { InstructionSet } from './avm/serialization/bytecode_serialization.js'; -import { WorldStateDB } from './public_db_sources.js'; +import { AvmFinalizedCallResult } from '../avm/avm_contract_call_result.js'; +import type { AvmPersistableStateManager } from '../avm/journal/journal.js'; +import type { InstructionSet } from '../avm/serialization/bytecode_serialization.js'; +import { WorldStateDB } from '../public_db_sources.js'; import { type PublicTxResult, PublicTxSimulator } from './public_tx_simulator.js'; describe('public_tx_simulator', () => { @@ -173,7 +173,7 @@ describe('public_tx_simulator', () => { publicContractClass.privateFunctionsRoot, ...bufferAsFields( publicContractClass.packedBytecode, - Math.ceil(publicContractClass.packedBytecode.length / 32) + 1, + Math.ceil(publicContractClass.packedBytecode.length / 31) + 1, ), ]; const contractClassLog = ContractClassLog.fromFields([ diff --git a/yarn-project/simulator/src/public/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts similarity index 78% rename from yarn-project/simulator/src/public/public_tx_simulator.ts rename to yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts index a9cd2523a0e8..51636ad3138a 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts @@ -20,12 +20,12 @@ import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trac import { strict as assert } from 'assert'; -import { getPublicFunctionDebugName } from '../common/debug_fn_name.js'; -import type { AvmFinalizedCallResult } from './avm/avm_contract_call_result.js'; -import { type AvmPersistableStateManager, AvmSimulator } from './avm/index.js'; -import { NullifierCollisionError } from './avm/journal/nullifiers.js'; -import { ExecutorMetrics } from './executor_metrics.js'; -import type { WorldStateDB } from './public_db_sources.js'; +import { getPublicFunctionDebugName } from '../../common/debug_fn_name.js'; +import type { AvmFinalizedCallResult } from '../avm/avm_contract_call_result.js'; +import { type AvmPersistableStateManager, AvmSimulator } from '../avm/index.js'; +import { NullifierCollisionError } from '../avm/journal/nullifiers.js'; +import { ExecutorMetrics } from '../executor_metrics.js'; +import type { WorldStateDB } from '../public_db_sources.js'; import { PublicTxContext } from './public_tx_context.js'; export type ProcessedPhase = { @@ -72,85 +72,89 @@ export class PublicTxSimulator { * @returns The result of the transaction's public execution. */ public async simulate(tx: Tx): Promise { - const startTime = process.hrtime.bigint(); - - const txHash = await tx.getTxHash(); - this.log.debug(`Simulating ${tx.enqueuedPublicFunctionCalls.length} public calls for tx ${txHash}`, { txHash }); - - const context = await PublicTxContext.create( - this.db, - this.worldStateDB, - tx, - this.globalVariables, - this.doMerkleOperations, - ); + try { + const startTime = process.hrtime.bigint(); + + const txHash = await tx.getTxHash(); + this.log.debug(`Simulating ${tx.enqueuedPublicFunctionCalls.length} public calls for tx ${txHash}`, { txHash }); + + const context = await PublicTxContext.create( + this.db, + this.worldStateDB, + tx, + this.globalVariables, + this.doMerkleOperations, + ); + + const nonRevertStart = process.hrtime.bigint(); + await this.insertNonRevertiblesFromPrivate(context); + // add new contracts to the contracts db so that their functions may be found and called + // TODO(#6464): Should we allow emitting contracts in the private setup phase? + await this.worldStateDB.addNewNonRevertibleContracts(tx); + const nonRevertEnd = process.hrtime.bigint(); + this.metrics.recordPrivateEffectsInsertion(Number(nonRevertEnd - nonRevertStart) / 1_000, 'non-revertible'); + + const processedPhases: ProcessedPhase[] = []; + if (context.hasPhase(TxExecutionPhase.SETUP)) { + const setupResult: ProcessedPhase = await this.simulateSetupPhase(context); + processedPhases.push(setupResult); + } - // add new contracts to the contracts db so that their functions may be found and called - // TODO(#4073): This is catching only private deployments, when we add public ones, we'll - // have to capture contracts emitted in that phase as well. - // TODO(@spalladino): Should we allow emitting contracts in the fee preparation phase? - // TODO(#6464): Should we allow emitting contracts in the private setup phase? - // if so, this should only add contracts that were deployed during private app logic. - // FIXME: we shouldn't need to directly modify worldStateDb here! - await this.worldStateDB.addNewContracts(tx); - - const nonRevertStart = process.hrtime.bigint(); - await this.insertNonRevertiblesFromPrivate(context); - const nonRevertEnd = process.hrtime.bigint(); - this.metrics.recordPrivateEffectsInsertion(Number(nonRevertEnd - nonRevertStart) / 1_000, 'non-revertible'); - const processedPhases: ProcessedPhase[] = []; - if (context.hasPhase(TxExecutionPhase.SETUP)) { - const setupResult: ProcessedPhase = await this.simulateSetupPhase(context); - processedPhases.push(setupResult); - } + const revertStart = process.hrtime.bigint(); + // FIXME(#12375): TX shouldn't die if revertible insertions fail. Should just revert to snapshot. + await this.insertRevertiblesFromPrivate(context); + // add new contracts to the contracts db so that their functions may be found and called + await this.worldStateDB.addNewRevertibleContracts(tx); + const revertEnd = process.hrtime.bigint(); + this.metrics.recordPrivateEffectsInsertion(Number(revertEnd - revertStart) / 1_000, 'revertible'); + + if (context.hasPhase(TxExecutionPhase.APP_LOGIC)) { + const appLogicResult: ProcessedPhase = await this.simulateAppLogicPhase(context); + processedPhases.push(appLogicResult); + } - const revertStart = process.hrtime.bigint(); - await this.insertRevertiblesFromPrivate(context); - const revertEnd = process.hrtime.bigint(); - this.metrics.recordPrivateEffectsInsertion(Number(revertEnd - revertStart) / 1_000, 'revertible'); - if (context.hasPhase(TxExecutionPhase.APP_LOGIC)) { - const appLogicResult: ProcessedPhase = await this.simulateAppLogicPhase(context); - processedPhases.push(appLogicResult); - } + if (context.hasPhase(TxExecutionPhase.TEARDOWN)) { + const teardownResult: ProcessedPhase = await this.simulateTeardownPhase(context); + processedPhases.push(teardownResult); + } - if (context.hasPhase(TxExecutionPhase.TEARDOWN)) { - const teardownResult: ProcessedPhase = await this.simulateTeardownPhase(context); - processedPhases.push(teardownResult); - } + await context.halt(); + await this.payFee(context); - await context.halt(); - await this.payFee(context); + const endStateReference = await this.db.getStateReference(); - const endStateReference = await this.db.getStateReference(); + const avmProvingRequest = await context.generateProvingRequest(endStateReference); - const avmProvingRequest = await context.generateProvingRequest(endStateReference); + const revertCode = context.getFinalRevertCode(); - const revertCode = context.getFinalRevertCode(); - if (!revertCode.isOK()) { - // TODO(#6464): Should we allow emitting contracts in the private setup phase? - // if so, this is removing contracts deployed in private setup - // You can't submit contracts in public, so this is only relevant for private-created side effects - // FIXME: we shouldn't need to directly modify worldStateDb here! - await this.worldStateDB.removeNewContracts(tx, true); - // FIXME(dbanks12): should not be changing immutable tx - await tx.filterRevertedLogs(); + if (!revertCode.isOK()) { + await tx.filterRevertedLogs(); + } + // Commit contracts from this TX to the block-level cache and clear tx cache + // If the tx reverted, only commit non-revertible contracts + // NOTE: You can't create contracts in public, so this is only relevant for private-created contracts + this.worldStateDB.commitContractsForTx(/*onlyNonRevertibles=*/ !revertCode.isOK()); + + const endTime = process.hrtime.bigint(); + this.log.debug(`Public TX simulator took ${Number(endTime - startTime) / 1_000_000} ms\n`); + + return { + avmProvingRequest, + gasUsed: { + totalGas: context.getActualGasUsed(), + teardownGas: context.teardownGasUsed, + publicGas: context.getActualPublicGasUsed(), + billedGas: context.getTotalGasUsed(), + }, + revertCode, + revertReason: context.revertReason, + processedPhases: processedPhases, + }; + } finally { + // Make sure there are no new contracts in the tx-level cache. + // They should either be committed to block-level cache or cleared. + this.worldStateDB.clearContractsForTx(); } - - const endTime = process.hrtime.bigint(); - this.log.debug(`Public TX simulator took ${Number(endTime - startTime) / 1_000_000} ms\n`); - - return { - avmProvingRequest, - gasUsed: { - totalGas: context.getActualGasUsed(), - teardownGas: context.teardownGasUsed, - publicGas: context.getActualPublicGasUsed(), - billedGas: context.getTotalGasUsed(), - }, - revertCode, - revertReason: context.revertReason, - processedPhases: processedPhases, - }; } /** @@ -310,7 +314,7 @@ export class PublicTxSimulator { /** * Simulate an enqueued public call, without modifying the context (PublicTxContext). - * Resulting modifcations to the context can be applied by the caller. + * Resulting modifications to the context can be applied by the caller. * * This function can be mocked for testing to skip actual AVM simulation * while still simulating phases and generating a proving request. @@ -409,6 +413,7 @@ export class PublicTxSimulator { await stateManager.writeSiloedNullifiersFromPrivate(context.revertibleAccumulatedDataFromPrivate.nullifiers); } catch (e) { if (e instanceof NullifierCollisionError) { + // FIXME(#12375): simulator should be able to recover from this and just revert to snapshot. throw new NullifierCollisionError( `Nullifier collision encountered when inserting revertible nullifiers from private. Details:\n${e.message}\n.Stack:${e.stack}`, ); diff --git a/yarn-project/simulator/src/public/tx_contract_cache.ts b/yarn-project/simulator/src/public/tx_contract_cache.ts new file mode 100644 index 000000000000..a5e6ff4764e3 --- /dev/null +++ b/yarn-project/simulator/src/public/tx_contract_cache.ts @@ -0,0 +1,69 @@ +import type { Fr } from '@aztec/foundation/fields'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; + +/** + * A cache for contract classes and instances for a single transaction. + * Useful for tracking/retrieving contracts for a tx while leaving + * the option to clear them if the tx is reverted. + */ +export class TxContractCache { + private instanceCache = new Map(); + private classCache = new Map(); + + /** + * Add a contract instance to the cache + */ + public addInstance(address: AztecAddress, instance: ContractInstanceWithAddress): void { + this.instanceCache.set(address.toString(), instance); + } + + /** + * Add a contract class to the cache + */ + public addClass(classId: Fr, contractClass: ContractClassPublic): void { + this.classCache.set(classId.toString(), contractClass); + } + + /** + * Get a contract instance from the cache + */ + public getInstance(address: AztecAddress): ContractInstanceWithAddress | undefined { + return this.instanceCache.get(address.toString()); + } + + /** + * Get a contract class from the cache + */ + public getClass(classId: Fr): ContractClassPublic | undefined { + return this.classCache.get(classId.toString()); + } + + /** + * Check if the cache has a contract class + */ + public hasClass(classId: Fr): boolean { + return this.classCache.has(classId.toString()); + } + + /** + * Clear all entries from the cache + */ + public clear(): void { + this.instanceCache.clear(); + this.classCache.clear(); + } + + /** + * Merge another cache into this one + */ + public mergeFrom(other: TxContractCache): void { + other.instanceCache.forEach((value, key) => { + this.instanceCache.set(key, value); + }); + + other.classCache.forEach((value, key) => { + this.classCache.set(key, value); + }); + } +} diff --git a/yarn-project/stdlib/src/tx/processed_tx.ts b/yarn-project/stdlib/src/tx/processed_tx.ts index 142cc64a2f1f..2fb55f947cbb 100644 --- a/yarn-project/stdlib/src/tx/processed_tx.ts +++ b/yarn-project/stdlib/src/tx/processed_tx.ts @@ -54,6 +54,10 @@ export type ProcessedTx = { * Gas used by the entire transaction. */ gasUsed: GasUsed; + /** + * Code the tx was reverted (or OK). + */ + revertCode: RevertCode; /** * Reason the tx was reverted. */ @@ -114,6 +118,7 @@ export async function makeProcessedTxFromPrivateOnlyTx( constants, txEffect, gasUsed, + revertCode: RevertCode.OK, revertReason: undefined, }; } @@ -170,6 +175,7 @@ export async function makeProcessedTxFromTxWithPublicCalls( constants, txEffect, gasUsed, + revertCode, revertReason, }; }