diff --git a/noir-projects/aztec-nr/aztec/src/context/interface.nr b/noir-projects/aztec-nr/aztec/src/context/interface.nr index fd21ae787062..19a34c90b0b4 100644 --- a/noir-projects/aztec-nr/aztec/src/context/interface.nr +++ b/noir-projects/aztec-nr/aztec/src/context/interface.nr @@ -9,5 +9,6 @@ trait ContextInterface { fn chain_id(self) -> Field; fn version(self) -> Field; fn selector(self) -> FunctionSelector; + fn get_args_hash(self) -> Field; fn get_header(self) -> Header; } diff --git a/noir-projects/aztec-nr/aztec/src/context/private_context.nr b/noir-projects/aztec-nr/aztec/src/context/private_context.nr index 4bc91a32835d..7e43711afeb6 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -92,6 +92,10 @@ impl ContextInterface for PrivateContext { self.inputs.call_context.function_selector } + fn get_args_hash(self) -> Field { + self.args_hash + } + // Returns the header of a block whose state is used during private execution (not the block the transaction is // included in). pub fn get_header(self) -> Header { diff --git a/noir-projects/aztec-nr/aztec/src/context/public_context.nr b/noir-projects/aztec-nr/aztec/src/context/public_context.nr index 352e8f03f386..09d780730cf6 100644 --- a/noir-projects/aztec-nr/aztec/src/context/public_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/public_context.nr @@ -74,6 +74,10 @@ impl ContextInterface for PublicContext { self.inputs.call_context.function_selector } + fn get_args_hash(self) -> Field { + self.args_hash + } + fn get_header(self) -> Header { self.historical_header } diff --git a/noir-projects/aztec-nr/aztec/src/initializer.nr b/noir-projects/aztec-nr/aztec/src/initializer.nr index 13826bf48db7..52562cb13887 100644 --- a/noir-projects/aztec-nr/aztec/src/initializer.nr +++ b/noir-projects/aztec-nr/aztec/src/initializer.nr @@ -1,6 +1,14 @@ -use dep::protocol_types::hash::silo_nullifier; -use crate::context::{PrivateContext, PublicContext, ContextInterface}; -use crate::history::nullifier_inclusion::prove_nullifier_inclusion; +use dep::protocol_types::{ + hash::{silo_nullifier, pedersen_hash}, + constants::GENERATOR_INDEX__CONSTRUCTOR, + abis::function_selector::FunctionSelector, +}; + +use crate::{ + context::{PrivateContext, PublicContext, ContextInterface}, + oracle::get_contract_instance::get_contract_instance, + history::nullifier_inclusion::prove_nullifier_inclusion, +}; pub fn mark_as_initialized(context: &mut TContext) where TContext: ContextInterface { let init_nullifier = compute_unsiloed_contract_initialization_nullifier(*context); @@ -23,3 +31,14 @@ pub fn compute_contract_initialization_nullifier(context: TContext) -> pub fn compute_unsiloed_contract_initialization_nullifier(context: TContext) -> Field where TContext: ContextInterface { context.this_address().to_field() } + +pub fn assert_initialization_args_match_address_preimage(context: TContext) where TContext: ContextInterface { + let address = context.this_address(); + let instance = get_contract_instance(address); + let expected_init = compute_initialization_hash(context.selector(), context.get_args_hash()); + assert(instance.initialization_hash == expected_init, "Initialization hash does not match"); +} + +pub fn compute_initialization_hash(init_selector: FunctionSelector, init_args_hash: Field) -> Field { + pedersen_hash([init_selector.to_field(), init_args_hash], GENERATOR_INDEX__CONSTRUCTOR) +} \ No newline at end of file diff --git a/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr index dddb482211e4..2a2da7ae8ce2 100644 --- a/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr @@ -5,7 +5,7 @@ contract StatefulTest { use dep::aztec::{ deploy::{deploy_contract as aztec_deploy_contract}, context::{PublicContext, Context}, oracle::get_contract_instance::get_contract_instance, - initializer::{mark_as_initialized, assert_is_initialized} + initializer::assert_is_initialized, }; struct Storage { diff --git a/noir/noir-repo/aztec_macros/src/transforms/functions.rs b/noir/noir-repo/aztec_macros/src/transforms/functions.rs index 8b3c7d2f53bf..09c11e173fe6 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/functions.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/functions.rs @@ -48,6 +48,12 @@ pub fn transform_function( func.def.body.0.insert(0, init_check); } + // Add assertion for initialization arguments + if is_initializer { + let assert_init_args = create_assert_init_args(); + func.def.body.0.insert(0, assert_init_args); + } + // Add access to the storage struct if storage_defined { let storage_def = abstract_storage(&ty.to_lowercase(), false); @@ -205,6 +211,23 @@ fn create_internal_check(fname: &str) -> Statement { ))) } +/// Creates a call to assert_initialization_args_match_address_preimage to ensure +/// the initialization arguments used in the init call match the address preimage. +/// +/// ```noir +/// assert_initialization_args_match_address_preimage(context); +/// ``` +fn create_assert_init_args() -> Statement { + make_statement(StatementKind::Expression(call( + variable_path(chained_dep!( + "aztec", + "initializer", + "assert_initialization_args_match_address_preimage" + )), + vec![variable("context")], + ))) +} + /// Creates the private context object to be accessed within the function, the parameters need to be extracted to be /// appended into the args hash object. /// diff --git a/yarn-project/aztec.js/src/account_manager/index.ts b/yarn-project/aztec.js/src/account_manager/index.ts index af1fe754c81d..7fcfa80f8077 100644 --- a/yarn-project/aztec.js/src/account_manager/index.ts +++ b/yarn-project/aztec.js/src/account_manager/index.ts @@ -1,5 +1,5 @@ import { CompleteAddress, GrumpkinPrivateKey, PXE } from '@aztec/circuit-types'; -import { EthAddress, PublicKey, getContractInstanceFromDeployParams } from '@aztec/circuits.js'; +import { PublicKey, getContractInstanceFromDeployParams } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { ContractInstanceWithAddress } from '@aztec/types/contracts'; @@ -77,14 +77,11 @@ export class AccountManager { public getInstance(): ContractInstanceWithAddress { if (!this.instance) { const encryptionPublicKey = generatePublicKey(this.encryptionPrivateKey); - const portalAddress = EthAddress.ZERO; - this.instance = getContractInstanceFromDeployParams( - this.accountContract.getContractArtifact(), - this.accountContract.getDeploymentArgs(), - this.salt, - encryptionPublicKey, - portalAddress, - ); + this.instance = getContractInstanceFromDeployParams(this.accountContract.getContractArtifact(), { + constructorArgs: this.accountContract.getDeploymentArgs(), + salt: this.salt, + publicKey: encryptionPublicKey, + }); } return this.instance; } diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index e247c9a3a0f8..3766e6e6bf5d 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -163,10 +163,13 @@ export class DeployMethod extends Bas */ public getInstance(options: DeployOptions = {}): ContractInstanceWithAddress { if (!this.instance) { - const portalContract = options.portalContract ?? EthAddress.ZERO; - const contractAddressSalt = options.contractAddressSalt ?? Fr.random(); - const deployParams = [this.artifact, this.args, contractAddressSalt, this.publicKey, portalContract] as const; - this.instance = getContractInstanceFromDeployParams(...deployParams); + this.instance = getContractInstanceFromDeployParams(this.artifact, { + constructorArgs: this.args, + salt: options.contractAddressSalt, + portalAddress: options.portalContract, + publicKey: this.publicKey, + constructorName: this.constructorArtifact.name, + }); } return this.instance; } diff --git a/yarn-project/circuits.js/src/contract/contract_instance.ts b/yarn-project/circuits.js/src/contract/contract_instance.ts index d5af7c7db5d2..b6e829fefd58 100644 --- a/yarn-project/circuits.js/src/contract/contract_instance.ts +++ b/yarn-project/circuits.js/src/contract/contract_instance.ts @@ -11,27 +11,32 @@ import { computeInitializationHash, computePublicKeysHash, } from './contract_address.js'; -import { isConstructor } from './contract_tree/contract_tree.js'; /** * Generates a Contract Instance from the deployment params. * @param artifact - The account contract build artifact. - * @param args - The args to the account contract constructor - * @param contractAddressSalt - The salt to be used in the contract address derivation - * @param publicKey - The account public key - * @param portalContractAddress - The portal contract address + * @param opts - Options for the deployment. * @returns - The contract instance */ export function getContractInstanceFromDeployParams( artifact: ContractArtifact, - args: any[] = [], - contractAddressSalt: Fr = Fr.random(), - publicKey: PublicKey = Point.ZERO, - portalContractAddress: EthAddress = EthAddress.ZERO, + opts: { + constructorName?: string; + constructorArgs?: any[]; + salt?: Fr; + publicKey?: PublicKey; + portalAddress?: EthAddress; + }, ): ContractInstanceWithAddress { - const constructorArtifact = artifact.functions.find(isConstructor); + const args = opts.constructorArgs ?? []; + const salt = opts.salt ?? Fr.random(); + const publicKey = opts.publicKey ?? Point.ZERO; + const portalContractAddress = opts.portalAddress ?? EthAddress.ZERO; + const constructorName = opts.constructorName ?? 'constructor'; + + const constructorArtifact = artifact.functions.find(fn => fn.name === constructorName); if (!constructorArtifact) { - throw new Error('Cannot find constructor in the artifact.'); + throw new Error(`Cannot find constructor with name ${constructorName} in the artifact.`); } if (!constructorArtifact.verificationKey) { throw new Error('Missing verification key for the constructor.'); @@ -47,7 +52,7 @@ export function getContractInstanceFromDeployParams( initializationHash, portalContractAddress, publicKeysHash, - salt: contractAddressSalt, + salt, version: 1, }; diff --git a/yarn-project/circuits.js/src/contract/contract_tree/contract_tree.ts b/yarn-project/circuits.js/src/contract/contract_tree/contract_tree.ts index a35782a6fd94..b8139394216b 100644 --- a/yarn-project/circuits.js/src/contract/contract_tree/contract_tree.ts +++ b/yarn-project/circuits.js/src/contract/contract_tree/contract_tree.ts @@ -20,7 +20,7 @@ export function hashVKStr(vk: string) { * Determine if the given function is a constructor. * This utility function checks if the 'name' property of the input object is "constructor". * Returns true if the function is a constructor, false otherwise. - * + * TODO(palla/purge-old-contract-deploy): Remove me * @param Object - An object containing a 'name' property. * @returns Boolean indicating if the function is a constructor. */ diff --git a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts index 26a4c06cea38..b48327dba1d2 100644 --- a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts +++ b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts @@ -14,7 +14,7 @@ import { generatePublicKey, getContractInstanceFromDeployParams, } from '@aztec/aztec.js'; -import { EthAddress, computePartialAddress } from '@aztec/circuits.js'; +import { computePartialAddress } from '@aztec/circuits.js'; import { InclusionProofsContract } from '@aztec/noir-contracts.js'; import { ClaimContract } from '@aztec/noir-contracts.js/Claim'; import { CrowdfundingContract, CrowdfundingContractArtifact } from '@aztec/noir-contracts.js/Crowdfunding'; @@ -108,13 +108,11 @@ describe('e2e_crowdfunding_and_claim', () => { const salt = Fr.random(); const args = [donationToken.address, operatorWallet.getAddress(), deadline]; - const deployInfo = getContractInstanceFromDeployParams( - CrowdfundingContractArtifact, - args, + const deployInfo = getContractInstanceFromDeployParams(CrowdfundingContractArtifact, { + constructorArgs: args, salt, - crowdfundingPublicKey, - EthAddress.ZERO, - ); + publicKey: crowdfundingPublicKey, + }); await pxe.registerAccount(crowdfundingPrivateKey, computePartialAddress(deployInfo)); crowdfundingContract = await CrowdfundingContract.deployWithPublicKey( diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts index cb52be1f2194..244e77c64dce 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts @@ -13,7 +13,6 @@ import { Fr, PXE, SignerlessWallet, - TxStatus, Wallet, getContractClassFromArtifact, getContractInstanceFromDeployParams, @@ -56,15 +55,9 @@ describe('e2e_deploy_contract', () => { * https://hackmd.io/ouVCnacHQRq2o1oRc5ksNA#Interfaces-and-Responsibilities */ it('should deploy a test contract', async () => { - const publicKey = accounts[0].publicKey; const salt = Fr.random(); - const deploymentData = getContractInstanceFromDeployParams( - TestContractArtifact, - [], - salt, - publicKey, - EthAddress.ZERO, - ); + const publicKey = accounts[0].publicKey; + const deploymentData = getContractInstanceFromDeployParams(TestContractArtifact, { salt, publicKey }); const deployer = new ContractDeployer(TestContractArtifact, wallet, publicKey); const receipt = await deployer.deploy().send({ contractAddressSalt: salt }).wait({ wallet }); expect(receipt.contract.address).toEqual(deploymentData.address); @@ -185,7 +178,7 @@ describe('e2e_deploy_contract', () => { const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet; const owner = await registerRandomAccount(pxe); const initArgs: StatefulContractCtorArgs = [owner, 42]; - const contract = await registerContract(testWallet, StatefulTestContract, initArgs); + const contract = await registerContract(testWallet, StatefulTestContract, { initArgs }); logger.info(`Calling the constructor for ${contract.address}`); await contract.methods .constructor(...initArgs) @@ -203,9 +196,11 @@ describe('e2e_deploy_contract', () => { // Tests privately initializing multiple undeployed contracts on the same tx through an account contract. it('initializes multiple undeployed contracts in a single tx', async () => { const owner = await registerRandomAccount(pxe); - const initArgs: StatefulContractCtorArgs[] = [42, 52].map(value => [owner, value]); - const contracts = await Promise.all(initArgs.map(args => registerContract(wallet, StatefulTestContract, args))); - const calls = contracts.map((c, i) => c.methods.constructor(...initArgs[i]).request()); + const initArgss: StatefulContractCtorArgs[] = [42, 52].map(value => [owner, value]); + const contracts = await Promise.all( + initArgss.map(initArgs => registerContract(wallet, StatefulTestContract, { initArgs })), + ); + const calls = contracts.map((c, i) => c.methods.constructor(...initArgss[i]).request()); await new BatchCall(wallet, calls).send().wait(); expect(await contracts[0].methods.summed_values(owner).view()).toEqual(42n); expect(await contracts[1].methods.summed_values(owner).view()).toEqual(52n); @@ -215,7 +210,7 @@ describe('e2e_deploy_contract', () => { it.skip('initializes and calls a private function in a single tx', async () => { const owner = await registerRandomAccount(pxe); const initArgs: StatefulContractCtorArgs = [owner, 42]; - const contract = await registerContract(wallet, StatefulTestContract, initArgs); + const contract = await registerContract(wallet, StatefulTestContract, { initArgs }); const batch = new BatchCall(wallet, [ contract.methods.constructor(...initArgs).request(), contract.methods.create_note(owner, 10).request(), @@ -228,7 +223,7 @@ describe('e2e_deploy_contract', () => { it('refuses to initialize a contract twice', async () => { const owner = await registerRandomAccount(pxe); const initArgs: StatefulContractCtorArgs = [owner, 42]; - const contract = await registerContract(wallet, StatefulTestContract, initArgs); + const contract = await registerContract(wallet, StatefulTestContract, { initArgs }); await contract.methods .constructor(...initArgs) .send() @@ -244,12 +239,20 @@ describe('e2e_deploy_contract', () => { it('refuses to call a private function that requires initialization', async () => { const owner = await registerRandomAccount(pxe); const initArgs: StatefulContractCtorArgs = [owner, 42]; - const contract = await registerContract(wallet, StatefulTestContract, initArgs); + const contract = await registerContract(wallet, StatefulTestContract, { initArgs }); // TODO(@spalladino): It'd be nicer to be able to fail the assert with a more descriptive message. await expect(contract.methods.create_note(owner, 10).send().wait()).rejects.toThrow( /nullifier witness not found/i, ); }); + + it('refuses to initialize a contract with incorrect args', async () => { + const owner = await registerRandomAccount(pxe); + const contract = await registerContract(wallet, StatefulTestContract, { initArgs: [owner, 42] }); + await expect(contract.methods.constructor(owner, 43).simulate()).rejects.toThrow( + /Initialization hash does not match/, + ); + }); }); describe('registering a contract class', () => { @@ -299,12 +302,18 @@ describe('e2e_deploy_contract', () => { let initArgs: StatefulContractCtorArgs; let contract: StatefulTestContract; - const deployInstance = async () => { + const deployInstance = async (opts: { constructorName?: string } = {}) => { const initArgs = [accounts[0].address, 42] as StatefulContractCtorArgs; const salt = Fr.random(); const portalAddress = EthAddress.random(); const publicKey = Point.random(); - const instance = getContractInstanceFromDeployParams(artifact, initArgs, salt, publicKey, portalAddress); + const instance = getContractInstanceFromDeployParams(artifact, { + constructorArgs: initArgs, + salt, + publicKey, + portalAddress, + constructorName: opts.constructorName, + }); const { address, contractClassId } = instance; logger(`Deploying contract instance at ${address.toString()} class id ${contractClassId.toString()}`); await deployFn(instance); @@ -318,87 +327,113 @@ describe('e2e_deploy_contract', () => { // we are not going to run those - but this may require registering "partial" contracts in the pxe. // Anyway, when we implement that, we should be able to replace this `registerContract` with // a simpler `Contract.at(instance.address, wallet)`. - const registered = await registerContract(wallet, StatefulTestContract, initArgs, { + const registered = await registerContract(wallet, StatefulTestContract, { + constructorName: opts.constructorName, salt: instance.salt, portalAddress: instance.portalContractAddress, publicKey, + initArgs, }); expect(registered.address).toEqual(instance.address); const contract = await StatefulTestContract.at(instance.address, wallet); return { contract, initArgs, instance, publicKey }; }; - beforeAll(async () => { - ({ instance, initArgs, contract } = await deployInstance()); - }, 60_000); - - it('stores contract instance in the aztec node', async () => { - const deployed = await aztecNode.getContract(instance.address); - expect(deployed).toBeDefined(); - expect(deployed!.address).toEqual(instance.address); - expect(deployed!.contractClassId).toEqual(contractClass.id); - expect(deployed!.initializationHash).toEqual(instance.initializationHash); - expect(deployed!.portalContractAddress).toEqual(instance.portalContractAddress); - expect(deployed!.publicKeysHash).toEqual(instance.publicKeysHash); - expect(deployed!.salt).toEqual(instance.salt); + describe('using a private constructor', () => { + beforeAll(async () => { + ({ instance, initArgs, contract } = await deployInstance()); + }, 60_000); + + it('stores contract instance in the aztec node', async () => { + const deployed = await aztecNode.getContract(instance.address); + expect(deployed).toBeDefined(); + expect(deployed!.address).toEqual(instance.address); + expect(deployed!.contractClassId).toEqual(contractClass.id); + expect(deployed!.initializationHash).toEqual(instance.initializationHash); + expect(deployed!.portalContractAddress).toEqual(instance.portalContractAddress); + expect(deployed!.publicKeysHash).toEqual(instance.publicKeysHash); + expect(deployed!.salt).toEqual(instance.salt); + }); + + it('calls a public function with no init check on the deployed instance', async () => { + const whom = AztecAddress.random(); + await contract.methods + .increment_public_value_no_init_check(whom, 10) + .send({ skipPublicSimulation: true }) + .wait(); + const stored = await contract.methods.get_public_value(whom).view(); + expect(stored).toEqual(10n); + }, 30_000); + + it('refuses to call a public function with init check if the instance is not initialized', async () => { + const whom = AztecAddress.random(); + await contract.methods.increment_public_value(whom, 10).send({ skipPublicSimulation: true }).wait(); + + // TODO(#4972) check for reverted flag + // Meanwhile we check we didn't increment the value + expect(await contract.methods.get_public_value(whom).view()).toEqual(0n); + }, 30_000); + + it('refuses to initialize the instance with wrong args via a private function', async () => { + await expect(contract.methods.constructor(AztecAddress.random(), 43).simulate()).rejects.toThrow( + /initialization hash does not match/i, + ); + }, 30_000); + + it('initializes the contract and calls a public function', async () => { + await contract.methods + .constructor(...initArgs) + .send() + .wait(); + const whom = AztecAddress.random(); + await contract.methods.increment_public_value(whom, 10).send({ skipPublicSimulation: true }).wait(); + const stored = await contract.methods.get_public_value(whom).view(); + expect(stored).toEqual(10n); + }, 30_000); + + it('refuses to reinitialize the contract', async () => { + await expect( + contract.methods + .constructor(...initArgs) + .send({ skipPublicSimulation: true }) + .wait(), + ).rejects.toThrow(/dropped/i); + }, 30_000); }); - it('calls a public function with no init check on the deployed instance', async () => { - const whom = AztecAddress.random(); - await contract.methods - .increment_public_value_no_init_check(whom, 10) - .send({ skipPublicSimulation: true }) - .wait(); - const stored = await contract.methods.get_public_value(whom).view(); - expect(stored).toEqual(10n); - }, 30_000); - - it('refuses to call a public function with init check if the instance is not initialized', async () => { - // TODO(#4972) check for reverted flag - const receipt = await contract.methods - .increment_public_value(AztecAddress.random(), 10) - .send({ skipPublicSimulation: true }) - .wait(); - - expect(receipt.status).toEqual(TxStatus.MINED); - }, 30_000); - - it('calls a public function with init check after initialization', async () => { - await contract.methods - .constructor(...initArgs) - .send() - .wait(); - const whom = AztecAddress.random(); - await contract.methods.increment_public_value(whom, 10).send({ skipPublicSimulation: true }).wait(); - const stored = await contract.methods.get_public_value(whom).view(); - expect(stored).toEqual(10n); - }, 30_000); - - it('refuses to reinitialize the contract', async () => { - await expect( - contract.methods + describe('using a public constructor', () => { + beforeAll(async () => { + ({ instance, initArgs, contract } = await deployInstance({ constructorName: 'public_constructor' })); + }, 60_000); + + it('refuses to initialize the instance with wrong args via a public function', async () => { + // TODO(@spalladino): This tx is mined but reverts, we need to check revert flag once it's available + // Meanwhile, we check that its side effects did not come through as a means to assert it reverted + const whom = AztecAddress.random(); + await contract.methods.public_constructor(whom, 43).send({ skipPublicSimulation: true }).wait(); + expect(await contract.methods.get_public_value(whom).view()).toEqual(0n); + }, 30_000); + + it('initializes the contract and calls a public function', async () => { + await contract.methods .public_constructor(...initArgs) - .send({ skipPublicSimulation: true }) - .wait(), - ).rejects.toThrow(/dropped/i); - }, 30_000); - - it('initializes a new instance of the contract via a public function', async () => { - const { contract, initArgs } = await deployInstance(); - const whom = initArgs[0]; - logger.info(`Initializing contract at ${contract.address} via a public function`); - await contract.methods - .public_constructor(...initArgs) - .send({ skipPublicSimulation: true }) - .wait(); - expect(await contract.methods.get_public_value(whom).view()).toEqual(42n); - logger.info(`Calling a public function that requires initialization on ${contract.address}`); - await contract.methods.increment_public_value(whom, 10).send().wait(); - expect(await contract.methods.get_public_value(whom).view()).toEqual(52n); - logger.info(`Calling a private function that requires initialization on ${contract.address}`); - await contract.methods.create_note(whom, 10).send().wait(); - expect(await contract.methods.summed_values(whom).view()).toEqual(10n); - }, 90_000); + .send() + .wait(); + const whom = AztecAddress.random(); + await contract.methods.increment_public_value(whom, 10).send({ skipPublicSimulation: true }).wait(); + const stored = await contract.methods.get_public_value(whom).view(); + expect(stored).toEqual(10n); + }, 30_000); + + it('refuses to reinitialize the contract', async () => { + await expect( + contract.methods + .public_constructor(...initArgs) + .send({ skipPublicSimulation: true }) + .wait(), + ).rejects.toThrow(/dropped/i); + }, 30_000); + }); }); testDeployingAnInstance('from a wallet', async instance => { @@ -452,11 +487,11 @@ describe('e2e_deploy_contract', () => { }, 60_000); it.skip('publicly deploys and calls a public function in the same batched call', async () => { - // TODO(@spalladino) + // TODO(@spalladino): Requires being able to read a nullifier on the same tx it was emitted. }); it.skip('publicly deploys and calls a public function in a tx in the same block', async () => { - // TODO(@spalladino) + // TODO(@spalladino): Requires being able to read a nullifier on the same block it was emitted. }); }); }); @@ -477,11 +512,16 @@ type ContractArtifactClass = { async function registerContract( wallet: Wallet, contractArtifact: ContractArtifactClass, - args: any[] = [], - opts: { salt?: Fr; publicKey?: Point; portalAddress?: EthAddress } = {}, + opts: { salt?: Fr; publicKey?: Point; portalAddress?: EthAddress; initArgs?: any[]; constructorName?: string } = {}, ): Promise { - const { salt, publicKey, portalAddress } = opts; - const instance = getContractInstanceFromDeployParams(contractArtifact.artifact, args, salt, publicKey, portalAddress); + const { salt, publicKey, portalAddress, initArgs, constructorName } = opts; + const instance = getContractInstanceFromDeployParams(contractArtifact.artifact, { + constructorArgs: initArgs ?? [], + constructorName, + salt, + publicKey, + portalAddress, + }); await wallet.addContracts([{ artifact: contractArtifact.artifact, instance }]); return contractArtifact.at(instance.address, wallet); } diff --git a/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts b/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts index 9df22ace0614..763ad54c6d74 100644 --- a/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts @@ -4,7 +4,6 @@ import { BatchCall, CompleteAddress, DebugLogger, - EthAddress, ExtendedNote, Fr, GrumpkinPrivateKey, @@ -59,13 +58,11 @@ describe('e2e_escrow_contract', () => { escrowPrivateKey = GrumpkinScalar.random(); escrowPublicKey = generatePublicKey(escrowPrivateKey); const salt = Fr.random(); - const deployInfo = getContractInstanceFromDeployParams( - EscrowContractArtifact, - [owner], + const deployInfo = getContractInstanceFromDeployParams(EscrowContractArtifact, { + constructorArgs: [owner], salt, - escrowPublicKey, - EthAddress.ZERO, - ); + publicKey: escrowPublicKey, + }); await pxe.registerAccount(escrowPrivateKey, computePartialAddress(deployInfo)); escrowContract = await EscrowContract.deployWithPublicKey(escrowPublicKey, wallet, owner) diff --git a/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts b/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts index 17361bdce81a..fcb4959f8fc6 100644 --- a/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts @@ -273,7 +273,7 @@ describe('e2e_inclusion_proofs_contract', () => { it('proves public deployment of a contract', async () => { // Publicly deploy another contract (so we don't test on the same contract) const initArgs = [accounts[0], 42n]; - const instance = getContractInstanceFromDeployParams(StatefulTestContractArtifact, initArgs); + const instance = getContractInstanceFromDeployParams(StatefulTestContractArtifact, { constructorArgs: initArgs }); await (await registerContractClass(wallets[0], StatefulTestContractArtifact)).send().wait(); const receipt = await deployInstance(wallets[0], instance).send().wait(); diff --git a/yarn-project/end-to-end/src/sample-dapp/index.mjs b/yarn-project/end-to-end/src/sample-dapp/index.mjs index f15c8d1c638d..73bf03685de5 100644 --- a/yarn-project/end-to-end/src/sample-dapp/index.mjs +++ b/yarn-project/end-to-end/src/sample-dapp/index.mjs @@ -103,7 +103,7 @@ async function mintPublicFunds(pxe) { // docs:start:showLogs const blockNumber = await pxe.getBlockNumber(); const logs = (await pxe.getUnencryptedLogs(blockNumber, 1)).logs; - const textLogs = logs.map(extendedLog => extendedLog.log.data.toString('ascii')); + const textLogs = logs.map(extendedLog => extendedLog.toHumanReadable().slice(0, 200)); for (const log of textLogs) console.log(`Log emitted: ${log}`); // docs:end:showLogs } diff --git a/yarn-project/protocol-contracts/src/protocol_contract.ts b/yarn-project/protocol-contracts/src/protocol_contract.ts index c3fa3c7d50da..8d7e4ccb371f 100644 --- a/yarn-project/protocol-contracts/src/protocol_contract.ts +++ b/yarn-project/protocol-contracts/src/protocol_contract.ts @@ -24,19 +24,18 @@ export interface ProtocolContract { export function getCanonicalProtocolContract( artifact: ContractArtifact, salt: Fr | number | bigint, - initArgs: any[] = [], + constructorArgs: any[] = [], publicKey: Point = Point.ZERO, - portalContractAddress = EthAddress.ZERO, + portalAddress = EthAddress.ZERO, ): ProtocolContract { // TODO(@spalladino): This computes the contract class from the artifact twice. const contractClass = getContractClassFromArtifact(artifact); - const instance = getContractInstanceFromDeployParams( - artifact, - initArgs, - new Fr(salt), + const instance = getContractInstanceFromDeployParams(artifact, { + constructorArgs, + salt: new Fr(salt), publicKey, - portalContractAddress, - ); + portalAddress, + }); return { instance, contractClass, diff --git a/yarn-project/sequencer-client/src/simulator/public_executor.ts b/yarn-project/sequencer-client/src/simulator/public_executor.ts index 9af59f7729be..44556483d27b 100644 --- a/yarn-project/sequencer-client/src/simulator/public_executor.ts +++ b/yarn-project/sequencer-client/src/simulator/public_executor.ts @@ -79,6 +79,10 @@ export class ContractsDataSourcePublicDB implements PublicContractsDB { return Promise.resolve(); } + public async getContractInstance(address: AztecAddress): Promise { + return this.instanceCache.get(address.toString()) ?? (await this.db.getContract(address)); + } + async getBytecode(address: AztecAddress, selector: FunctionSelector): Promise { const contract = await this.#getContract(address); return contract?.getPublicFunction(selector)?.bytecode; diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 22eaf2f29386..0a6d350d386d 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -69,6 +69,12 @@ export class MessageLoadOracleInputs { } } +class OracleMethodNotAvailableError extends Error { + constructor(methodName: string) { + super(`Oracle method ${methodName} is not available.`); + } +} + /** * Oracle with typed parameters and typed return values. * Methods that require read and/or write will have to be implemented based on the context (public, private, or view) @@ -80,58 +86,58 @@ export abstract class TypedOracle { } packArguments(_args: Fr[]): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('packArguments'); } getNullifierKeyPair(_accountAddress: AztecAddress): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getNullifierKeyPair'); } getPublicKeyAndPartialAddress(_address: AztecAddress): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getPublicKeyAndPartialAddress'); } getContractInstance(_address: AztecAddress): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getContractInstance'); } getMembershipWitness(_blockNumber: number, _treeId: MerkleTreeId, _leafValue: Fr): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getMembershipWitness'); } getSiblingPath(_blockNumber: number, _treeId: MerkleTreeId, _leafIndex: Fr): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getSiblingPath'); } getNullifierMembershipWitness(_blockNumber: number, _nullifier: Fr): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getNullifierMembershipWitness'); } getPublicDataTreeWitness(_blockNumber: number, _leafSlot: Fr): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getPublicDataTreeWitness'); } getLowNullifierMembershipWitness( _blockNumber: number, _nullifier: Fr, ): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getLowNullifierMembershipWitness'); } getHeader(_blockNumber: number): Promise
{ - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getHeader'); } getCompleteAddress(_address: AztecAddress): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getCompleteAddress'); } getAuthWitness(_messageHash: Fr): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getAuthWitness'); } popCapsule(): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('popCapsule'); } getNotes( @@ -146,35 +152,35 @@ export abstract class TypedOracle { _offset: number, _status: NoteStatus, ): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getNotes'); } notifyCreatedNote(_storageSlot: Fr, _noteTypeId: Fr, _note: Fr[], _innerNoteHash: Fr): void { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('notifyCreatedNote'); } notifyNullifiedNote(_innerNullifier: Fr, _innerNoteHash: Fr): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('notifyNullifiedNote'); } checkNullifierExists(_innerNullifier: Fr): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('checkNullifierExists'); } getL1ToL2MembershipWitness(_entryKey: Fr): Promise> { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getL1ToL2MembershipWitness'); } getPortalContractAddress(_contractAddress: AztecAddress): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('getPortalContractAddress'); } storageRead(_startStorageSlot: Fr, _numberOfElements: number): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('storageRead'); } storageWrite(_startStorageSlot: Fr, _values: Fr[]): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('storageWrite'); } emitEncryptedLog( @@ -184,11 +190,11 @@ export abstract class TypedOracle { _publicKey: PublicKey, _log: Fr[], ): void { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('emitEncryptedLog'); } emitUnencryptedLog(_log: UnencryptedL2Log): void { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('emitUnencryptedLog'); } callPrivateFunction( @@ -199,7 +205,7 @@ export abstract class TypedOracle { _isStaticCall: boolean, _isDelegateCall: boolean, ): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('callPrivateFunction'); } callPublicFunction( @@ -209,7 +215,7 @@ export abstract class TypedOracle { _isStaticCall: boolean, _isDelegateCall: boolean, ): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('callPublicFunction'); } enqueuePublicFunctionCall( @@ -220,6 +226,6 @@ export abstract class TypedOracle { _isStaticCall: boolean, _isDelegateCall: boolean, ): Promise { - throw new Error('Not available.'); + throw new OracleMethodNotAvailableError('enqueuePublicFunctionCall'); } } diff --git a/yarn-project/simulator/src/client/private_execution.test.ts b/yarn-project/simulator/src/client/private_execution.test.ts index 736d147b66f6..56921a181241 100644 --- a/yarn-project/simulator/src/client/private_execution.test.ts +++ b/yarn-project/simulator/src/client/private_execution.test.ts @@ -15,6 +15,7 @@ import { computeNullifierSecretKey, computeSiloedNullifierSecretKey, derivePublicKey, + getContractInstanceFromDeployParams, nonEmptySideEffects, sideEffectArrayToValueArray, } from '@aztec/circuits.js'; @@ -117,13 +118,7 @@ describe('Private Execution test suite', () => { authWitnesses: [], }); - return acirSimulator.run( - txRequest, - artifact, - functionData.isConstructor ? AztecAddress.ZERO : contractAddress, - portalContractAddress, - msgSender, - ); + return acirSimulator.run(txRequest, artifact, contractAddress, portalContractAddress, msgSender); }; const insertLeaves = async (leaves: Fr[], name = 'noteHash') => { @@ -298,8 +293,11 @@ describe('Private Execution test suite', () => { }); it('should have a constructor with arguments that inserts notes', async () => { + const initArgs = [owner, 140]; + const instance = getContractInstanceFromDeployParams(StatefulTestContractArtifact, { constructorArgs: initArgs }); + oracle.getContractInstance.mockResolvedValue(instance); const artifact = getFunctionArtifact(StatefulTestContractArtifact, 'constructor'); - const topLevelResult = await runSimulator({ args: [owner, 140], artifact }); + const topLevelResult = await runSimulator({ args: initArgs, artifact, contractAddress: instance.address }); const result = topLevelResult.nestedExecutions[0]; expect(result.newNotes).toHaveLength(1); diff --git a/yarn-project/simulator/src/public/db.ts b/yarn-project/simulator/src/public/db.ts index 316b14d67525..fedbfe55c155 100644 --- a/yarn-project/simulator/src/public/db.ts +++ b/yarn-project/simulator/src/public/db.ts @@ -2,6 +2,7 @@ import { NullifierMembershipWitness } from '@aztec/circuit-types'; import { EthAddress, FunctionSelector, L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; +import { ContractInstanceWithAddress } from '@aztec/types/contracts'; import { MessageLoadOracleInputs } from '../acvm/index.js'; @@ -65,6 +66,13 @@ export interface PublicContractsDB { * @returns The portal contract address or undefined if not found. */ getPortalContractAddress(address: AztecAddress): Promise; + + /** + * Returns a publicly deployed contract instance. + * @param address - Address of the contract. + * @returns The contract instance or undefined if not found. + */ + getContractInstance(address: AztecAddress): Promise; } /** Database interface for providing access to commitment tree, l1 to l2 message tree, and nullifier tree. */ diff --git a/yarn-project/simulator/src/public/public_execution_context.ts b/yarn-project/simulator/src/public/public_execution_context.ts index 8d332e8edb72..d86e20e78c91 100644 --- a/yarn-project/simulator/src/public/public_execution_context.ts +++ b/yarn-project/simulator/src/public/public_execution_context.ts @@ -4,6 +4,7 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; +import { ContractInstance } from '@aztec/types/contracts'; import { TypedOracle, toACVMWitness } from '../acvm/index.js'; import { PackedArgsCache, SideEffectCounter } from '../common/index.js'; @@ -237,4 +238,17 @@ export class PublicExecutionContext extends TypedOracle { } return await this.commitmentsDb.getNullifierMembershipWitnessAtLatestBlock(nullifier); } + + public async getContractInstance(address: AztecAddress): Promise { + // Note to AVM implementor: The wrapper of the oracle call get_contract_instance in aztec-nr + // automatically checks that the returned instance is correct, by hashing it together back + // into the address. However, in the AVM, we also need to prove the negative, otherwise a malicious + // sequencer could just lie about not having the instance available in its local db. We can do this + // by using the prove_contract_non_deployment_at method if the contract is not found in the db. + const instance = await this.contractsDb.getContractInstance(address); + if (!instance) { + throw new Error(`Contract instance at ${address} not found`); + } + return instance; + } }