diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp b/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp index 49a89b364acb..257c31e8081d 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp @@ -322,7 +322,9 @@ void AvmTraceBuilder::pay_fee() // TS equivalent: // computeFeePayerBalanceStorageSlot(fee_payer); std::vector slot_hash_inputs = { FEE_JUICE_BALANCES_SLOT, public_inputs.fee_payer }; - const auto balance_slot = poseidon2_trace_builder.poseidon2_hash(slot_hash_inputs, clk, Poseidon2Caller::SILO); + // TODO: do constrained slot derivations! + // const auto balance_slot = poseidon2_trace_builder.poseidon2_hash(slot_hash_inputs, clk, Poseidon2Caller::SILO); + const auto balance_slot = Poseidon2::hash(slot_hash_inputs); // ** Read the balance before fee payment ** // TS equivalent: diff --git a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr index 581d4827c8b8..91f12b945528 100644 --- a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr @@ -670,7 +670,14 @@ contract AvmTest { * be optimized away. ************************************************************************/ #[public] - fn bulk_testing(args_field: [Field; 10], args_u8: [u8; 10]) { + fn bulk_testing( + args_field: [Field; 10], + args_u8: [u8; 10], + get_instance_for_address: AztecAddress, + expected_deployer: AztecAddress, + expected_class_id: ContractClassId, + expected_initialization_hash: Field, + ) { dep::aztec::oracle::debug_log::debug_log("set_storage_single"); set_storage_single(30); dep::aztec::oracle::debug_log::debug_log("set_storage_list"); @@ -692,9 +699,12 @@ contract AvmTest { dep::aztec::oracle::debug_log::debug_log("pedersen_hash_with_index"); let _ = pedersen_hash_with_index(args_field); dep::aztec::oracle::debug_log::debug_log("test_get_contract_instance"); - // address should match yarn-project/simulator/src/public/fixtures/index.ts's - // MockedAvmTestContractDataSource.otherContractInstance - test_get_contract_instance(AztecAddress::from_field(0x4444)); + test_get_contract_instance_matches( + get_instance_for_address, + expected_deployer, + expected_class_id, + expected_initialization_hash, + ); dep::aztec::oracle::debug_log::debug_log("get_address"); let _ = get_address(); dep::aztec::oracle::debug_log::debug_log("get_sender"); diff --git a/yarn-project/bb-prover/package.json b/yarn-project/bb-prover/package.json index d6c9fd741940..acff4b624eb5 100644 --- a/yarn-project/bb-prover/package.json +++ b/yarn-project/bb-prover/package.json @@ -86,6 +86,8 @@ "devDependencies": { "@aztec/ethereum": "workspace:^", "@aztec/kv-store": "workspace:^", + "@aztec/noir-contracts.js": "workspace:^", + "@aztec/protocol-contracts": "workspace:^", "@aztec/types": "workspace:^", "@jest/globals": "^29.5.0", "@types/jest": "^29.5.0", diff --git a/yarn-project/bb-prover/src/avm_proving.test.ts b/yarn-project/bb-prover/src/avm_proving.test.ts deleted file mode 100644 index 9b506e52c064..000000000000 --- a/yarn-project/bb-prover/src/avm_proving.test.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { - MAX_L2_TO_L1_MSGS_PER_TX, - MAX_NOTE_HASHES_PER_TX, - MAX_NULLIFIERS_PER_TX, - MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS, - MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, - MAX_PUBLIC_LOGS_PER_TX, - VerificationKeyData, -} from '@aztec/circuits.js'; -import { Fr } from '@aztec/foundation/fields'; -import { createLogger } from '@aztec/foundation/log'; -import { - MockedAvmTestContractDataSource, - simulateAvmTestContractGenerateCircuitInputs, -} from '@aztec/simulator/public/fixtures'; - -import fs from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import path from 'path'; - -import { - type BBSuccess, - BB_RESULT, - generateAvmProof, - generateAvmProofV2, - verifyAvmProof, - verifyAvmProofV2, -} from './bb/execute.js'; -import { extractAvmVkData } from './verification_key/verification_key_data.js'; - -const TIMEOUT = 300_000; - -describe('AVM WitGen, proof generation and verification tests', () => { - it( - 'bulk_testing v1', - async () => { - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ false, // full proving & verifying - 'bulk_testing', - /*args=*/ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)), - ); - }, - TIMEOUT, - ); -}); -describe('AVM WitGen, "check circuit" tests', () => { - it( - 'perform too many storage writes and revert', - async () => { - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'n_storage_writes', - /*args=*/ [new Fr(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX + 1)], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); - it( - 'create too many note hashes and revert', - async () => { - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'n_new_note_hashes', - /*args=*/ [new Fr(MAX_NOTE_HASHES_PER_TX + 1)], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); - it( - 'create too many nullifiers and revert', - async () => { - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'n_new_nullifiers', - /*args=*/ [new Fr(MAX_NULLIFIERS_PER_TX + 1)], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); - it( - 'create too many l2tol1 messages and revert', - async () => { - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'n_new_l2_to_l1_msgs', - /*args=*/ [new Fr(MAX_L2_TO_L1_MSGS_PER_TX + 1)], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); - it( - 'create too many public logs and revert', - async () => { - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'n_new_public_logs', - /*args=*/ [new Fr(MAX_PUBLIC_LOGS_PER_TX + 1)], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); - it( - 'call the max number of unique contract classes', - async () => { - const contractDataSource = await MockedAvmTestContractDataSource.create(); - // args is initialized to MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS contract addresses with unique class IDs - const args = Array.from(contractDataSource.contractInstances.values()) - .map(instance => instance.address.toField()) - .slice(0, MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS); - // include the first contract again again at the end to ensure that we can call it even after the limit is reached - args.push(args[0]); - // include another contract address that reuses a class ID to ensure that we can call it even after the limit is reached - args.push(contractDataSource.instanceSameClassAsFirstContract.address.toField()); - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'nested_call_to_add_n_times_different_addresses', - args, - /*expectRevert=*/ false, - /*skipContractDeployments=*/ false, - contractDataSource, - ); - }, - TIMEOUT, - ); - it( - 'attempt too many calls to unique contract class ids', - async () => { - const contractDataSource = await MockedAvmTestContractDataSource.create(); - // args is initialized to MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS+1 contract addresses with unique class IDs - // should fail because we are trying to call MAX+1 unique class IDs - const args = Array.from(contractDataSource.contractInstances.values()).map(instance => - instance.address.toField(), - ); - // push an empty one (just padding to match function calldata size of MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS+2) - args.push(new Fr(0)); - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'nested_call_to_add_n_times_different_addresses', - args, - /*expectRevert=*/ true, - /*skipContractDeployments=*/ false, - contractDataSource, - ); - }, - TIMEOUT, - ); - it( - 'top-level exceptional halts in both app logic and teardown', - async () => { - await proveAndVerifyAvmTestContract( - /*checkCircuitOnly=*/ true, - /*setupFunctionNames=*/ [], - /*setupArgs=*/ [], - /*appFunctionNames=*/ ['divide_by_zero'], - /*appArgs=*/ [[]], - /*teardownFunctionName=*/ 'divide_by_zero', - /*teardownArgs=*/ [], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); - it( - 'top-level exceptional halt in app logic, but teardown succeeds', - async () => { - await proveAndVerifyAvmTestContract( - /*checkCircuitOnly=*/ true, - /*setupFunctionNames=*/ [], - /*setupArgs=*/ [], - /*appFunctionNames=*/ ['divide_by_zero'], - /*appArgs=*/ [[]], - /*teardownFunctionName=*/ 'add_args_return', - /*teardownArgs=*/ [new Fr(1), new Fr(2)], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); - it( - 'top-level exceptional halt in teardown, but app logic succeeds', - async () => { - await proveAndVerifyAvmTestContract( - /*checkCircuitOnly=*/ true, - /*setupFunctionNames=*/ [], - /*setupArgs=*/ [], - /*appFunctionNames=*/ ['add_args_return'], - /*appArgs=*/ [[new Fr(1), new Fr(2)]], - /*teardownFunctionName=*/ 'divide_by_zero', - /*teardownArgs=*/ [], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); - it( - 'a nested exceptional halt propagate to top-level', - async () => { - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'external_call_to_divide_by_zero', - /*args=*/ [], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); - it( - 'a nested exceptional halt is recovered from in caller', - async () => { - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'external_call_to_divide_by_zero_recovers', - /*args=*/ [], - /*expectRevert=*/ false, - ); - }, - TIMEOUT, - ); - it( - 'an exceptional halt due to a nested call to non-existent contract is propagated to top-level', - async () => { - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'nested_call_to_nothing', - /*args=*/ [], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); - it( - 'an exceptional halt due to a nested call to non-existent contract is recovered from in caller', - async () => { - await proveAndVerifyAvmTestContractSimple( - /*checkCircuitOnly=*/ true, // quick - 'nested_call_to_nothing_recovers', - /*args=*/ [], - /*expectRevert=*/ false, - ); - }, - TIMEOUT, - ); - // FIXME(dbanks12): fails with "Lookup PERM_MAIN_ALU failed." - it.skip('a top-level exceptional halts due to a non-existent contract in app-logic and teardown', async () => { - await proveAndVerifyAvmTestContract( - /*checkCircuitOnly=*/ true, - /*setupFunctionNames=*/ [], - /*setupArgs=*/ [], - /*appFunctionNames=*/ ['add_args_return'], - /*appArgs=*/ [[new Fr(1), new Fr(2)]], - /*teardownFunctionName=*/ 'add_args_return', - /*teardownArgs=*/ [new Fr(1), new Fr(2)], - /*expectRevert=*/ true, - /*skipContractDeployments=*/ true, - ); - }); - it( - 'enqueued calls in every phase, with enqueued calls that depend on each other', - async () => { - await proveAndVerifyAvmTestContract( - /*checkCircuitOnly=*/ true, - /*setupFunctionNames=*/ ['read_assert_storage_single', 'set_storage_single'], - /*setupArgs=*/ [[new Fr(0)], [new Fr(5)]], - /*appFunctionNames=*/ ['read_assert_storage_single', 'set_storage_single'], - /*appArgs=*/ [[new Fr(5)], [new Fr(10)]], - /*teardownFunctionName=*/ 'read_assert_storage_single', - /*teardownArgs=*/ [new Fr(10)], - ); - }, - TIMEOUT, - ); - it( - 'Should prove and verify a TX that reverts in teardown', - async () => { - await proveAndVerifyAvmTestContract( - /*checkCircuitOnly=*/ true, - /*setupFunctionNames=*/ [], - /*setupArgs=*/ [], - /*appFunctionNames=*/ [], - /*appArgs=*/ [], - /*teardownFunctionName=*/ 'read_assert_storage_single', - /*teardownArgs=*/ [new Fr(10)], - /*expectRevert=*/ true, - ); - }, - TIMEOUT, - ); -}); - -/** - * Simulate, prove and verify just a single App Logic enqueued call. - */ -async function proveAndVerifyAvmTestContractSimple( - checkCircuitOnly: boolean, - functionName: string, - args: Fr[] = [], - expectRevert = false, - skipContractDeployments = false, - contractDataSource?: MockedAvmTestContractDataSource, -) { - if (!contractDataSource) { - contractDataSource = await MockedAvmTestContractDataSource.create(skipContractDeployments); - } - await proveAndVerifyAvmTestContract( - checkCircuitOnly, - /*setupFunctionNames=*/ [], - /*setupArgs=*/ [], - /*appFunctionNames=*/ [functionName], - /*appArgs=*/ [args], - /*teardownFunctionName=*/ undefined, - /*teardownArgs=*/ [], - expectRevert, - skipContractDeployments, - contractDataSource, - ); -} - -/** - * Simulate, prove and verify setup calls, app logic calls and optionally a teardown call in one TX. - */ -async function proveAndVerifyAvmTestContract( - checkCircuitOnly: boolean, - setupFunctionNames: string[], - setupArgs: Fr[][], - appFunctionNames: string[], - appArgs: Fr[][] = [], - teardownFunctionName?: string, - teardownArgs: Fr[] = [], - expectRevert = false, - skipContractDeployments = false, - contractDataSource?: MockedAvmTestContractDataSource, -) { - if (!contractDataSource) { - contractDataSource = await MockedAvmTestContractDataSource.create(skipContractDeployments); - } - const avmCircuitInputs = await simulateAvmTestContractGenerateCircuitInputs( - setupFunctionNames, - setupArgs, - appFunctionNames, - appArgs, - teardownFunctionName, - teardownArgs, - expectRevert, - contractDataSource, - ); - - const logger = createLogger('bb-prover:avm-proving-test'); - - // The paths for the barretenberg binary and the write path are hardcoded for now. - const bbPath = path.resolve('../../barretenberg/cpp/build/bin/bb'); - const bbWorkingDirectory = await fs.mkdtemp(path.join(tmpdir(), 'bb-')); - - // Then we prove. - const proofRes = await generateAvmProof(bbPath, bbWorkingDirectory, avmCircuitInputs, logger, checkCircuitOnly); - if (proofRes.status === BB_RESULT.FAILURE) { - logger.error(`Proof generation failed: ${proofRes.reason}`); - } - expect(proofRes.status).toEqual(BB_RESULT.SUCCESS); - - // There is no proof to verify if we only check circuit. - if (!checkCircuitOnly) { - // Then we test VK extraction and serialization. - const succeededRes = proofRes as BBSuccess; - const vkData = await extractAvmVkData(succeededRes.vkPath!); - VerificationKeyData.fromBuffer(vkData.toBuffer()); - - // Then we verify. - const rawVkPath = path.join(succeededRes.vkPath!, 'vk'); - const verificationRes = await verifyAvmProof(bbPath, succeededRes.proofPath!, rawVkPath, logger); - expect(verificationRes.status).toBe(BB_RESULT.SUCCESS); - } -} - -describe('AVM WitGen, proof generation and verification', () => { - it('bulk_testing v2', async () => { - const functionName = 'bulk_testing'; - const calldata = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)); - const avmCircuitInputs = await simulateAvmTestContractGenerateCircuitInputs( - /*setupFunctionNames=*/ [], - /*setupArgs=*/ [], - /*appFunctionNames=*/ [functionName], - /*appArgs=*/ [calldata], - /*teardownFunctionName=*/ undefined, - /*teardownArgs=*/ [], - /*expectRevert=*/ false, - ); - - const logger = createLogger('bb-prover:avm-proving-test'); - - // The paths for the barretenberg binary and the write path are hardcoded for now. - const bbPath = path.resolve('../../barretenberg/cpp/build/bin/bb'); - const bbWorkingDirectory = await fs.mkdtemp(path.join(tmpdir(), 'bb-')); - - // Then we prove. - const proofRes = await generateAvmProofV2(bbPath, bbWorkingDirectory, avmCircuitInputs, logger); - if (proofRes.status === BB_RESULT.FAILURE) { - logger.error(`Proof generation failed: ${proofRes.reason}`); - } - expect(proofRes.status).toEqual(BB_RESULT.SUCCESS); - const succeededRes = proofRes as BBSuccess; - - // Then we verify. - // Placeholder for now. - const publicInputs = { - dummy: [] as any[], - }; - - const rawVkPath = path.join(succeededRes.vkPath!, 'vk'); - const verificationRes = await verifyAvmProofV2( - bbPath, - bbWorkingDirectory, - succeededRes.proofPath!, - publicInputs, - rawVkPath, - logger, - ); - expect(verificationRes.status).toBe(BB_RESULT.SUCCESS); - }, 180_000); -}); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_amm.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_amm.test.ts new file mode 100644 index 000000000000..8f569f52de1b --- /dev/null +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_amm.test.ts @@ -0,0 +1,130 @@ +import { AztecAddress, type ContractInstanceWithAddress } from '@aztec/circuits.js'; +import { Fr } from '@aztec/foundation/fields'; +import { AMMContractArtifact } from '@aztec/noir-contracts.js/AMM'; +import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; + +import { jest } from '@jest/globals'; + +import { AvmProvingTester } from './avm_proving_tester.js'; + +const TIMEOUT = 300_000; +const INITIAL_TOKEN_BALANCE = 1_000_000_000n; + +describe('AVM Witgen & Circuit apps tests: AMM', () => { + jest.setTimeout(TIMEOUT); + const admin = AztecAddress.fromNumber(42); + const liquidityProvider = AztecAddress.fromNumber(111); + const otherLiqduidityProvider = AztecAddress.fromNumber(222); + const swapper = AztecAddress.fromNumber(333); + + let token0: ContractInstanceWithAddress; + let token1: ContractInstanceWithAddress; + let liquidityToken: ContractInstanceWithAddress; + let amm: ContractInstanceWithAddress; + let tester: AvmProvingTester; + + beforeEach(async () => { + tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + }); + + // TODO(dbanks12): add tester support for authwit and finish implementing this test + it.skip('amm operations', async () => { + token0 = await deployToken(/*seed=*/ 0); + token1 = await deployToken(/*seed=*/ 1); + liquidityToken = await deployToken(/*seed=*/ 2); + amm = await deployAMM(); + + // set the AMM as the minter for the liquidity token + await tester.simProveVerify( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'set_minter', + args: [/*minter=*/ amm, /*approve=*/ true], + address: liquidityToken.address, + }, + ], + /*teardownCall=*/ undefined, + /*expectRevert=*/ false, + ); + + await mint(/*to=*/ liquidityProvider, /*amount=*/ INITIAL_TOKEN_BALANCE, token0); + await mint(/*to=*/ liquidityProvider, /*amount=*/ INITIAL_TOKEN_BALANCE, token1); + await mint(/*to=*/ otherLiqduidityProvider, /*amount=*/ INITIAL_TOKEN_BALANCE, token0); + await mint(/*to=*/ otherLiqduidityProvider, /*amount=*/ INITIAL_TOKEN_BALANCE, token1); + await mint(/*to=*/ swapper, /*amount=*/ INITIAL_TOKEN_BALANCE, token0); + + //const ammBalancesBefore = await getAmmBalances(); + //const lpBalancesBefore = await getWalletBalances(liquidityProvider); + + //const amount0Max = lpBalancesBefore.token0; + //const amount0Min = lpBalancesBefore.token0 / 2n; + //const amount1Max = lpBalancesBefore.token1; + //const amount1Min = lpBalancesBefore.token1 / 2n; + + //const nonceForAuthwits = Fr.random(); + }); + + const deployToken = async (seed = 0) => { + const constructorArgs = [admin, /*name=*/ 'Token', /*symbol=*/ 'TOK', /*decimals=*/ new Fr(18)]; + const token = await tester.registerAndDeployContract( + constructorArgs, + /*deployer=*/ admin, + TokenContractArtifact, + seed, + ); + + await tester.simProveVerify( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'constructor', + args: constructorArgs, + address: token.address, + }, + ], + /*teardownCall=*/ undefined, + /*expectRevert=*/ false, + ); + return token; + }; + + const deployAMM = async (seed = 0) => { + const constructorArgs = [token0, token1, liquidityToken]; + const amm = await tester.registerAndDeployContract(constructorArgs, /*deployer=*/ admin, AMMContractArtifact, seed); + + await tester.simProveVerify( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'constructor', + args: constructorArgs, + address: amm.address, + }, + ], + /*teardownCall=*/ undefined, + /*expectRevert=*/ false, + ); + return amm; + }; + + const mint = async (to: AztecAddress, amount: bigint, token: ContractInstanceWithAddress) => { + await tester.simProveVerify( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'mint_to_public', + args: [to, amount], + address: token.address, + }, + ], + /*teardownCall=*/ undefined, + /*expectRevert=*/ false, + ); + return token; + }; +}); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_token.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_token.test.ts new file mode 100644 index 000000000000..5450e836c98f --- /dev/null +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_token.test.ts @@ -0,0 +1,118 @@ +import { AztecAddress, type ContractInstanceWithAddress } from '@aztec/circuits.js'; +import { Fr } from '@aztec/foundation/fields'; +import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; +import { type PublicTxResult } from '@aztec/simulator/server'; + +import { jest } from '@jest/globals'; + +import { AvmProvingTester } from './avm_proving_tester.js'; + +const TIMEOUT = 300_000; + +describe('AVM Witgen & Circuit apps tests: TokenContract', () => { + jest.setTimeout(TIMEOUT); + const admin = AztecAddress.fromNumber(42); + const sender = AztecAddress.fromNumber(111); + const receiver = AztecAddress.fromNumber(222); + + let token: ContractInstanceWithAddress; + let tester: AvmProvingTester; + + beforeEach(async () => { + tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + + const constructorArgs = [admin, /*name=*/ 'Token', /*symbol=*/ 'TOK', /*decimals=*/ new Fr(18)]; + token = await tester.registerAndDeployContract(constructorArgs, /*deployer=*/ admin, TokenContractArtifact); + + await tester.simProveVerify( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'constructor', + args: constructorArgs, + address: token.address, + }, + ], + /*teardownCall=*/ undefined, + /*expectRevert=*/ false, + ); + }); + + it('token mint, transfer, burn', async () => { + const mintAmount = 100n; + const transferAmount = 50n; + const nonce = new Fr(0); + + await checkBalance(sender, 0n); + + await tester.simProveVerify( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'mint_to_public', + args: [/*to=*/ sender, mintAmount], + address: token.address, + }, + ], + /*teardownCall=*/ undefined, + /*expectRevert=*/ false, + ); + await checkBalance(sender, mintAmount); + + await tester.simProveVerify( + /*sender=*/ sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'transfer_in_public', + args: [/*from=*/ sender, /*to=*/ receiver, transferAmount, nonce], + address: token.address, + }, + ], + /*teardownCall=*/ undefined, + /*expectRevert=*/ false, + ); + await checkBalance(sender, mintAmount - transferAmount); + await checkBalance(receiver, transferAmount); + + await tester.simProveVerify( + /*sender=*/ receiver, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'burn_public', + args: [/*from=*/ receiver, transferAmount, nonce], + address: token.address, + }, + ], + /*teardownCall=*/ undefined, + /*expectRevert=*/ false, + ); + await checkBalance(receiver, 0n); + }); + + const checkBalance = async (account: AztecAddress, expectedBalance: bigint) => { + const balResult = await tester.simulateTx( + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'balance_of_public', + args: [/*owner=*/ account], + isStaticCall: true, + }, + ], + ); + expect(balResult.revertCode.isOK()).toBe(true); + expectAppCall0Output(balResult, [new Fr(expectedBalance), Fr.zero()]); + }; +}); + +function expectAppCall0Output(txResult: PublicTxResult, expectedOutput: Fr[]): void { + expect(txResult.processedPhases).toEqual([ + expect.objectContaining({ returnValues: [expect.objectContaining({ values: expectedOutput })] }), + ]); +} diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proving.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving.test.ts new file mode 100644 index 000000000000..898899e2195a --- /dev/null +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving.test.ts @@ -0,0 +1,486 @@ +import { + AztecAddress, + type ContractClassPublic, + type ContractInstanceWithAddress, + FunctionSelector, + MAX_L2_TO_L1_MSGS_PER_TX, + MAX_NOTE_HASHES_PER_TX, + MAX_NULLIFIERS_PER_TX, + MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS, + MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, + MAX_PUBLIC_LOGS_PER_TX, + PUBLIC_DISPATCH_SELECTOR, +} from '@aztec/circuits.js'; +import { makeContractClassPublic, makeContractInstanceFromClassId } from '@aztec/circuits.js/testing'; +import { Fr } from '@aztec/foundation/fields'; +import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; +import { type ProtocolContract } from '@aztec/protocol-contracts'; +import { FeeJuiceArtifact, getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; +import { getAvmTestContractBytecode } from '@aztec/simulator/public/fixtures'; + +import { AvmProvingTester, AvmProvingTesterV2 } from './avm_proving_tester.js'; + +const TIMEOUT = 300_000; +const DISPATCH_FN_NAME = 'public_dispatch'; +const DISPATCH_SELECTOR = new FunctionSelector(PUBLIC_DISPATCH_SELECTOR); + +describe('AVM WitGen & Circuit', () => { + describe('proving and verification', () => { + const avmTestContractClassSeed = 0; + const avmTestContractBytecode = getAvmTestContractBytecode(DISPATCH_FN_NAME); + let avmTestContractClass: ContractClassPublic; + let avmTestContractInstance: ContractInstanceWithAddress; + let tester: AvmProvingTester; + + beforeEach(async () => { + avmTestContractClass = await makeContractClassPublic( + /*seed=*/ avmTestContractClassSeed, + /*publicDispatchFunction=*/ { bytecode: avmTestContractBytecode, selector: DISPATCH_SELECTOR }, + ); + avmTestContractInstance = await makeContractInstanceFromClassId( + avmTestContractClass.id, + /*seed=*/ avmTestContractClassSeed, + ); + tester = await AvmProvingTester.create(/*checkCircuitOnly*/ false); + await tester.addContractClass(avmTestContractClass, AvmTestContractArtifact); + await tester.addContractInstance(avmTestContractInstance); + }); + + it( + 'bulk_testing v1', + async () => { + // Get a deployed contract instance to pass to the contract + // for it to use as "expected" values when testing contract instance retrieval. + const expectContractInstance = avmTestContractInstance; + const argsField = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)); + const argsU8 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)); + const args = [ + argsField, + argsU8, + /*getInstanceForAddress=*/ expectContractInstance.address.toField(), + /*expectedDeployer=*/ expectContractInstance.deployer.toField(), + /*expectedClassId=*/ expectContractInstance.contractClassId.toField(), + /*expectedInitializationHash=*/ expectContractInstance.initializationHash.toField(), + ]; + + await tester.simProveVerifyAppLogic({ address: avmTestContractInstance.address, fnName: 'bulk_testing', args }); + }, + TIMEOUT, + ); + }); + + describe('check circuit', () => { + const sender = AztecAddress.fromNumber(42); + const avmTestContractClassSeed = 0; + const avmTestContractBytecode = getAvmTestContractBytecode(DISPATCH_FN_NAME); + let avmTestContractClass: ContractClassPublic; + let avmTestContractInstance: ContractInstanceWithAddress; + let tester: AvmProvingTester; + + beforeEach(async () => { + avmTestContractClass = await makeContractClassPublic( + /*seed=*/ avmTestContractClassSeed, + /*publicDispatchFunction=*/ { bytecode: avmTestContractBytecode, selector: DISPATCH_SELECTOR }, + ); + avmTestContractInstance = await makeContractInstanceFromClassId( + avmTestContractClass.id, + /*seed=*/ avmTestContractClassSeed, + ); + tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + await tester.addContractClass(avmTestContractClass, AvmTestContractArtifact); + await tester.addContractInstance(avmTestContractInstance); + }); + + it( + 'perform too many storage writes and revert', + async () => { + await tester.simProveVerifyAppLogic( + { + address: avmTestContractInstance.address, + fnName: 'n_storage_writes', + args: [new Fr(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX + 1)], + }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + it( + 'create too many note hashes and revert', + async () => { + await tester.simProveVerifyAppLogic( + { + address: avmTestContractInstance.address, + fnName: 'n_new_note_hashes', + args: [new Fr(MAX_NOTE_HASHES_PER_TX + 1)], + }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + it( + 'create too many nullifiers and revert', + async () => { + await tester.simProveVerifyAppLogic( + { + address: avmTestContractInstance.address, + fnName: 'n_new_nullifiers', + args: [new Fr(MAX_NULLIFIERS_PER_TX + 1)], + }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + it( + 'create too many l2tol1 messages and revert', + async () => { + await tester.simProveVerifyAppLogic( + { + address: avmTestContractInstance.address, + fnName: 'n_new_l2_to_l1_msgs', + args: [new Fr(MAX_L2_TO_L1_MSGS_PER_TX + 1)], + }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + it( + 'create too many public logs and revert', + async () => { + await tester.simProveVerifyAppLogic( + { + address: avmTestContractInstance.address, + fnName: 'n_new_public_logs', + args: [new Fr(MAX_PUBLIC_LOGS_PER_TX + 1)], + }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + it( + 'top-level exceptional halts in both app logic and teardown', + async () => { + await tester.simProveVerify( + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [{ address: avmTestContractInstance.address, fnName: 'divide_by_zero', args: [] }], + /*teardownCall=*/ undefined, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + it( + 'top-level exceptional halt in app logic, but teardown succeeds', + async () => { + await tester.simProveVerify( + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [{ address: avmTestContractInstance.address, fnName: 'divide_by_zero', args: [] }], + /*teardownCall=*/ { + address: avmTestContractInstance.address, + fnName: 'add_args_return', + args: [new Fr(1), new Fr(2)], + }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + it( + 'top-level exceptional halt in teardown, but app logic succeeds', + async () => { + await tester.simProveVerify( + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { address: avmTestContractInstance.address, fnName: 'add_args_return', args: [new Fr(1), new Fr(2)] }, + ], + /*teardownCall=*/ { address: avmTestContractInstance.address, fnName: 'divide_by_zero', args: [] }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + it( + 'a nested exceptional halt propagate to top-level', + async () => { + await tester.simProveVerifyAppLogic( + { address: avmTestContractInstance.address, fnName: 'external_call_to_divide_by_zero', args: [] }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + it( + 'a nested exceptional halt is recovered from in caller', + async () => { + await tester.simProveVerifyAppLogic( + { address: avmTestContractInstance.address, fnName: 'external_call_to_divide_by_zero_recovers', args: [] }, + /*expectRevert=*/ false, + ); + }, + TIMEOUT, + ); + it( + 'an exceptional halt due to a nested call to non-existent contract is propagated to top-level', + async () => { + await tester.simProveVerifyAppLogic( + { address: avmTestContractInstance.address, fnName: 'nested_call_to_nothing', args: [] }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + it( + 'an exceptional halt due to a nested call to non-existent contract is recovered from in caller', + async () => { + await tester.simProveVerifyAppLogic( + { address: avmTestContractInstance.address, fnName: 'nested_call_to_nothing_recovers', args: [] }, + /*expectRevert=*/ false, + ); + }, + TIMEOUT, + ); + // FIXME(dbanks12): fails with "Lookup PERM_MAIN_ALU failed." + it.skip('top-level exceptional halts due to a non-existent contract in app-logic and teardown', async () => { + // don't insert contracts into trees, and make sure retrieval fails + const tester = await AvmProvingTester.create(/*checkCircuitOnly=*/ true, /*skipContractDeployments=*/ true); + await tester.simProveVerify( + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { address: avmTestContractInstance.address, fnName: 'add_args_return', args: [new Fr(1), new Fr(2)] }, + ], + /*teardownCall=*/ { + address: avmTestContractInstance.address, + fnName: 'add_args_return', + args: [new Fr(1), new Fr(2)], + }, + /*expectRevert=*/ true, + ); + }); + it( + 'enqueued calls in every phase, with enqueued calls that depend on each other', + async () => { + await tester.simProveVerify( + sender, + /*setupCalls=*/ [ + { address: avmTestContractInstance.address, fnName: 'read_assert_storage_single', args: [new Fr(0)] }, + { address: avmTestContractInstance.address, fnName: 'set_storage_single', args: [new Fr(5)] }, + ], + /*appCalls=*/ [ + { address: avmTestContractInstance.address, fnName: 'read_assert_storage_single', args: [new Fr(5)] }, + { address: avmTestContractInstance.address, fnName: 'set_storage_single', args: [new Fr(10)] }, + ], + /*teardownCall=*/ { + address: avmTestContractInstance.address, + fnName: 'read_assert_storage_single', + args: [new Fr(10)], + }, + /*expectRevert=*/ false, + ); + }, + TIMEOUT, + ); + it( + 'Should prove and verify a TX that reverts in teardown', + async () => { + await tester.simProveVerify( + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [], + /*teardownCall=*/ { + address: avmTestContractInstance.address, + fnName: 'read_assert_storage_single', + args: [new Fr(10)], + }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + }); + + describe('check circuit - contract class limits', () => { + const deployer = AztecAddress.fromNumber(42); + let instances: ContractInstanceWithAddress[]; + let tester: AvmProvingTester; + let avmTestContractAddress: AztecAddress; + + beforeEach(async () => { + tester = await AvmProvingTester.create(/*checkCircuitOnly=*/ true); + // create enough unique contract classes to hit the limit + instances = []; + for (let i = 0; i <= MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS; i++) { + const instance = await tester.registerAndDeployContract( + /*constructorArgs=*/ [], + deployer, + /*contractArtifact=*/ AvmTestContractArtifact, + /*seed=*/ i, + ); + instances.push(instance); + } + avmTestContractAddress = instances[0].address; + }); + it( + 'call the max number of unique contract classes', + async () => { + // args is initialized to MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS contract addresses with unique class IDs + const instanceAddresses = instances + .map(instance => instance.address) + .slice(0, MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS); + + // include the first contract again again at the end to ensure that we can call it even after the limit is reached + instanceAddresses.push(instanceAddresses[0]); + + // include another contract address that reuses a class ID to ensure that we can call it even after the limit is reached + const instanceSameClassAsFirstContract = await makeContractInstanceFromClassId( + instances[0].contractClassId, + /*seed=*/ 1000, + ); + instanceAddresses.push(instanceSameClassAsFirstContract.address); + // add it to the contract data source so it is found + await tester.addContractInstance(instanceSameClassAsFirstContract); + + await tester.simProveVerifyAppLogic( + { + address: avmTestContractAddress, + fnName: 'nested_call_to_add_n_times_different_addresses', + args: [instanceAddresses], + }, + /*expectRevert=*/ false, + ); + }, + TIMEOUT, + ); + it( + 'attempt too many calls to unique contract class ids', + async () => { + // args is initialized to MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS+1 contract addresses with unique class IDs + // should fail because we are trying to call MAX+1 unique class IDs + const instanceAddresses = instances.map(instance => instance.address); + // push an empty one (just padding to match function calldata size of MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS+2) + instanceAddresses.push(AztecAddress.zero()); + await tester.simProveVerifyAppLogic( + { + address: avmTestContractAddress, + fnName: 'nested_call_to_add_n_times_different_addresses', + args: [instanceAddresses], + }, + /*expectRevert=*/ true, + ); + }, + TIMEOUT, + ); + }); + + // Add instance & class for fee juice token contract + // Initialize balance of payer + // Include payer in TX + describe('public fee payment', () => { + const sender = AztecAddress.fromNumber(42); + const feePayer = sender; + + const initialFeeJuiceBalance = new Fr(10000); + let feeJuice: ProtocolContract; + let feeJuiceContractClassPublic: ContractClassPublic; + + const avmTestContractClassSeed = 0; + const avmTestContractBytecode = getAvmTestContractBytecode(DISPATCH_FN_NAME); + let avmTestContractClass: ContractClassPublic; + let avmTestContractInstance: ContractInstanceWithAddress; + let tester: AvmProvingTester; + + beforeEach(async () => { + feeJuice = await getCanonicalFeeJuice(); + feeJuiceContractClassPublic = { + ...feeJuice.contractClass, + privateFunctions: [], + unconstrainedFunctions: [], + }; + avmTestContractClass = await makeContractClassPublic( + /*seed=*/ avmTestContractClassSeed, + /*publicDispatchFunction=*/ { bytecode: avmTestContractBytecode, selector: DISPATCH_SELECTOR }, + ); + avmTestContractInstance = await makeContractInstanceFromClassId( + avmTestContractClass.id, + /*seed=*/ avmTestContractClassSeed, + ); + tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + await tester.addContractClass(feeJuiceContractClassPublic, FeeJuiceArtifact); + await tester.addContractInstance(feeJuice.instance); + await tester.addContractClass(avmTestContractClass, AvmTestContractArtifact); + await tester.addContractInstance(avmTestContractInstance); + await tester.setFeePayerBalance(feePayer, initialFeeJuiceBalance); + }); + it( + 'fee payment', + async () => { + await tester.simProveVerify( + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { address: avmTestContractInstance.address, fnName: 'add_args_return', args: [new Fr(1), new Fr(2)] }, + ], + /*teardownCall=*/ undefined, + /*expectRevert=*/ false, + feePayer, + ); + }, + TIMEOUT, + ); + }); +}); + +describe('AVM v2', () => { + const sender = AztecAddress.fromNumber(42); + const avmTestContractClassSeed = 0; + const avmTestContractBytecode = getAvmTestContractBytecode(DISPATCH_FN_NAME); + let avmTestContractClass: ContractClassPublic; + let avmTestContractInstance: ContractInstanceWithAddress; + + let tester: AvmProvingTesterV2; + + beforeEach(async () => { + avmTestContractClass = await makeContractClassPublic( + /*seed=*/ avmTestContractClassSeed, + /*publicDispatchFunction=*/ { bytecode: avmTestContractBytecode, selector: DISPATCH_SELECTOR }, + ); + avmTestContractInstance = await makeContractInstanceFromClassId( + avmTestContractClass.id, + /*seed=*/ avmTestContractClassSeed, + ); + tester = await AvmProvingTesterV2.create(); + await tester.addContractClass(avmTestContractClass, AvmTestContractArtifact); + await tester.addContractInstance(avmTestContractInstance); + }); + + it('bulk_testing v2', async () => { + // Get a deployed contract instance to pass to the contract + // for it to use as "expected" values when testing contract instance retrieval. + const expectContractInstance = avmTestContractInstance; + const argsField = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)); + const argsU8 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)); + const args = [ + argsField, + argsU8, + /*getInstanceForAddress=*/ expectContractInstance.address.toField(), + /*expectedDeployer=*/ expectContractInstance.deployer.toField(), + /*expectedClassId=*/ expectContractInstance.contractClassId.toField(), + /*expectedInitializationHash=*/ expectContractInstance.initializationHash.toField(), + ]; + + await tester.simProveVerifyV2( + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [{ address: avmTestContractInstance.address, fnName: 'bulk_testing', args }], + /*teardownCall=*/ undefined, + /*expectRevert=*/ false, + ); + }, 180_000); +}); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts new file mode 100644 index 000000000000..6e4b3fb181d5 --- /dev/null +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts @@ -0,0 +1,175 @@ +import { type MerkleTreeWriteOperations } from '@aztec/circuit-types'; +import { type AvmCircuitInputs, AztecAddress, VerificationKeyData } from '@aztec/circuits.js'; +import { openTmpStore } from '@aztec/kv-store/lmdb'; +import { PublicTxSimulationTester, type TestEnqueuedCall } from '@aztec/simulator/public/fixtures'; +import { MerkleTrees } from '@aztec/world-state'; + +import fs from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'path'; + +import { SimpleContractDataSource } from '../../../simulator/src/avm/fixtures/simple_contract_data_source.js'; +import { + type BBResult, + type BBSuccess, + BB_RESULT, + generateAvmProof, + generateAvmProofV2, + verifyAvmProof, + verifyAvmProofV2, +} from '../bb/execute.js'; +import { extractAvmVkData } from '../verification_key/verification_key_data.js'; + +const BB_PATH = path.resolve('../../barretenberg/cpp/build/bin/bb'); + +export class AvmProvingTester extends PublicTxSimulationTester { + constructor( + private bbWorkingDirectory: string, + private checkCircuitOnly: boolean, + contractDataSource: SimpleContractDataSource, + merkleTrees: MerkleTreeWriteOperations, + skipContractDeployments: boolean, + ) { + super(contractDataSource, merkleTrees, skipContractDeployments); + } + + static override async create(checkCircuitOnly: boolean = false, skipContractDeployments: boolean = false) { + const bbWorkingDirectory = await fs.mkdtemp(path.join(tmpdir(), 'bb-')); + + const contractDataSource = new SimpleContractDataSource(); + const merkleTrees = await (await MerkleTrees.new(openTmpStore())).fork(); + return new AvmProvingTester( + bbWorkingDirectory, + checkCircuitOnly, + contractDataSource, + merkleTrees, + skipContractDeployments, + ); + } + + async prove(avmCircuitInputs: AvmCircuitInputs): Promise { + // Then we prove. + const proofRes = await generateAvmProof( + BB_PATH, + this.bbWorkingDirectory, + avmCircuitInputs, + this.logger, + this.checkCircuitOnly, + ); + if (proofRes.status === BB_RESULT.FAILURE) { + this.logger.error(`Proof generation failed: ${proofRes.reason}`); + } + return proofRes; + } + + async verify(proofRes: BBSuccess): Promise { + if (this.checkCircuitOnly) { + // Skip verification if we're only checking the circuit. + // Check-circuit doesn't generate a proof to verify. + return proofRes; + } + // Then we test VK extraction and serialization. + const succeededRes = proofRes as BBSuccess; + const vkData = await extractAvmVkData(succeededRes.vkPath!); + VerificationKeyData.fromBuffer(vkData.toBuffer()); + + // Then we verify. + const rawVkPath = path.join(succeededRes.vkPath!, 'vk'); + return await verifyAvmProof(BB_PATH, succeededRes.proofPath!, rawVkPath, this.logger); + } + + public async simProveVerify( + sender: AztecAddress, + setupCalls: TestEnqueuedCall[], + appCalls: TestEnqueuedCall[], + teardownCall: TestEnqueuedCall | undefined, + expectRevert: boolean | undefined, + feePayer?: AztecAddress, + ) { + const simRes = await this.simulateTx(sender, setupCalls, appCalls, teardownCall, feePayer); + expect(simRes.revertCode.isOK()).toBe(expectRevert ? false : true); + const avmCircuitInputs = simRes.avmProvingRequest.inputs; + const provingRes = await this.prove(avmCircuitInputs); + expect(provingRes.status).toEqual(BB_RESULT.SUCCESS); + const verificationRes = await this.verify(provingRes as BBSuccess); + expect(verificationRes.status).toBe(BB_RESULT.SUCCESS); + } + + public async simProveVerifyAppLogic(appCall: TestEnqueuedCall, expectRevert?: boolean) { + const simRes = await this.simulateTx(/*sender=*/ AztecAddress.fromNumber(42), /*setupCalls=*/ [], [appCall]); + expect(simRes.revertCode.isOK()).toBe(expectRevert ? false : true); + + const avmCircuitInputs = simRes.avmProvingRequest.inputs; + const provingRes = await this.prove(avmCircuitInputs); + expect(provingRes.status).toEqual(BB_RESULT.SUCCESS); + + const verificationRes = await this.verify(provingRes as BBSuccess); + expect(verificationRes.status).toBe(BB_RESULT.SUCCESS); + } +} + +export class AvmProvingTesterV2 extends PublicTxSimulationTester { + constructor( + private bbWorkingDirectory: string, + contractDataSource: SimpleContractDataSource, + merkleTrees: MerkleTreeWriteOperations, + skipContractDeployments: boolean, + ) { + super(contractDataSource, merkleTrees, skipContractDeployments); + } + + static override async create(skipContractDeployments: boolean = false) { + const bbWorkingDirectory = await fs.mkdtemp(path.join(tmpdir(), 'bb-')); + + const contractDataSource = new SimpleContractDataSource(); + const merkleTrees = await (await MerkleTrees.new(openTmpStore())).fork(); + return new AvmProvingTesterV2(bbWorkingDirectory, contractDataSource, merkleTrees, skipContractDeployments); + } + + async proveV2(avmCircuitInputs: AvmCircuitInputs): Promise { + // Then we prove. + const proofRes = await generateAvmProofV2(BB_PATH, this.bbWorkingDirectory, avmCircuitInputs, this.logger); + if (proofRes.status === BB_RESULT.FAILURE) { + this.logger.error(`Proof generation failed: ${proofRes.reason}`); + } + expect(proofRes.status).toEqual(BB_RESULT.SUCCESS); + return proofRes as BBSuccess; + } + + async verifyV2(proofRes: BBSuccess): Promise { + // Then we verify. + // Placeholder for now. + const publicInputs = { + dummy: [] as any[], + }; + + const rawVkPath = path.join(proofRes.vkPath!, 'vk'); + return await verifyAvmProofV2( + BB_PATH, + this.bbWorkingDirectory, + proofRes.proofPath!, + publicInputs, + rawVkPath, + this.logger, + ); + } + + public async simProveVerifyV2( + sender: AztecAddress, + setupCalls: TestEnqueuedCall[], + appCalls: TestEnqueuedCall[], + teardownCall: TestEnqueuedCall | undefined, + expectRevert: boolean | undefined, + feePayer?: AztecAddress, + ) { + const simRes = await this.simulateTx(sender, setupCalls, appCalls, teardownCall, feePayer); + expect(simRes.revertCode.isOK()).toBe(expectRevert ? false : true); + + const avmCircuitInputs = simRes.avmProvingRequest.inputs; + const provingRes = await this.proveV2(avmCircuitInputs); + expect(provingRes.status).toEqual(BB_RESULT.SUCCESS); + + const verificationRes = await this.verifyV2(provingRes as BBSuccess); + expect(verificationRes.status).toBe(BB_RESULT.SUCCESS); + } +} diff --git a/yarn-project/bb-prover/tsconfig.json b/yarn-project/bb-prover/tsconfig.json index 4936e927765f..f5fca8a14377 100644 --- a/yarn-project/bb-prover/tsconfig.json +++ b/yarn-project/bb-prover/tsconfig.json @@ -33,6 +33,12 @@ { "path": "../kv-store" }, + { + "path": "../noir-contracts.js" + }, + { + "path": "../protocol-contracts" + }, { "path": "../types" } diff --git a/yarn-project/circuits.js/src/tests/factories.ts b/yarn-project/circuits.js/src/tests/factories.ts index 5e84a47a468b..bfb4bd5aa867 100644 --- a/yarn-project/circuits.js/src/tests/factories.ts +++ b/yarn-project/circuits.js/src/tests/factories.ts @@ -1327,11 +1327,19 @@ export async function makeMapAsync( return new Map(await makeArrayAsync(size, i => fn(i + offset))); } -export async function makeContractInstanceFromClassId(classId: Fr, seed = 0): Promise { +export async function makeContractInstanceFromClassId( + classId: Fr, + seed = 0, + overrides?: { + deployer?: AztecAddress; + initializationHash?: Fr; + publicKeys?: PublicKeys; + }, +): Promise { const salt = new Fr(seed); - const initializationHash = new Fr(seed + 1); - const deployer = new AztecAddress(new Fr(seed + 2)); - const publicKeys = await PublicKeys.random(); + const initializationHash = overrides?.initializationHash ?? new Fr(seed + 1); + const deployer = overrides?.deployer ?? new AztecAddress(new Fr(seed + 2)); + const publicKeys = overrides?.publicKeys ?? (await PublicKeys.random()); const saltedInitializationHash = await poseidon2HashWithSeparator( [salt, initializationHash, deployer], diff --git a/yarn-project/ivc-integration/package.json b/yarn-project/ivc-integration/package.json index ed615b1ac099..390b0e5cdc08 100644 --- a/yarn-project/ivc-integration/package.json +++ b/yarn-project/ivc-integration/package.json @@ -78,6 +78,7 @@ "devDependencies": { "@aztec/bb-prover": "workspace:^", "@aztec/kv-store": "workspace:^", + "@aztec/noir-contracts.js": "workspace:^", "@aztec/simulator": "workspace:^", "@aztec/telemetry-client": "workspace:^", "@aztec/world-state": "workspace:^", diff --git a/yarn-project/ivc-integration/src/avm_integration.test.ts b/yarn-project/ivc-integration/src/avm_integration.test.ts index 067ea935bca4..31824adaf248 100644 --- a/yarn-project/ivc-integration/src/avm_integration.test.ts +++ b/yarn-project/ivc-integration/src/avm_integration.test.ts @@ -1,16 +1,26 @@ import { type BBSuccess, BB_RESULT, generateAvmProof, generateProof, verifyProof } from '@aztec/bb-prover'; +import { + type AvmCircuitInputs, + AztecAddress, + type ContractClassPublic, + type ContractInstanceWithAddress, + FunctionSelector, +} from '@aztec/circuits.js'; import { AVM_PROOF_LENGTH_IN_FIELDS, AVM_PUBLIC_COLUMN_MAX_SIZE, AVM_PUBLIC_INPUTS_FLATTENED_SIZE, AVM_VERIFICATION_KEY_LENGTH_IN_FIELDS, PUBLIC_CIRCUIT_PUBLIC_INPUTS_LENGTH, + PUBLIC_DISPATCH_SELECTOR, } from '@aztec/circuits.js/constants'; +import { makeContractClassPublic, makeContractInstanceFromClassId } from '@aztec/circuits.js/testing'; import { Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; import { BufferReader } from '@aztec/foundation/serialize'; +import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; import { type FixedLengthArray } from '@aztec/noir-protocol-circuits-types/types'; -import { simulateAvmTestContractGenerateCircuitInputs } from '@aztec/simulator/public/fixtures'; +import { PublicTxSimulationTester, getAvmTestContractPublicDispatchBytecode } from '@aztec/simulator/public/fixtures'; import { promises as fs } from 'fs'; import { tmpdir } from 'node:os'; @@ -22,16 +32,42 @@ import { MockPublicBaseCircuit, witnessGenMockPublicBaseCircuit } from './index. // Auto-generated types from noir are not in camel case. /* eslint-disable camelcase */ + const logger = createLogger('ivc-integration:test:avm-integration'); describe('AVM Integration', () => { let bbWorkingDirectory: string; let bbBinaryPath: string; + const avmTestContractClassSeed = 0; + const avmTestContractBytecode = getAvmTestContractPublicDispatchBytecode(); + let avmTestContractClass: ContractClassPublic; + let avmTestContractInstance: ContractInstanceWithAddress; + let avmTestContractAddress: AztecAddress; + + let simTester: PublicTxSimulationTester; + beforeEach(async () => { //Create a temp working dir bbWorkingDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'bb-avm-integration-')); bbBinaryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../barretenberg/cpp/build/bin', 'bb'); + + avmTestContractClass = await makeContractClassPublic( + /*seed=*/ avmTestContractClassSeed, + /*publicDispatchFunction=*/ { + bytecode: avmTestContractBytecode, + selector: new FunctionSelector(PUBLIC_DISPATCH_SELECTOR), + }, + ); + avmTestContractInstance = await makeContractInstanceFromClassId( + avmTestContractClass.id, + /*seed=*/ avmTestContractClassSeed, + ); + avmTestContractAddress = avmTestContractInstance.address; + + simTester = await PublicTxSimulationTester.create(); + await simTester.addContractClass(avmTestContractClass, AvmTestContractArtifact); + await simTester.addContractInstance(avmTestContractInstance); }); async function createHonkProof(witness: Uint8Array, bytecode: string): Promise { @@ -55,10 +91,26 @@ describe('AVM Integration', () => { // TODO: Skipping for now as per Davids advice. it.skip('Should generate and verify an ultra honk proof from an AVM verification', async () => { - const bbSuccess = await proveAvmTestContract( - 'bulk_testing', - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)), + // Get a deployed contract instance to pass to the contract + // for it to use as "expected" values when testing contract instance retrieval. + const expectContractInstance = avmTestContractInstance; + const argsField = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const argsU8 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const args = [ + ...argsField, + ...argsU8, + /*getInstanceForAddress=*/ expectContractInstance.address.toField(), + /*expectedDeployer=*/ expectContractInstance.deployer.toField(), + /*expectedClassId=*/ expectContractInstance.contractClassId.toField(), + /*expectedInitializationHash=*/ expectContractInstance.initializationHash.toField(), + ].map(x => new Fr(x)); + const simRes = await simTester.simulateTx( + /*sender=*/ AztecAddress.fromNumber(42), + /*setupCalls=*/ [], + /*appCalls=*/ [{ address: avmTestContractAddress, fnName: 'bulk_testing', args }], ); + const avmCircuitInputs = simRes.avmProvingRequest.inputs; + const bbSuccess = await proveAvm(avmCircuitInputs); const avmProofPath = bbSuccess.proofPath; const avmVkPath = bbSuccess.vkPath; @@ -120,17 +172,7 @@ describe('AVM Integration', () => { }, 240_000); }); -async function proveAvmTestContract(functionName: string, calldata: Fr[] = []): Promise { - const avmCircuitInputs = await simulateAvmTestContractGenerateCircuitInputs( - /*setupFunctionNames=*/ [], - /*setupArgs=*/ [], - /*appFunctionNames=*/ [functionName], - /*appArgs=*/ [calldata], - /*teardownFunctionName=*/ undefined, - /*teardownArgs=*/ [], - /*expectRevert=*/ false, - ); - +async function proveAvm(avmCircuitInputs: AvmCircuitInputs): Promise { const internalLogger = createLogger('ivc-integration:test:avm-proving'); // The paths for the barretenberg binary and the write path are hardcoded for now. diff --git a/yarn-project/ivc-integration/tsconfig.json b/yarn-project/ivc-integration/tsconfig.json index a355b926142a..9824741f2bbe 100644 --- a/yarn-project/ivc-integration/tsconfig.json +++ b/yarn-project/ivc-integration/tsconfig.json @@ -22,6 +22,9 @@ { "path": "../kv-store" }, + { + "path": "../noir-contracts.js" + }, { "path": "../simulator" }, diff --git a/yarn-project/simulator/src/avm/apps_tests/avm_test.test.ts b/yarn-project/simulator/src/avm/apps_tests/avm_test.test.ts new file mode 100644 index 000000000000..1d37b14c4185 --- /dev/null +++ b/yarn-project/simulator/src/avm/apps_tests/avm_test.test.ts @@ -0,0 +1,103 @@ +import { + AztecAddress, + type ContractInstanceWithAddress, + MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS, +} from '@aztec/circuits.js'; +import { makeContractInstanceFromClassId } from '@aztec/circuits.js/testing'; +import { Fr } from '@aztec/foundation/fields'; +import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; + +import { AvmSimulationTester } from '../fixtures/avm_simulation_tester.js'; + +describe('AVM simulator apps tests: AvmTestContract', () => { + const deployer = AztecAddress.fromNumber(42); + const sender = AztecAddress.fromNumber(4200); + let testContractAddress: AztecAddress; + let instances: ContractInstanceWithAddress[]; + let simTester: AvmSimulationTester; + + beforeEach(async () => { + simTester = await AvmSimulationTester.create(); + // create enough unique contract classes to hit the limit + instances = []; + for (let i = 0; i <= MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS; i++) { + const instance = await simTester.registerAndDeployContract( + /*constructorArgs=*/ [], + deployer, + /*contractArtifact=*/ AvmTestContractArtifact, + /*seed=*/ i, + ); + instances.push(instance); + } + testContractAddress = instances[0].address; + }); + + it('bulk testing', async () => { + // Get a deployed contract instance to pass to the contract + // for it to use as "expected" values when testing contract instance retrieval. + const expectContractInstance = instances[1]; + const argsField = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)); + const argsU8 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)); + const args = [ + argsField, + argsU8, + /*getInstanceForAddress=*/ expectContractInstance.address, + /*expectedDeployer=*/ expectContractInstance.deployer, + /*expectedClassId=*/ expectContractInstance.contractClassId, + /*expectedInitializationHash=*/ expectContractInstance.initializationHash, + ]; + const results = await simTester.simulateCall(sender, /*address=*/ testContractAddress, 'bulk_testing', args); + expect(results.reverted).toBe(false); + }); + + it('call max unique contract classes', async () => { + // args is initialized to MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS contract addresses with unique class IDs + const instanceAddresses = instances + .map(instance => instance.address) + .slice(0, MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS); + + // include the first contract again again at the end to ensure that we can call it even after the limit is reached + instanceAddresses.push(instanceAddresses[0]); + + // include another contract address that reuses a class ID to ensure that we can call it even after the limit is reached + const instanceSameClassAsFirstContract = await makeContractInstanceFromClassId( + instances[0].contractClassId, + /*seed=*/ 1000, + ); + instanceAddresses.push(instanceSameClassAsFirstContract.address); + // add it to the contract data source so it is found + await simTester.addContractInstance(instanceSameClassAsFirstContract); + + const results = await simTester.simulateCall( + sender, + /*address=*/ testContractAddress, + 'nested_call_to_add_n_times_different_addresses', + /*args=*/ [instanceAddresses], + ); + expect(results.reverted).toBe(false); + }); + + it('call too many unique contract classes fails', async () => { + // args is initialized to MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS+1 contract addresses with unique class IDs + // should fail because we are trying to call MAX+1 unique class IDs + const instanceAddresses = instances.map(instance => instance.address.toField()); + // push an empty one (just padding to match function calldata size of MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS+2) + instanceAddresses.push(new Fr(0)); + const results = await simTester.simulateCall( + sender, + /*address=*/ testContractAddress, + 'nested_call_to_add_n_times_different_addresses', + /*args=*/ [instanceAddresses], + ); + expect(results.reverted).toBe(true); + }); + + it('an exceptional halt due to a nested call to non-existent contract is recovered from in caller', async () => { + await simTester.simulateCall( + sender, + /*address=*/ testContractAddress, + 'nested_call_to_nothing_recovers', + /*args=*/ [], + ); + }); +}); diff --git a/yarn-project/simulator/src/avm/apps_tests/token.test.ts b/yarn-project/simulator/src/avm/apps_tests/token.test.ts new file mode 100644 index 000000000000..9f0966ccf7a8 --- /dev/null +++ b/yarn-project/simulator/src/avm/apps_tests/token.test.ts @@ -0,0 +1,75 @@ +import { AztecAddress, type ContractInstanceWithAddress, Fr } from '@aztec/circuits.js'; +import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; + +import { AvmSimulationTester } from '../fixtures/avm_simulation_tester.js'; + +describe('AVM simulator apps tests: TokenContract', () => { + const admin = AztecAddress.fromNumber(42); + const sender = AztecAddress.fromNumber(111); + const receiver = AztecAddress.fromNumber(222); + + let token: ContractInstanceWithAddress; + let simTester: AvmSimulationTester; + + beforeEach(async () => { + simTester = await AvmSimulationTester.create(); + const constructorArgs = [admin, /*name=*/ 'Token', /*symbol=*/ 'TOK', /*decimals=*/ new Fr(18)]; + token = await simTester.registerAndDeployContract(constructorArgs, /*deployer=*/ admin, TokenContractArtifact); + + const constructorResult = await simTester.simulateCall( + /*sender=*/ admin, + token.address, + 'constructor', + constructorArgs, + ); + expect(constructorResult.reverted).toBe(false); + }); + + it('token mint, transfer, burn (and check balances)', async () => { + const mintAmount = 100n; + const transferAmount = 50n; + const nonce = new Fr(0); + + await checkBalance(sender, 0n); + + const mintResult = await simTester.simulateCall( + /*sender=*/ admin, + token.address, + 'mint_to_public', + /*args=*/ [/*to=*/ sender, mintAmount], + ); + expect(mintResult.reverted).toBe(false); + await checkBalance(sender, mintAmount); + + const transferResult = await simTester.simulateCall( + /*sender=*/ sender, + token.address, + 'transfer_in_public', + /*args=*/ [/*from=*/ sender, /*to=*/ receiver, transferAmount, nonce], + ); + expect(transferResult.reverted).toBe(false); + await checkBalance(sender, mintAmount - transferAmount); + await checkBalance(receiver, transferAmount); + + const burnResult = await simTester.simulateCall( + /*sender=*/ receiver, + token.address, + 'burn_public', + /*args=*/ [/*from=*/ receiver, transferAmount, nonce], + ); + expect(burnResult.reverted).toBe(false); + await checkBalance(receiver, 0n); + }); + + const checkBalance = async (account: AztecAddress, expectedBalance: bigint) => { + const balResult = await simTester.simulateCall( + sender, + token.address, + 'balance_of_public', + /*args=*/ [/*owner=*/ account], + /*isStaticCall=*/ true, + ); + expect(balResult.reverted).toBe(false); + expect(balResult.output).toEqual([new Fr(expectedBalance), Fr.zero()]); + }; +}); diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index 90287aa79dfc..84b86c73eb83 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -2,7 +2,6 @@ import { MerkleTreeId, type MerkleTreeWriteOperations } from '@aztec/circuit-typ import { DEPLOYER_CONTRACT_ADDRESS, GasFees, - MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS, PublicDataTreeLeafPreimage, PublicKeys, SerializableContractInstance, @@ -30,7 +29,6 @@ import { randomInt } from 'crypto'; import { mock } from 'jest-mock-extended'; import { PublicEnqueuedCallSideEffectTrace } from '../public/enqueued_call_side_effect_trace.js'; -import { MockedAvmTestContractDataSource, simulateAvmTestContractCall } from '../public/fixtures/index.js'; import { type WorldStateDB } from '../public/public_db_sources.js'; import { type PublicSideEffectTraceInterface } from '../public/side_effect_trace_interface.js'; import { type AvmContext } from './avm_context.js'; @@ -149,44 +147,6 @@ describe('AVM simulator: injected bytecode', () => { }); describe('AVM simulator: transpiled Noir contracts', () => { - it('bulk testing', async () => { - const args = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)); - await simulateAvmTestContractCall('bulk_testing', args, /*expectRevert=*/ false); - }); - - it('call max unique contract classes', async () => { - const contractDataSource = await MockedAvmTestContractDataSource.create(); - // args is initialized to MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS contract addresses with unique class IDs - const args = Array.from(contractDataSource.contractInstances.values()) - .map(instance => instance.address.toField()) - .slice(0, MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS); - // include the first contract again again at the end to ensure that we can call it even after the limit is reached - args.push(args[0]); - // include another contract address that reuses a class ID to ensure that we can call it even after the limit is reached - args.push(contractDataSource.instanceSameClassAsFirstContract.address.toField()); - await simulateAvmTestContractCall( - 'nested_call_to_add_n_times_different_addresses', - args, - /*expectRevert=*/ false, - contractDataSource, - ); - }); - - it('call too many unique contract classes fails', async () => { - const contractDataSource = await MockedAvmTestContractDataSource.create(); - // args is initialized to MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS+1 contract addresses with unique class IDs - // should fail because we are trying to call MAX+1 unique class IDs - const args = Array.from(contractDataSource.contractInstances.values()).map(instance => instance.address.toField()); - // push an empty one (just padding to match function calldata size of MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS+2) - args.push(new Fr(0)); - await simulateAvmTestContractCall( - 'nested_call_to_add_n_times_different_addresses', - args, - /*expectRevert=*/ true, - contractDataSource, - ); - }); - it('execution of a non-existent contract immediately reverts and consumes all allocated gas', async () => { const context = initContext(); const results = await new AvmSimulator(context).execute(); diff --git a/yarn-project/simulator/src/avm/avm_simulator.ts b/yarn-project/simulator/src/avm/avm_simulator.ts index 3549706283e5..48bc92beb152 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.ts @@ -28,20 +28,14 @@ type OpcodeTally = { count: number; gas: Gas; }; -type PcTally = { - opcode: string; - count: number; - gas: Gas; -}; export class AvmSimulator { private log: Logger; private bytecode: Buffer | undefined; private opcodeTallies: Map = new Map(); - private pcTallies: Map = new Map(); private tallyPrintFunction = () => {}; - private tallyInstructionFunction = (_a: number, _b: string, _c: Gas) => {}; + private tallyInstructionFunction = (_b: string, _c: Gas) => {}; // Test Purposes only: Logger will not have the proper function name. Use this constructor for testing purposes // only. Otherwise, use build() below. @@ -145,7 +139,6 @@ export class AvmSimulator { while (!machineState.getHalted()) { const [instruction, bytesRead] = decodeInstructionFromBytecode(bytecode, machineState.pc, this.instructionSet); const instrStartGas = machineState.gasLeft; // Save gas before executing instruction (for profiling) - const instrPc = machineState.pc; // Save PC before executing instruction (for profiling) this.log.trace( `[PC:${machineState.pc}] [IC:${instrCounter++}] ${instruction.toString()} (gasLeft l2=${ @@ -168,7 +161,7 @@ export class AvmSimulator { l2Gas: instrStartGas.l2Gas - machineState.l2GasLeft, daGas: instrStartGas.daGas - machineState.daGasLeft, }; - this.tallyInstructionFunction(instrPc, instruction.constructor.name, gasUsed); + this.tallyInstructionFunction(instruction.constructor.name, gasUsed); if (machineState.pc >= bytecode.length) { this.log.warn('Passed end of program'); @@ -239,18 +232,12 @@ export class AvmSimulator { ); } - private tallyInstruction(pc: number, opcode: string, gasUsed: Gas) { + private tallyInstruction(opcode: string, gasUsed: Gas) { const opcodeTally = this.opcodeTallies.get(opcode) || ({ count: 0, gas: { l2Gas: 0, daGas: 0 } } as OpcodeTally); opcodeTally.count++; opcodeTally.gas.l2Gas += gasUsed.l2Gas; opcodeTally.gas.daGas += gasUsed.daGas; this.opcodeTallies.set(opcode, opcodeTally); - - const pcTally = this.pcTallies.get(pc) || ({ opcode: opcode, count: 0, gas: { l2Gas: 0, daGas: 0 } } as PcTally); - pcTally.count++; - pcTally.gas.l2Gas += gasUsed.l2Gas; - pcTally.gas.daGas += gasUsed.daGas; - this.pcTallies.set(pc, pcTally); } private printOpcodeTallies() { @@ -261,16 +248,5 @@ export class AvmSimulator { // NOTE: don't care to clutter the logs with DA gas for now this.log.debug(`${opcode} executed ${tally.count} times consuming a total of ${tally.gas.l2Gas} L2 gas`); } - - this.log.debug(`Printing tallies per PC sorted by #times each PC was executed...`); - const sortedPcs = Array.from(this.pcTallies.entries()) - .sort((a, b) => b[1].count - a[1].count) - .filter((_, i) => i < 20); - for (const [pc, tally] of sortedPcs) { - // NOTE: don't care to clutter the logs with DA gas for now - this.log.debug( - `PC:${pc} containing opcode ${tally.opcode} executed ${tally.count} times consuming a total of ${tally.gas.l2Gas} L2 gas`, - ); - } } } diff --git a/yarn-project/simulator/src/avm/fixtures/avm_simulation_tester.ts b/yarn-project/simulator/src/avm/fixtures/avm_simulation_tester.ts new file mode 100644 index 000000000000..cad15629c749 --- /dev/null +++ b/yarn-project/simulator/src/avm/fixtures/avm_simulation_tester.ts @@ -0,0 +1,105 @@ +import { type MerkleTreeWriteOperations } from '@aztec/circuit-types'; +import { GasFees, GlobalVariables } from '@aztec/circuits.js'; +import { encodeArguments } from '@aztec/foundation/abi'; +import { type AztecAddress } from '@aztec/foundation/aztec-address'; +import { Fr } from '@aztec/foundation/fields'; +import { openTmpStore } from '@aztec/kv-store/lmdb'; +import { MerkleTrees } from '@aztec/world-state'; + +import { type AvmContractCallResult } from '../../avm/avm_contract_call_result.js'; +import { + getContractFunctionArtifact, + getFunctionSelector, + initContext, + initExecutionEnvironment, + resolveContractAssertionMessage, +} from '../../avm/fixtures/index.js'; +import { PublicEnqueuedCallSideEffectTrace } from '../../public/enqueued_call_side_effect_trace.js'; +import { WorldStateDB } from '../../public/public_db_sources.js'; +import { AvmPersistableStateManager, AvmSimulator } from '../../server.js'; +import { BaseAvmSimulationTester } from './base_avm_simulation_tester.js'; +import { SimpleContractDataSource } from './simple_contract_data_source.js'; + +const TIMESTAMP = new Fr(99833); +const DEFAULT_GAS_FEES = new GasFees(2, 3); + +/** + * A test class that extends the BaseAvmSimulationTester to enable real-app testing of the core AvmSimulator. + * It provides an interface for simulating one top-level call at a time and maintains state between + * subsequent top-level calls. + */ +export class AvmSimulationTester extends BaseAvmSimulationTester { + constructor( + contractDataSource: SimpleContractDataSource, + merkleTrees: MerkleTreeWriteOperations, + skipContractDeployments = false, + private stateManager: AvmPersistableStateManager, + ) { + super(contractDataSource, merkleTrees, skipContractDeployments); + } + + static async create(skipContractDeployments = false): Promise { + const contractDataSource = new SimpleContractDataSource(); + const merkleTrees = await (await MerkleTrees.new(openTmpStore())).fork(); + const worldStateDB = new WorldStateDB(merkleTrees, contractDataSource); + const trace = new PublicEnqueuedCallSideEffectTrace(); + const firstNullifier = new Fr(420000); + // FIXME: merkle ops should work, but I'm seeing frequent (but inconsistent) bytecode retrieval + // failures on 2nd call to simulateEnqueuedCall with merkle ops on + const stateManager = await AvmPersistableStateManager.create( + worldStateDB, + trace, + /*doMerkleOperations=*/ false, + firstNullifier, + ); + return new AvmSimulationTester(contractDataSource, merkleTrees, skipContractDeployments, stateManager); + } + + /** + * Simulate a top-level contract call. + */ + async simulateCall( + sender: AztecAddress, + address: AztecAddress, + fnName: string, + args: any[], + isStaticCall = false, + ): Promise { + const contractArtifact = await this.contractDataSource.getContractArtifact(address); + if (!contractArtifact) { + throw new Error(`Contract not found at address: ${address}`); + } + const fnSelector = await getFunctionSelector(fnName, contractArtifact); + const fnAbi = getContractFunctionArtifact(fnName, contractArtifact); + const encodedArgs = encodeArguments(fnAbi!, args); + const calldata = [fnSelector.toField(), ...encodedArgs]; + + const globals = GlobalVariables.empty(); + globals.timestamp = TIMESTAMP; + globals.gasFees = DEFAULT_GAS_FEES; + + const environment = initExecutionEnvironment({ + calldata, + globals, + address, + sender, + isStaticCall, + }); + const persistableState = this.stateManager.fork(); + const context = initContext({ env: environment, persistableState }); + + // First we simulate (though it's not needed in this simple case). + const simulator = new AvmSimulator(context); + const result = await simulator.execute(); + if (result.reverted) { + this.logger.error(`Error in ${fnName}:`); + this.logger.error( + resolveContractAssertionMessage(fnName, result.revertReason!, result.output, contractArtifact)!, + ); + } else { + this.logger.info(`Simulation of function ${fnName} succeeded!`); + this.stateManager.merge(persistableState); + } + return result; + } +} diff --git a/yarn-project/simulator/src/avm/fixtures/base_avm_simulation_tester.ts b/yarn-project/simulator/src/avm/fixtures/base_avm_simulation_tester.ts new file mode 100644 index 000000000000..b8a9eb6c6ef7 --- /dev/null +++ b/yarn-project/simulator/src/avm/fixtures/base_avm_simulation_tester.ts @@ -0,0 +1,104 @@ +import { MerkleTreeId, type MerkleTreeWriteOperations } from '@aztec/circuit-types'; +import { + type ContractClassPublic, + type ContractInstanceWithAddress, + DEPLOYER_CONTRACT_ADDRESS, + PUBLIC_DISPATCH_SELECTOR, + PublicDataWrite, + computeInitializationHash, +} from '@aztec/circuits.js'; +import { computePublicDataTreeLeafSlot, siloNullifier } from '@aztec/circuits.js/hash'; +import { makeContractClassPublic, makeContractInstanceFromClassId } from '@aztec/circuits.js/testing'; +import { type ContractArtifact, FunctionSelector } from '@aztec/foundation/abi'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { type Fr } from '@aztec/foundation/fields'; +import { createLogger } from '@aztec/foundation/log'; +import { ProtocolContractAddress } from '@aztec/protocol-contracts'; + +import { computeFeePayerBalanceStorageSlot } from '../../server.js'; +import { PUBLIC_DISPATCH_FN_NAME, getContractFunctionArtifact } from './index.js'; +import { type SimpleContractDataSource } from './simple_contract_data_source.js'; + +/** + * An abstract test class that enables tests of real apps in the AVM without requiring e2e tests. + * It enables this by letting us (1) perform pseudo-contract-deployments (and registrations) + * that trigger merkle tree operations and (2) maintain a contract data source to store + * and retrieve contract classes and instances. + * + * This class is meant to be extended when writing tests for a specific simulation interface. + * For example, has been extended for testing of the core AvmSimulator, and again for the PublicTxSimulator, + * both of which benefit from such pseudo-deployments by populating merkle trees and a contract data source + * with contract information. + */ +export abstract class BaseAvmSimulationTester { + public logger = createLogger('avm-simulation-tester'); + + constructor( + public contractDataSource: SimpleContractDataSource, + public merkleTrees: MerkleTreeWriteOperations, + /* May want to skip contract deployment tree ops to test failed contract address nullifier checks on CALL */ + private skipContractDeployments = false, + ) {} + + async setFeePayerBalance(feePayer: AztecAddress, balance: Fr) { + const feeJuiceAddress = ProtocolContractAddress.FeeJuice; + const balanceSlot = await computeFeePayerBalanceStorageSlot(feePayer); + await this.setPublicStorage(feeJuiceAddress, balanceSlot, balance); + } + + async setPublicStorage(address: AztecAddress, slot: Fr, value: Fr) { + const leafSlot = await computePublicDataTreeLeafSlot(address, slot); + // get existing preimage + const publicDataWrite = new PublicDataWrite(leafSlot, value); + await this.merkleTrees.batchInsert(MerkleTreeId.PUBLIC_DATA_TREE, [publicDataWrite.toBuffer()], 0); + } + + /** + * 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, + contractArtifact: ContractArtifact, + seed = 0, + ): 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); + const contractInstance = await makeContractInstanceFromClassId(contractClass.id, seed, { + deployer, + initializationHash, + }); + + await this.addContractClass(contractClass, contractArtifact); + await this.addContractInstance(contractInstance); + return contractInstance; + } + + getFirstContractInstance(): ContractInstanceWithAddress { + return this.contractDataSource.getFirstContractInstance(); + } + + addContractClass(contractClass: ContractClassPublic, contractArtifact: ContractArtifact): Promise { + this.logger.debug(`Adding contract class with Id ${contractClass.id}`); + this.contractDataSource.addContractArtifact(contractClass.id, contractArtifact); + return this.contractDataSource.addContractClass(contractClass); + } + + async addContractInstance(contractInstance: ContractInstanceWithAddress) { + if (!this.skipContractDeployments) { + const contractAddressNullifier = await siloNullifier( + AztecAddress.fromNumber(DEPLOYER_CONTRACT_ADDRESS), + contractInstance.address.toField(), + ); + await this.merkleTrees.batchInsert(MerkleTreeId.NULLIFIER_TREE, [contractAddressNullifier.toBuffer()], 0); + } + await this.contractDataSource.addContractInstance(contractInstance); + } +} diff --git a/yarn-project/simulator/src/avm/fixtures/index.ts b/yarn-project/simulator/src/avm/fixtures/index.ts index 71ca64231774..938bebf2a2c1 100644 --- a/yarn-project/simulator/src/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/avm/fixtures/index.ts @@ -1,6 +1,6 @@ import { isNoirCallStackUnresolved } from '@aztec/circuit-types'; import { GasFees, GlobalVariables, MAX_L2_GAS_PER_TX_PUBLIC_PORTION } from '@aztec/circuits.js'; -import { type FunctionArtifact, FunctionSelector } from '@aztec/foundation/abi'; +import { type ContractArtifact, type FunctionArtifact, FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; @@ -23,6 +23,8 @@ import { AvmPersistableStateManager } from '../journal/journal.js'; import { NullifierManager } from '../journal/nullifiers.js'; import { PublicStorage } from '../journal/public_storage.js'; +export const PUBLIC_DISPATCH_FN_NAME = 'public_dispatch'; + /** * Create a new AVM context with default values. */ @@ -124,15 +126,51 @@ export function randomMemoryFields(length: number): Field[] { return [...Array(length)].map(_ => new Field(Fr.random())); } +export function getFunctionSelector( + functionName: string, + contractArtifact: ContractArtifact, +): Promise { + const fnArtifact = contractArtifact.functions.find(f => f.name === functionName)!; + assert(!!fnArtifact, `Function ${functionName} not found in ${contractArtifact.name}`); + const params = fnArtifact.parameters; + return FunctionSelector.fromNameAndParameters(fnArtifact.name, params); +} + +export function getContractFunctionArtifact( + functionName: string, + contractArtifact: ContractArtifact, +): FunctionArtifact | undefined { + const artifact = contractArtifact.functions.find(f => f.name === functionName)!; + if (!artifact) { + return undefined; + } + return artifact; +} + +export function resolveContractAssertionMessage( + functionName: string, + revertReason: AvmRevertReason, + output: Fr[], + contractArtifact: ContractArtifact, +): string | undefined { + traverseCauseChain(revertReason, cause => { + revertReason = cause as AvmRevertReason; + }); + + const functionArtifact = contractArtifact.functions.find(f => f.name === functionName); + if (!functionArtifact || !revertReason.noirCallStack || !isNoirCallStackUnresolved(revertReason.noirCallStack)) { + return undefined; + } + + return resolveAssertionMessageFromRevertData(output, functionArtifact); +} + export function getAvmTestContractFunctionSelector(functionName: string): Promise { - const artifact = AvmTestContractArtifact.functions.find(f => f.name === functionName)!; - assert(!!artifact, `Function ${functionName} not found in AvmTestContractArtifact`); - const params = artifact.parameters; - return FunctionSelector.fromNameAndParameters(artifact.name, params); + return getFunctionSelector(functionName, AvmTestContractArtifact); } export function getAvmTestContractArtifact(functionName: string): FunctionArtifact { - const artifact = AvmTestContractArtifact.functions.find(f => f.name === functionName)!; + const artifact = getContractFunctionArtifact(functionName, AvmTestContractArtifact); assert( !!artifact?.bytecode, `No bytecode found for function ${functionName}. Try re-running bootstrap.sh on the repository root.`, @@ -150,14 +188,5 @@ export function resolveAvmTestContractAssertionMessage( revertReason: AvmRevertReason, output: Fr[], ): string | undefined { - traverseCauseChain(revertReason, cause => { - revertReason = cause as AvmRevertReason; - }); - - const functionArtifact = AvmTestContractArtifact.functions.find(f => f.name === functionName); - if (!functionArtifact || !revertReason.noirCallStack || !isNoirCallStackUnresolved(revertReason.noirCallStack)) { - return undefined; - } - - return resolveAssertionMessageFromRevertData(output, functionArtifact); + return resolveContractAssertionMessage(functionName, revertReason, output, AvmTestContractArtifact); } diff --git a/yarn-project/simulator/src/avm/fixtures/simple_contract_data_source.ts b/yarn-project/simulator/src/avm/fixtures/simple_contract_data_source.ts new file mode 100644 index 000000000000..e76de873ebb4 --- /dev/null +++ b/yarn-project/simulator/src/avm/fixtures/simple_contract_data_source.ts @@ -0,0 +1,98 @@ +import { + type ContractClassPublic, + type ContractDataSource, + type ContractInstanceWithAddress, + type FunctionSelector, + type PublicFunction, + computePublicBytecodeCommitment, +} from '@aztec/circuits.js'; +import { type ContractArtifact } from '@aztec/foundation/abi'; +import { type AztecAddress } from '@aztec/foundation/aztec-address'; +import { type Fr } from '@aztec/foundation/fields'; +import { createLogger } from '@aztec/foundation/log'; + +import { PUBLIC_DISPATCH_FN_NAME } from './index.js'; + +/** + * This class is used during public/avm testing to function as a database of + * contract contract classes and instances. Tests can populate it with classes + * and instances and then probe it via the ContractDataSource interface. + * + * This class does not include any real merkle trees & merkle operations. + */ +export class SimpleContractDataSource implements ContractDataSource { + public logger = createLogger('simple-contract-data-source'); + + // maps contract class ID to class + private contractClasses: Map = new Map(); + // maps contract instance address to instance + private contractInstances: Map = new Map(); + // maps contract instance address to address + private contractArtifacts: Map = new Map(); + + ///////////////////////////////////////////////////////////// + // Helper functions not in the contract data source interface + getFirstContractInstance(): ContractInstanceWithAddress { + return this.contractInstances.values().next().value; + } + + addContractArtifact(classId: Fr, artifact: ContractArtifact): void { + this.contractArtifacts.set(classId.toString(), artifact); + } + + ///////////////////////////////////////////////////////////// + // ContractDataSource function impelementations + getPublicFunction(_address: AztecAddress, _selector: FunctionSelector): Promise { + throw new Error('Method not implemented.'); + } + + getBlockNumber(): Promise { + throw new Error('Method not implemented.'); + } + + getContractClass(id: Fr): Promise { + 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)); + } + + getContract(address: AztecAddress): Promise { + return Promise.resolve(this.contractInstances.get(address.toString())); + } + + getContractClassIds(): Promise { + throw new Error('Method not implemented.'); + } + + async getContractArtifact(address: AztecAddress): Promise { + const contractInstance = await this.getContract(address); + if (!contractInstance) { + this.logger.warn(`Contract not found at address: ${address}`); + return undefined; + } + this.logger.debug(`Retrieved contract artifact for address: ${address}`); + this.logger.debug(`Contract class ID: ${contractInstance.contractClassId}`); + return Promise.resolve(this.contractArtifacts.get(contractInstance!.contractClassId.toString())); + } + + getContractFunctionName(_address: AztecAddress, _selector: FunctionSelector): Promise { + return Promise.resolve(PUBLIC_DISPATCH_FN_NAME); + } + + registerContractFunctionSignatures(_address: AztecAddress, _signatures: string[]): Promise { + return Promise.resolve(); + } + + addContractClass(contractClass: ContractClassPublic): Promise { + this.contractClasses.set(contractClass.id.toString(), contractClass); + return Promise.resolve(); + } + + addContractInstance(contractInstance: ContractInstanceWithAddress): Promise { + this.contractInstances.set(contractInstance.address.toString(), contractInstance); + return Promise.resolve(); + } +} diff --git a/yarn-project/simulator/src/avm/journal/journal.ts b/yarn-project/simulator/src/avm/journal/journal.ts index e08f6e9e510c..3dd20c52eb4b 100644 --- a/yarn-project/simulator/src/avm/journal/journal.ts +++ b/yarn-project/simulator/src/avm/journal/journal.ts @@ -72,7 +72,7 @@ export class AvmPersistableStateManager { trace: PublicSideEffectTraceInterface, doMerkleOperations: boolean = false, firstNullifier: Fr, - ) { + ): Promise { const ephemeralForest = await AvmEphemeralForest.create(worldStateDB.getMerkleInterface()); return new AvmPersistableStateManager( worldStateDB, @@ -126,11 +126,12 @@ export class AvmPersistableStateManager { this.trace.merge(forkedState.trace, reverted); if (reverted) { if (this.doMerkleOperations) { - this.log.debug( + this.log.trace( `Rolled back nullifier tree to root ${this.merkleTrees.treeMap.get(MerkleTreeId.NULLIFIER_TREE)!.getRoot()}`, ); } } else { + this.log.trace('Merging forked state into parent...'); this.merkleTrees = forkedState.merkleTrees; } } diff --git a/yarn-project/simulator/src/public/apps_tests/token.test.ts b/yarn-project/simulator/src/public/apps_tests/token.test.ts new file mode 100644 index 000000000000..19b07924f297 --- /dev/null +++ b/yarn-project/simulator/src/public/apps_tests/token.test.ts @@ -0,0 +1,107 @@ +import { AztecAddress, type ContractInstanceWithAddress, Fr } from '@aztec/circuits.js'; +import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; + +import { PublicTxSimulationTester } from '../fixtures/public_tx_simulation_tester.js'; +import { type PublicTxResult } from '../public_tx_simulator.js'; + +describe('Public TX simulator apps tests: TokenContract', () => { + const admin = AztecAddress.fromNumber(42); + const sender = AztecAddress.fromNumber(111); + const receiver = AztecAddress.fromNumber(222); + + let token: ContractInstanceWithAddress; + let simTester: PublicTxSimulationTester; + + beforeEach(async () => { + simTester = await PublicTxSimulationTester.create(); + const constructorArgs = [admin, /*name=*/ 'Token', /*symbol=*/ 'TOK', /*decimals=*/ new Fr(18)]; + token = await simTester.registerAndDeployContract(constructorArgs, /*deployer=*/ admin, TokenContractArtifact); + + const constructorResult = await simTester.simulateTx( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'constructor', + args: constructorArgs, + }, + ], + ); + expect(constructorResult.revertCode.isOK()).toBe(true); + }); + + it('token mint, transfer, burn (and check balances)', async () => { + const mintAmount = 100n; + const transferAmount = 50n; + const nonce = new Fr(0); + + await checkBalance(sender, 0n); + + const mintResult = await simTester.simulateTx( + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'mint_to_public', + args: [/*to=*/ sender, mintAmount], + }, + ], + ); + expect(mintResult.revertCode.isOK()).toBe(true); + await checkBalance(sender, mintAmount); + + const transferResult = await simTester.simulateTx( + /*sender=*/ sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'transfer_in_public', + args: [/*from=*/ sender, /*to=*/ receiver, transferAmount, nonce], + }, + ], + ); + expect(transferResult.revertCode.isOK()).toBe(true); + await checkBalance(sender, mintAmount - transferAmount); + await checkBalance(receiver, transferAmount); + + const burnResult = await simTester.simulateTx( + /*sender=*/ receiver, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'burn_public', + args: [/*from=*/ receiver, transferAmount, nonce], + }, + ], + ); + expect(burnResult.revertCode.isOK()).toBe(true); + await checkBalance(receiver, 0n); + }); + + const checkBalance = async (account: AztecAddress, expectedBalance: bigint) => { + const balResult = await simTester.simulateTx( + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'balance_of_public', + args: [/*owner=*/ account], + isStaticCall: true, + }, + ], + ); + expect(balResult.revertCode.isOK()).toBe(true); + expectAppCall0Output(balResult, [new Fr(expectedBalance), Fr.zero()]); + }; +}); + +function expectAppCall0Output(txResult: PublicTxResult, expectedOutput: Fr[]): void { + expect(txResult.processedPhases).toEqual([ + expect.objectContaining({ returnValues: [expect.objectContaining({ values: expectedOutput })] }), + ]); +} diff --git a/yarn-project/simulator/src/public/fixtures/index.ts b/yarn-project/simulator/src/public/fixtures/index.ts index 6ef4f94e544d..98fc8928e0c6 100644 --- a/yarn-project/simulator/src/public/fixtures/index.ts +++ b/yarn-project/simulator/src/public/fixtures/index.ts @@ -1,388 +1,2 @@ -import { MerkleTreeId, type MerkleTreeWriteOperations, PublicExecutionRequest, Tx } from '@aztec/circuit-types'; -import { - type AvmCircuitInputs, - BlockHeader, - CallContext, - type ContractClassPublic, - type ContractDataSource, - type ContractInstanceWithAddress, - DEFAULT_GAS_LIMIT, - DEPLOYER_CONTRACT_ADDRESS, - FunctionSelector, - Gas, - GasFees, - GasSettings, - GlobalVariables, - MAX_L2_GAS_PER_TX_PUBLIC_PORTION, - MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS, - PartialPrivateTailPublicInputsForPublic, - PrivateKernelTailCircuitPublicInputs, - type PublicFunction, - PublicKeys, - RollupValidationRequests, - SerializableContractInstance, - TxConstantData, - TxContext, - computePublicBytecodeCommitment, -} from '@aztec/circuits.js'; -import { siloNullifier } from '@aztec/circuits.js/hash'; -import { makeContractClassPublic, makeContractInstanceFromClassId } from '@aztec/circuits.js/testing'; -import { type ContractArtifact, type FunctionArtifact } from '@aztec/foundation/abi'; -import { AztecAddress } from '@aztec/foundation/aztec-address'; -import { Fr, Point } from '@aztec/foundation/fields'; -import { openTmpStore } from '@aztec/kv-store/lmdb'; -import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; -import { MerkleTrees } from '@aztec/world-state'; - -import { strict as assert } from 'assert'; - -import { initContext, initExecutionEnvironment, initPersistableStateManager } from '../../avm/fixtures/index.js'; -import { AvmEphemeralForest, AvmSimulator } from '../../server.js'; -import { PublicEnqueuedCallSideEffectTrace } from '../enqueued_call_side_effect_trace.js'; -import { WorldStateDB } from '../public_db_sources.js'; -import { PublicTxSimulator } from '../public_tx_simulator.js'; - -const TIMESTAMP = new Fr(99833); - -export async function simulateAvmTestContractGenerateCircuitInputs( - setupFunctionNames: string[], - setupArgs: Fr[][] = [], - appFunctionNames: string[], - appArgs: Fr[][] = [], - teardownFunctionName?: string, - teardownArgs: Fr[] = [], - expectRevert: boolean = false, - contractDataSource?: MockedAvmTestContractDataSource, - assertionErrString?: string, -): Promise { - const globals = GlobalVariables.empty(); - globals.timestamp = TIMESTAMP; - - const merkleTrees = await (await MerkleTrees.new(openTmpStore())).fork(); - if (!contractDataSource) { - contractDataSource = await MockedAvmTestContractDataSource.create(); - } - await contractDataSource.deployContracts(merkleTrees); - const worldStateDB = new WorldStateDB(merkleTrees, contractDataSource); - - const simulator = new PublicTxSimulator(merkleTrees, worldStateDB, globals, /*doMerkleOperations=*/ true); - - const sender = await AztecAddress.random(); - const callContext = new CallContext( - sender, - contractDataSource.firstContractInstance.address, - contractDataSource.fnSelector, - /*isStaticCall=*/ false, - ); - const setupExecutionRequests: PublicExecutionRequest[] = []; - for (let i = 0; i < setupFunctionNames.length; i++) { - const functionSelector = await getAvmTestContractFunctionSelector(setupFunctionNames[i]); - const fnArgs = [functionSelector.toField(), ...setupArgs[i]]; - const executionRequest = new PublicExecutionRequest(callContext, fnArgs); - setupExecutionRequests.push(executionRequest); - } - const appExecutionRequests: PublicExecutionRequest[] = []; - for (let i = 0; i < appFunctionNames.length; i++) { - const functionSelector = await getAvmTestContractFunctionSelector(appFunctionNames[i]); - const fnArgs = [functionSelector.toField(), ...appArgs[i]]; - const executionRequest = new PublicExecutionRequest(callContext, fnArgs); - appExecutionRequests.push(executionRequest); - } - - let teardownExecutionRequest: PublicExecutionRequest | undefined = undefined; - if (teardownFunctionName) { - const functionSelector = await getAvmTestContractFunctionSelector(teardownFunctionName); - const fnArgs = [functionSelector.toField(), ...teardownArgs]; - teardownExecutionRequest = new PublicExecutionRequest(callContext, fnArgs); - } - - const tx: Tx = await createTxForPublicCalls( - setupExecutionRequests, - appExecutionRequests, - Fr.random(), - teardownExecutionRequest, - ); - - const avmResult = await simulator.simulate(tx); - - if (!expectRevert) { - expect(avmResult.revertCode.isOK()).toBe(true); - } else { - // Explicit revert when an assertion failed. - expect(avmResult.revertCode.isOK()).toBe(false); - expect(avmResult.revertReason).toBeDefined(); - if (assertionErrString !== undefined) { - expect(avmResult.revertReason?.getMessage()).toContain(assertionErrString); - } - } - - const avmCircuitInputs: AvmCircuitInputs = avmResult.avmProvingRequest.inputs; - return avmCircuitInputs; -} - -export async function simulateAvmTestContractCall( - functionName: string, - args: Fr[] = [], - expectRevert: boolean = false, - contractDataSource?: MockedAvmTestContractDataSource, -) { - const globals = GlobalVariables.empty(); - globals.timestamp = TIMESTAMP; - - if (!contractDataSource) { - contractDataSource = await MockedAvmTestContractDataSource.create(); - } - - const merkleTrees = await (await MerkleTrees.new(openTmpStore())).fork(); - await contractDataSource.deployContracts(merkleTrees); - const worldStateDB = new WorldStateDB(merkleTrees, contractDataSource); - - const trace = new PublicEnqueuedCallSideEffectTrace(); - const ephemeralTrees = await AvmEphemeralForest.create(worldStateDB.getMerkleInterface()); - const persistableState = initPersistableStateManager({ - worldStateDB, - trace, - merkleTrees: ephemeralTrees, - doMerkleOperations: true, - }); - - const sender = await AztecAddress.random(); - const functionSelector = await getAvmTestContractFunctionSelector(functionName); - args = [functionSelector.toField(), ...args]; - const environment = initExecutionEnvironment({ - calldata: args, - globals, - address: contractDataSource.firstContractInstance.address, - sender, - }); - const context = initContext({ env: environment, persistableState }); - - // First we simulate (though it's not needed in this simple case). - const simulator = new AvmSimulator(context); - const results = await simulator.execute(); - - expect(results.reverted).toBe(expectRevert); -} - -/** - * Craft a carrier transaction for some public calls for simulation by PublicTxSimulator. - */ -export async function createTxForPublicCalls( - setupExecutionRequests: PublicExecutionRequest[], - appExecutionRequests: PublicExecutionRequest[], - firstNullifier: Fr, - teardownExecutionRequest?: PublicExecutionRequest, - gasUsedByPrivate: Gas = Gas.empty(), -): Promise { - assert( - setupExecutionRequests.length > 0 || appExecutionRequests.length > 0 || teardownExecutionRequest !== undefined, - "Can't create public tx with no enqueued calls", - ); - const setupCallRequests = await Promise.all(setupExecutionRequests.map(er => er.toCallRequest())); - const appCallRequests = await Promise.all(appExecutionRequests.map(er => er.toCallRequest())); - // use max limits - const gasLimits = new Gas(DEFAULT_GAS_LIMIT, MAX_L2_GAS_PER_TX_PUBLIC_PORTION); - - const forPublic = PartialPrivateTailPublicInputsForPublic.empty(); - // TODO(#9269): Remove this fake nullifier method as we move away from 1st nullifier as hash. - forPublic.nonRevertibleAccumulatedData.nullifiers[0] = firstNullifier; // fake tx nullifier - - // We reverse order because the simulator expects it to be like a "stack" of calls to pop from - for (let i = setupCallRequests.length - 1; i >= 0; i--) { - forPublic.nonRevertibleAccumulatedData.publicCallRequests[i] = setupCallRequests[i]; - } - for (let i = appCallRequests.length - 1; i >= 0; i--) { - forPublic.revertibleAccumulatedData.publicCallRequests[i] = appCallRequests[i]; - } - if (teardownExecutionRequest) { - forPublic.publicTeardownCallRequest = await teardownExecutionRequest.toCallRequest(); - } - - const teardownGasLimits = teardownExecutionRequest ? gasLimits : Gas.empty(); - const gasSettings = new GasSettings(gasLimits, teardownGasLimits, GasFees.empty(), 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, - AztecAddress.zero(), - forPublic, - ); - const tx = Tx.newWithTxData(txData, teardownExecutionRequest); - - // Reverse order because the simulator expects it to be like a "stack" of calls to pop from. - // Also push app calls before setup calls for this reason. - for (let i = appExecutionRequests.length - 1; i >= 0; i--) { - tx.enqueuedPublicFunctionCalls.push(appExecutionRequests[i]); - } - for (let i = setupExecutionRequests.length - 1; i >= 0; i--) { - tx.enqueuedPublicFunctionCalls.push(setupExecutionRequests[i]); - } - - return tx; -} - -export class MockedAvmTestContractDataSource implements ContractDataSource { - // maps contract class ID to class - private contractClasses: Map = new Map(); - // maps contract instance address to instance - public contractInstances: Map = new Map(); - - public firstContractInstance: ContractInstanceWithAddress = SerializableContractInstance.default().withAddress( - AztecAddress.fromNumber(0), - ); - public instanceSameClassAsFirstContract: ContractInstanceWithAddress = - SerializableContractInstance.default().withAddress(AztecAddress.fromNumber(0)); - public otherContractInstance!: ContractInstanceWithAddress; - - private constructor( - private skipContractDeployments: boolean, - public fnName: string, - private publicFn: PublicFunction, - ) {} - - get fnSelector() { - return this.publicFn.selector; - } - - async deployContracts(merkleTrees: MerkleTreeWriteOperations) { - if (!this.skipContractDeployments) { - for (const contractInstance of this.contractInstances.values()) { - const contractAddressNullifier = await siloNullifier( - AztecAddress.fromNumber(DEPLOYER_CONTRACT_ADDRESS), - contractInstance.address.toField(), - ); - await merkleTrees.batchInsert(MerkleTreeId.NULLIFIER_TREE, [contractAddressNullifier.toBuffer()], 0); - } - - const instanceSameClassAsFirstContractNullifier = await siloNullifier( - AztecAddress.fromNumber(DEPLOYER_CONTRACT_ADDRESS), - this.instanceSameClassAsFirstContract.address.toField(), - ); - await merkleTrees.batchInsert( - MerkleTreeId.NULLIFIER_TREE, - [instanceSameClassAsFirstContractNullifier.toBuffer()], - 0, - ); - - // other contract address used by the bulk test's GETCONTRACTINSTANCE test - const otherContractAddressNullifier = await siloNullifier( - AztecAddress.fromNumber(DEPLOYER_CONTRACT_ADDRESS), - this.otherContractInstance.address.toField(), - ); - await merkleTrees.batchInsert(MerkleTreeId.NULLIFIER_TREE, [otherContractAddressNullifier.toBuffer()], 0); - } - } - - public static async create(skipContractDeployments: boolean = false): Promise { - const fnName = 'public_dispatch'; - const bytecode = getAvmTestContractBytecode(fnName); - const fnSelector = await getAvmTestContractFunctionSelector(fnName); - const publicFn = { bytecode: bytecode, selector: fnSelector }; - const dataSource = new MockedAvmTestContractDataSource(skipContractDeployments, fnName, publicFn); - // create enough unique classes to hit the limit (plus two extra) - for (let i = 0; i < MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS + 1; i++) { - const contractClass = await makeContractClassPublic(/*seed=*/ i, dataSource.publicFn); - const contractInstance = await makeContractInstanceFromClassId(contractClass.id, /*seed=*/ i); - dataSource.contractClasses.set(contractClass.id.toString(), contractClass); - dataSource.contractInstances.set(contractInstance.address.toString(), contractInstance); - if (i === 0) { - dataSource.firstContractInstance = contractInstance; - } - } - // a contract with the same class but different instance/address as the first contract - dataSource.instanceSameClassAsFirstContract = await makeContractInstanceFromClassId( - dataSource.firstContractInstance.contractClassId, - /*seed=*/ 1000, - ); - - // The values here should match those in `avm_simulator.test.ts` - // Used for GETCONTRACTINSTANCE test - dataSource.otherContractInstance = new SerializableContractInstance({ - version: 1, - salt: new Fr(0x123), - deployer: new AztecAddress(new Fr(0x456)), - contractClassId: new Fr(0x789), - initializationHash: new Fr(0x101112), - publicKeys: new PublicKeys( - new Point(new Fr(0x131415), new Fr(0x161718), false), - new Point(new Fr(0x192021), new Fr(0x222324), false), - new Point(new Fr(0x252627), new Fr(0x282930), false), - new Point(new Fr(0x313233), new Fr(0x343536), false), - ), - }).withAddress(AztecAddress.fromNumber(0x4444)); - return dataSource; - } - - getPublicFunction(_address: AztecAddress, _selector: FunctionSelector): Promise { - return Promise.resolve(this.publicFn); - } - - getBlockNumber(): Promise { - throw new Error('Method not implemented.'); - } - - getContractClass(id: Fr): Promise { - return Promise.resolve(this.contractClasses.get(id.toString())); - } - - getBytecodeCommitment(_id: Fr): Promise { - return computePublicBytecodeCommitment(this.publicFn.bytecode); - } - - addContractClass(_contractClass: ContractClassPublic): Promise { - return Promise.resolve(); - } - - getContract(address: AztecAddress): Promise { - if (!this.skipContractDeployments) { - if (address.equals(this.otherContractInstance.address)) { - return Promise.resolve(this.otherContractInstance); - } else if (address.equals(this.instanceSameClassAsFirstContract.address)) { - return Promise.resolve(this.instanceSameClassAsFirstContract); - } else { - return Promise.resolve(this.contractInstances.get(address.toString())); - } - } - return Promise.resolve(undefined); - } - - getContractClassIds(): Promise { - throw new Error('Method not implemented.'); - } - - getContractArtifact(_address: AztecAddress): Promise { - throw new Error('Method not implemented.'); - } - - getContractFunctionName(_address: AztecAddress, _selector: FunctionSelector): Promise { - return Promise.resolve(this.fnName); - } - - registerContractFunctionSignatures(_address: AztecAddress, _signatures: string[]): Promise { - return Promise.resolve(); - } -} - -function getAvmTestContractFunctionSelector(functionName: string): Promise { - const artifact = AvmTestContractArtifact.functions.find(f => f.name === functionName)!; - assert(!!artifact, `Function ${functionName} not found in AvmTestContractArtifact`); - const params = artifact.parameters; - return FunctionSelector.fromNameAndParameters(artifact.name, params); -} - -function getAvmTestContractArtifact(functionName: string): FunctionArtifact { - const artifact = AvmTestContractArtifact.functions.find(f => f.name === functionName)!; - assert( - !!artifact?.bytecode, - `No bytecode found for function ${functionName}. Try re-running bootstrap.sh on the repository root.`, - ); - return artifact; -} - -function getAvmTestContractBytecode(functionName: string): Buffer { - const artifact = getAvmTestContractArtifact(functionName); - return artifact.bytecode; -} +export * from './public_tx_simulation_tester.js'; +export * from './utils.js'; 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 new file mode 100644 index 000000000000..a35e62a0e4fa --- /dev/null +++ b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts @@ -0,0 +1,174 @@ +import { MerkleTreeId, PublicExecutionRequest, type Tx } from '@aztec/circuit-types'; +import { + type AvmCircuitPublicInputs, + CallContext, + FunctionSelector, + GasFees, + GlobalVariables, + MAX_NOTE_HASHES_PER_TX, + MAX_NULLIFIERS_PER_TX, + NULLIFIER_SUBTREE_HEIGHT, + PUBLIC_DATA_TREE_HEIGHT, + PUBLIC_DISPATCH_SELECTOR, +} from '@aztec/circuits.js'; +import { type ContractArtifact, encodeArguments } from '@aztec/foundation/abi'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { padArrayEnd } from '@aztec/foundation/collection'; +import { Fr } from '@aztec/foundation/fields'; +import { openTmpStore } from '@aztec/kv-store/lmdb'; +import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; +import { MerkleTrees } from '@aztec/world-state'; + +import { BaseAvmSimulationTester } from '../../avm/fixtures/base_avm_simulation_tester.js'; +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 { createTxForPublicCalls } from './index.js'; + +const TIMESTAMP = new Fr(99833); +const DEFAULT_GAS_FEES = new GasFees(2, 3); + +export type TestEnqueuedCall = { + address: AztecAddress; + fnName: string; + args: any[]; + isStaticCall?: boolean; +}; + +/** + * A test class that extends the BaseAvmSimulationTester to enable real-app testing of the PublicTxSimulator. + * It provides an interface for simulating one transaction at a time and maintains state between subsequent + * transactions. + */ +export class PublicTxSimulationTester extends BaseAvmSimulationTester { + private txCount = 0; + + public static async create(skipContractDeployments = false): Promise { + const contractDataSource = new SimpleContractDataSource(); + const merkleTrees = await (await MerkleTrees.new(openTmpStore())).fork(); + return new PublicTxSimulationTester(contractDataSource, merkleTrees, skipContractDeployments); + } + + public async simulateTx( + sender: AztecAddress, + setupCalls: TestEnqueuedCall[] = [], + appCalls: TestEnqueuedCall[] = [], + teardownCall?: TestEnqueuedCall, + feePayer: AztecAddress = AztecAddress.zero(), + ): Promise { + const globals = GlobalVariables.empty(); + globals.timestamp = TIMESTAMP; + globals.gasFees = DEFAULT_GAS_FEES; + + const worldStateDB = new WorldStateDB(this.merkleTrees, this.contractDataSource); + const simulator = new PublicTxSimulator(this.merkleTrees, worldStateDB, globals, /*doMerkleOperations=*/ true); + + const setupExecutionRequests: PublicExecutionRequest[] = []; + for (let i = 0; i < setupCalls.length; i++) { + const address = setupCalls[i].address; + const contractArtifact = await this.contractDataSource.getContractArtifact(address); + const req = await executionRequestForCall( + 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 req = await executionRequestForCall( + sender, + address, + appCalls[i].fnName, + appCalls[i].args, + appCalls[i].isStaticCall, + contractArtifact, + ); + appExecutionRequests.push(req); + } + + let teardownExecutionRequest: PublicExecutionRequest | undefined = undefined; + if (teardownCall) { + const address = teardownCall.address; + const contractArtifact = await this.contractDataSource.getContractArtifact(address); + teardownExecutionRequest = await executionRequestForCall( + sender, + address, + teardownCall.fnName, + teardownCall.args, + teardownCall.isStaticCall, + contractArtifact, + ); + } + + // Use a fake "first nullifier" to make sure note hash nonces are computed properly, + // but make sure each tx has a unique first nullifier. + const firstNullifier = new Fr(420000 + this.txCount++); + + const tx: Tx = await createTxForPublicCalls( + firstNullifier, + setupExecutionRequests, + appExecutionRequests, + teardownExecutionRequest, + feePayer, + ); + + const avmResult = await simulator.simulate(tx); + + if (avmResult.revertCode.isOK()) { + await this.commitTxStateUpdates(avmResult.avmProvingRequest.inputs.output); + } + + return avmResult; + } + + private async commitTxStateUpdates(avmCircuitInputs: AvmCircuitPublicInputs) { + await this.merkleTrees.appendLeaves( + MerkleTreeId.NOTE_HASH_TREE, + padArrayEnd(avmCircuitInputs.accumulatedData.noteHashes, Fr.ZERO, MAX_NOTE_HASHES_PER_TX), + ); + try { + await this.merkleTrees.batchInsert( + MerkleTreeId.NULLIFIER_TREE, + padArrayEnd(avmCircuitInputs.accumulatedData.nullifiers, Fr.ZERO, MAX_NULLIFIERS_PER_TX).map(n => n.toBuffer()), + NULLIFIER_SUBTREE_HEIGHT, + ); + } catch (error) { + this.logger.warn(`Detected duplicate nullifier.`); + } + + await this.merkleTrees.batchInsert( + MerkleTreeId.PUBLIC_DATA_TREE, + avmCircuitInputs.accumulatedData.publicDataWrites.map(w => w.toBuffer()), + PUBLIC_DATA_TREE_HEIGHT, + ); + } +} + +async function executionRequestForCall( + 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); + const encodedArgs = encodeArguments(fnAbi!, args); + const calldata = [fnSelector.toField(), ...encodedArgs]; + + const callContext = new CallContext( + sender, + address, + /*selector=*/ new FunctionSelector(PUBLIC_DISPATCH_SELECTOR), + isStaticCall, + ); + return new PublicExecutionRequest(callContext, calldata); +} diff --git a/yarn-project/simulator/src/public/fixtures/utils.ts b/yarn-project/simulator/src/public/fixtures/utils.ts new file mode 100644 index 000000000000..869ba063b16a --- /dev/null +++ b/yarn-project/simulator/src/public/fixtures/utils.ts @@ -0,0 +1,110 @@ +import { type PublicExecutionRequest, Tx } from '@aztec/circuit-types'; +import { + BlockHeader, + DEFAULT_GAS_LIMIT, + FunctionSelector, + Gas, + GasFees, + GasSettings, + MAX_L2_GAS_PER_TX_PUBLIC_PORTION, + PartialPrivateTailPublicInputsForPublic, + PrivateKernelTailCircuitPublicInputs, + RollupValidationRequests, + TxConstantData, + TxContext, +} from '@aztec/circuits.js'; +import { type FunctionArtifact } from '@aztec/foundation/abi'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { Fr } from '@aztec/foundation/fields'; +import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; + +import { strict as assert } from 'assert'; + +export const PUBLIC_DISPATCH_FN_NAME = 'public_dispatch'; + +/** + * Craft a carrier transaction for some public calls for simulation by PublicTxSimulator. + */ +export async function createTxForPublicCalls( + firstNullifier: Fr, + setupExecutionRequests: PublicExecutionRequest[], + appExecutionRequests: PublicExecutionRequest[], + teardownExecutionRequest?: PublicExecutionRequest, + feePayer = AztecAddress.zero(), + gasUsedByPrivate: Gas = Gas.empty(), +): Promise { + assert( + setupExecutionRequests.length > 0 || appExecutionRequests.length > 0 || teardownExecutionRequest !== undefined, + "Can't create public tx with no enqueued calls", + ); + const setupCallRequests = await Promise.all(setupExecutionRequests.map(er => er.toCallRequest())); + const appCallRequests = await Promise.all(appExecutionRequests.map(er => er.toCallRequest())); + // use max limits + const gasLimits = new Gas(DEFAULT_GAS_LIMIT, MAX_L2_GAS_PER_TX_PUBLIC_PORTION); + + const forPublic = PartialPrivateTailPublicInputsForPublic.empty(); + // TODO(#9269): Remove this fake nullifier method as we move away from 1st nullifier as hash. + forPublic.nonRevertibleAccumulatedData.nullifiers[0] = firstNullifier; // fake tx nullifier + + // We reverse order because the simulator expects it to be like a "stack" of calls to pop from + for (let i = setupCallRequests.length - 1; i >= 0; i--) { + forPublic.nonRevertibleAccumulatedData.publicCallRequests[i] = setupCallRequests[i]; + } + for (let i = appCallRequests.length - 1; i >= 0; i--) { + forPublic.revertibleAccumulatedData.publicCallRequests[i] = appCallRequests[i]; + } + if (teardownExecutionRequest) { + forPublic.publicTeardownCallRequest = await teardownExecutionRequest.toCallRequest(); + } + + const maxFeesPerGas = feePayer.isZero() ? GasFees.empty() : new GasFees(10, 10); + const teardownGasLimits = teardownExecutionRequest ? gasLimits : Gas.empty(); + const gasSettings = new GasSettings(gasLimits, teardownGasLimits, 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, + ); + const tx = Tx.newWithTxData(txData, teardownExecutionRequest); + + // Reverse order because the simulator expects it to be like a "stack" of calls to pop from. + // Also push app calls before setup calls for this reason. + for (let i = appExecutionRequests.length - 1; i >= 0; i--) { + tx.enqueuedPublicFunctionCalls.push(appExecutionRequests[i]); + } + for (let i = setupExecutionRequests.length - 1; i >= 0; i--) { + tx.enqueuedPublicFunctionCalls.push(setupExecutionRequests[i]); + } + + return tx; +} + +export function getAvmTestContractFunctionSelector(functionName: string): Promise { + const artifact = AvmTestContractArtifact.functions.find(f => f.name === functionName)!; + assert(!!artifact, `Function ${functionName} not found in AvmTestContractArtifact`); + const params = artifact.parameters; + return FunctionSelector.fromNameAndParameters(artifact.name, params); +} + +export function getAvmTestContractArtifact(functionName: string): FunctionArtifact { + const artifact = AvmTestContractArtifact.functions.find(f => f.name === functionName)!; + assert( + !!artifact?.bytecode, + `No bytecode found for function ${functionName}. Try re-running bootstrap.sh on the repository root.`, + ); + return artifact; +} + +export function getAvmTestContractBytecode(functionName: string): Buffer { + const artifact = getAvmTestContractArtifact(functionName); + return artifact.bytecode; +} + +export function getAvmTestContractPublicDispatchBytecode(): Buffer { + return getAvmTestContractBytecode(PUBLIC_DISPATCH_FN_NAME); +} diff --git a/yarn-project/simulator/src/public/public_tx_context.ts b/yarn-project/simulator/src/public/public_tx_context.ts index 156c1fa7aca8..f18209e8c456 100644 --- a/yarn-project/simulator/src/public/public_tx_context.ts +++ b/yarn-project/simulator/src/public/public_tx_context.ts @@ -99,7 +99,7 @@ export class PublicTxContext { countAccumulatedItems(nonRevertibleAccumulatedDataFromPrivate.l2ToL1Msgs), /*publicLogs*/ 0, ); - const enqueuedCallTrace = new PublicEnqueuedCallSideEffectTrace( + const trace = new PublicEnqueuedCallSideEffectTrace( /*startSideEffectCounter=*/ 0, previousAccumulatedDataArrayLengths, ); @@ -109,7 +109,7 @@ export class PublicTxContext { // Transaction level state manager that will be forked for revertible phases. const txStateManager = await AvmPersistableStateManager.create( worldStateDB, - enqueuedCallTrace, + trace, doMerkleOperations, firstNullifier, ); @@ -136,7 +136,7 @@ export class PublicTxContext { tx.data.forPublic!.nonRevertibleAccumulatedData, tx.data.forPublic!.revertibleAccumulatedData, tx.data.feePayer, - enqueuedCallTrace, + trace, ); } diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 297c307f38c3..0391ce68d113 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -838,10 +838,11 @@ export class TXE implements TypedOracle { // private execution used Gas(1, 1) so it can compute a tx fee. const gasUsedByPrivate = isTeardown ? new Gas(1, 1) : Gas.empty(); const tx = await createTxForPublicCalls( + firstNullifier, /*setupExecutionRequests=*/ [], /*appExecutionRequests=*/ isTeardown ? [] : [executionRequest], - firstNullifier, /*teardownExecutionRequests=*/ isTeardown ? executionRequest : undefined, + /*feePayer=*/ AztecAddress.zero(), gasUsedByPrivate, ); diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index a9e250f3a676..85a057953f8d 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -324,7 +324,9 @@ __metadata: "@aztec/ethereum": "workspace:^" "@aztec/foundation": "workspace:^" "@aztec/kv-store": "workspace:^" + "@aztec/noir-contracts.js": "workspace:^" "@aztec/noir-protocol-circuits-types": "workspace:^" + "@aztec/protocol-contracts": "workspace:^" "@aztec/simulator": "workspace:^" "@aztec/telemetry-client": "workspace:^" "@aztec/types": "workspace:^" @@ -800,6 +802,7 @@ __metadata: "@aztec/circuits.js": "workspace:^" "@aztec/foundation": "workspace:^" "@aztec/kv-store": "workspace:^" + "@aztec/noir-contracts.js": "workspace:^" "@aztec/simulator": "workspace:^" "@aztec/telemetry-client": "workspace:^" "@aztec/types": "workspace:^"