From 407985c227b05d1d24eb1878489093de95e0045d Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 11 Apr 2024 15:18:57 -0300 Subject: [PATCH 1/3] chore(ci): Break e2e-deploy into multiple test suites --- .circleci/config.yml | 2 +- .../src/e2e_deploy_contract.test.ts | 643 ------------------ .../contract_class_registration.test.ts | 268 ++++++++ .../e2e_deploy_contract/deploy_method.test.ts | 89 +++ .../src/e2e_deploy_contract/deploy_test.ts | 100 +++ .../src/e2e_deploy_contract/legacy.test.ts | 131 ++++ .../private_initialization.test.ts | 124 ++++ .../e2e_deploy_contract/regressions.test.ts | 27 + .../{sheilding.test.ts => shielding.test.ts} | 0 ...nsheilding.test.ts => unshielding.test.ts} | 0 10 files changed, 740 insertions(+), 644 deletions(-) delete mode 100644 yarn-project/end-to-end/src/e2e_deploy_contract.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_deploy_contract/contract_class_registration.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts create mode 100644 yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_deploy_contract/regressions.test.ts rename yarn-project/end-to-end/src/e2e_token_contract/{sheilding.test.ts => shielding.test.ts} (100%) rename yarn-project/end-to-end/src/e2e_token_contract/{unsheilding.test.ts => unshielding.test.ts} (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9a6268b2c75..abbe9172527 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -695,7 +695,7 @@ jobs: - *setup_env - run: name: "Test" - command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_deploy_contract.test.ts + command: cond_spot_run_container end-to-end 4 ./src/e2e_deploy_contract/ aztec_manifest_key: end-to-end <<: *defaults_e2e_test 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 deleted file mode 100644 index c5f02ed116f..00000000000 --- a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts +++ /dev/null @@ -1,643 +0,0 @@ -import { getDeployedTestAccountsWallets } from '@aztec/accounts/testing'; -import { - AztecAddress, - type AztecNode, - BatchCall, - CompleteAddress, - type ContractArtifact, - type ContractBase, - type ContractClassWithId, - ContractDeployer, - type ContractInstanceWithAddress, - type DebugLogger, - EthAddress, - Fr, - type PXE, - SignerlessWallet, - TxStatus, - type Wallet, - createPXEClient, - getContractClassFromArtifact, - getContractInstanceFromDeployParams, - makeFetch, -} from '@aztec/aztec.js'; -import { - broadcastPrivateFunction, - broadcastUnconstrainedFunction, - deployInstance, - registerContractClass, -} from '@aztec/aztec.js/deployment'; -import { type ContractClassIdPreimage, Point } from '@aztec/circuits.js'; -import { siloNullifier } from '@aztec/circuits.js/hash'; -import { FunctionSelector, FunctionType } from '@aztec/foundation/abi'; -import { writeTestData } from '@aztec/foundation/testing'; -import { CounterContract, StatefulTestContract } from '@aztec/noir-contracts.js'; -import { TestContract, TestContractArtifact } from '@aztec/noir-contracts.js/Test'; -import { TokenContract, TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; -import { type SequencerClient } from '@aztec/sequencer-client'; - -import { setup } from './fixtures/utils.js'; - -describe('e2e_deploy_contract', () => { - let pxe: PXE; - let logger: DebugLogger; - let wallet: Wallet; - let sequencer: SequencerClient | undefined; - let aztecNode: AztecNode; - let teardown: () => Promise; - - describe('legacy tests', () => { - beforeAll(async () => { - ({ teardown, pxe, logger, wallet, sequencer, aztecNode } = await setup()); - }, 100_000); - - afterAll(() => teardown()); - - /** - * Milestone 1.1. - * https://hackmd.io/ouVCnacHQRq2o1oRc5ksNA#Interfaces-and-Responsibilities - */ - it('should deploy a test contract', async () => { - const salt = Fr.random(); - const publicKey = wallet.getCompleteAddress().publicKey; - const deploymentData = getContractInstanceFromDeployParams(TestContractArtifact, { - salt, - publicKey, - deployer: wallet.getAddress(), - }); - const deployer = new ContractDeployer(TestContractArtifact, wallet, publicKey); - const receipt = await deployer.deploy().send({ contractAddressSalt: salt }).wait({ wallet }); - expect(receipt.contract.address).toEqual(deploymentData.address); - expect(await pxe.getContractInstance(deploymentData.address)).toBeDefined(); - expect(await pxe.isContractPubliclyDeployed(deploymentData.address)).toBeDefined(); - }, 60_000); - - /** - * Verify that we can produce multiple rollups. - */ - it('should deploy one contract after another in consecutive rollups', async () => { - const deployer = new ContractDeployer(TestContractArtifact, wallet); - - for (let index = 0; index < 2; index++) { - logger.info(`Deploying contract ${index + 1}...`); - await deployer.deploy().send({ contractAddressSalt: Fr.random() }).wait({ wallet }); - } - }, 60_000); - - /** - * Verify that we can deploy multiple contracts and interact with all of them. - */ - it('should deploy multiple contracts and interact with them', async () => { - const deployer = new ContractDeployer(TestContractArtifact, wallet); - - for (let index = 0; index < 2; index++) { - logger.info(`Deploying contract ${index + 1}...`); - const receipt = await deployer.deploy().send({ contractAddressSalt: Fr.random() }).wait({ wallet }); - logger.info(`Sending TX to contract ${index + 1}...`); - await receipt.contract.methods.get_public_key(wallet.getAddress()).send().wait(); - } - }, 90_000); - - /** - * Milestone 1.2. - * https://hackmd.io/-a5DjEfHTLaMBR49qy6QkA - */ - it('should not deploy a contract with the same salt twice', async () => { - const contractAddressSalt = Fr.random(); - const deployer = new ContractDeployer(TestContractArtifact, wallet); - - await deployer.deploy().send({ contractAddressSalt }).wait({ wallet }); - await expect(deployer.deploy().send({ contractAddressSalt }).wait()).rejects.toThrow(/dropped/); - }, 60_000); - - it('should deploy a contract connected to a portal contract', async () => { - const deployer = new ContractDeployer(TestContractArtifact, wallet); - const portalContract = EthAddress.random(); - - // ContractDeployer was instantiated with wallet so we don't have to pass it to wait(...) - const receipt = await deployer.deploy().send({ portalContract }).wait(); - const address = receipt.contract.address; - - const expectedPortal = portalContract.toString(); - expect((await pxe.getContractInstance(address))?.portalContractAddress.toString()).toEqual(expectedPortal); - }, 60_000); - - it('should not deploy a contract which failed the public part of the execution', async () => { - sequencer?.updateSequencerConfig({ minTxsPerBlock: 2 }); - try { - // This test requires at least another good transaction to go through in the same block as the bad one. - const artifact = TokenContractArtifact; - const initArgs = ['TokenName', 'TKN', 18] as const; - const goodDeploy = StatefulTestContract.deploy(wallet, wallet.getAddress(), 42); - const badDeploy = new ContractDeployer(artifact, wallet).deploy(AztecAddress.ZERO, ...initArgs); - - const firstOpts = { skipPublicSimulation: true, skipClassRegistration: true, skipInstanceDeploy: true }; - const secondOpts = { skipPublicSimulation: true }; - - await Promise.all([goodDeploy.prove(firstOpts), badDeploy.prove(secondOpts)]); - const [goodTx, badTx] = [goodDeploy.send(firstOpts), badDeploy.send(secondOpts)]; - const [goodTxPromiseResult, badTxReceiptResult] = await Promise.allSettled([ - goodTx.wait(), - badTx.wait({ dontThrowOnRevert: true }), - ]); - - expect(goodTxPromiseResult.status).toBe('fulfilled'); - expect(badTxReceiptResult.status).toBe('fulfilled'); // but reverted - - const [goodTxReceipt, badTxReceipt] = await Promise.all([goodTx.getReceipt(), badTx.getReceipt()]); - - // Both the good and bad transactions are included - expect(goodTxReceipt.blockNumber).toEqual(expect.any(Number)); - expect(badTxReceipt.blockNumber).toEqual(expect.any(Number)); - - expect(badTxReceipt.status).toEqual(TxStatus.REVERTED); - - // But the bad tx did not deploy - await expect(pxe.isContractClassPubliclyRegistered(badDeploy.getInstance().address)).resolves.toBeFalsy(); - } finally { - sequencer?.updateSequencerConfig({ minTxsPerBlock: 1 }); - } - }, 90_000); - }); - - describe('regressions', () => { - beforeAll(async () => { - ({ teardown, pxe, logger, wallet, sequencer, aztecNode } = await setup()); - }, 100_000); - afterAll(() => teardown()); - - it('fails properly when trying to deploy a contract with a failing constructor with a pxe client with retries', async () => { - const { PXE_URL } = process.env; - if (!PXE_URL) { - return; - } - const pxeClient = createPXEClient(PXE_URL, makeFetch([1, 2, 3], false)); - const [wallet] = await getDeployedTestAccountsWallets(pxeClient); - await expect( - StatefulTestContract.deployWithOpts({ wallet, method: 'wrong_constructor' }).send().deployed(), - ).rejects.toThrow(/Unknown function/); - }); - }); - - describe('private initialization', () => { - beforeAll(async () => { - ({ teardown, pxe, logger, wallet, sequencer, aztecNode } = await setup()); - }, 100_000); - afterAll(() => teardown()); - - // Tests calling a private function in an uninitialized and undeployed contract. Note that - // it still requires registering the contract artifact and instance locally in the pxe. - test.each(['as entrypoint', 'from an account contract'] as const)( - 'executes a function in an undeployed contract %s', - async kind => { - const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet; - const contract = await registerContract(testWallet, TestContract); - const receipt = await contract.methods.emit_nullifier(10).send().wait({ debug: true }); - const expected = siloNullifier(contract.address, new Fr(10)); - expect(receipt.debugInfo?.nullifiers[1]).toEqual(expected); - }, - 30_000, - ); - - // Tests privately initializing an undeployed contract. Also requires pxe registration in advance. - test.each(['as entrypoint', 'from an account contract'] as const)( - 'privately initializes an undeployed contract contract %s', - async kind => { - 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 }); - logger.info(`Calling the constructor for ${contract.address}`); - await contract.methods - .constructor(...initArgs) - .send() - .wait(); - logger.info(`Checking if the constructor was run for ${contract.address}`); - expect(await contract.methods.summed_values(owner).simulate()).toEqual(42n); - logger.info(`Calling a private function that requires initialization on ${contract.address}`); - await contract.methods.create_note(owner, 10).send().wait(); - expect(await contract.methods.summed_values(owner).simulate()).toEqual(52n); - }, - 30_000, - ); - - // 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 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).simulate()).toEqual(42n); - expect(await contracts[1].methods.summed_values(owner).simulate()).toEqual(52n); - }, 30_000); - - // TODO(@spalladino): This won't work until we can read a nullifier in the same tx in which it was emitted. - 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 batch = new BatchCall(wallet, [ - contract.methods.constructor(...initArgs).request(), - contract.methods.create_note(owner, 10).request(), - ]); - logger.info(`Executing constructor and private function in batch at ${contract.address}`); - await batch.send().wait(); - expect(await contract.methods.summed_values(owner).simulate()).toEqual(52n); - }); - - 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 }); - await contract.methods - .constructor(...initArgs) - .send() - .wait(); - await expect( - contract.methods - .constructor(...initArgs) - .send() - .wait(), - ).rejects.toThrow(/dropped/); - }); - - 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 }); - // 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).prove()).rejects.toThrow( - /Initialization hash does not match/, - ); - }); - - it('refuses to initialize an instance from a different deployer', async () => { - const owner = await registerRandomAccount(pxe); - const contract = await registerContract(wallet, StatefulTestContract, { initArgs: [owner, 42], deployer: owner }); - await expect(contract.methods.constructor(owner, 42).prove()).rejects.toThrow( - /Initializer address is not the contract deployer/i, - ); - }); - }); - - describe('registering a contract class', () => { - beforeAll(async () => { - ({ teardown, pxe, logger, wallet, sequencer, aztecNode } = await setup()); - }, 100_000); - afterAll(() => teardown()); - - let artifact: ContractArtifact; - let contractClass: ContractClassWithId & ContractClassIdPreimage; - - beforeAll(async () => { - artifact = StatefulTestContract.artifact; - await registerContractClass(wallet, artifact).then(c => c.send().wait()); - contractClass = getContractClassFromArtifact(artifact); - }, 60_000); - - it('registers the contract class on the node', async () => { - const registeredClass = await aztecNode.getContractClass(contractClass.id); - expect(registeredClass).toBeDefined(); - expect(registeredClass!.artifactHash.toString()).toEqual(contractClass.artifactHash.toString()); - expect(registeredClass!.privateFunctionsRoot.toString()).toEqual(contractClass.privateFunctionsRoot.toString()); - expect(registeredClass!.packedBytecode.toString('hex')).toEqual(contractClass.packedBytecode.toString('hex')); - expect(registeredClass!.publicFunctions).toEqual(contractClass.publicFunctions); - expect(registeredClass!.privateFunctions).toEqual([]); - }); - - it('broadcasts a private function', async () => { - const selector = contractClass.privateFunctions[0].selector; - const tx = await broadcastPrivateFunction(wallet, artifact, selector).send().wait(); - const logs = await pxe.getUnencryptedLogs({ txHash: tx.txHash }); - const logData = logs.logs[0].log.data; - writeTestData('yarn-project/circuits.js/fixtures/PrivateFunctionBroadcastedEventData.hex', logData); - - const fetchedClass = await aztecNode.getContractClass(contractClass.id); - const fetchedFunction = fetchedClass!.privateFunctions[0]!; - expect(fetchedFunction).toBeDefined(); - expect(fetchedFunction.selector).toEqual(selector); - }, 60_000); - - it('broadcasts an unconstrained function', async () => { - const functionArtifact = artifact.functions.find(fn => fn.functionType === FunctionType.UNCONSTRAINED)!; - const selector = FunctionSelector.fromNameAndParameters(functionArtifact); - const tx = await broadcastUnconstrainedFunction(wallet, artifact, selector).send().wait(); - const logs = await pxe.getUnencryptedLogs({ txHash: tx.txHash }); - const logData = logs.logs[0].log.data; - writeTestData('yarn-project/circuits.js/fixtures/UnconstrainedFunctionBroadcastedEventData.hex', logData); - - const fetchedClass = await aztecNode.getContractClass(contractClass.id); - const fetchedFunction = fetchedClass!.unconstrainedFunctions[0]!; - expect(fetchedFunction).toBeDefined(); - expect(fetchedFunction.selector).toEqual(selector); - }, 60_000); - - const testDeployingAnInstance = (how: string, deployFn: (toDeploy: ContractInstanceWithAddress) => Promise) => - describe(`deploying a contract instance ${how}`, () => { - let instance: ContractInstanceWithAddress; - let initArgs: StatefulContractCtorArgs; - let contract: StatefulTestContract; - - const deployInstance = async (opts: { constructorName?: string; deployer?: AztecAddress } = {}) => { - const initArgs = [wallet.getAddress(), 42] as StatefulContractCtorArgs; - const salt = Fr.random(); - const portalAddress = EthAddress.random(); - const publicKey = Point.random(); - const instance = getContractInstanceFromDeployParams(artifact, { - constructorArgs: initArgs, - salt, - publicKey, - portalAddress, - constructorArtifact: opts.constructorName, - deployer: opts.deployer, - }); - const { address, contractClassId } = instance; - logger.info(`Deploying contract instance at ${address.toString()} class id ${contractClassId.toString()}`); - await deployFn(instance); - - // TODO(@spalladino) We should **not** need the whole instance, including initArgs and salt, - // in order to interact with a public function for the contract. We may even not need - // all of it for running a private function. Consider removing `instance` as a required - // field in the aztec.js `Contract` class, maybe we can replace it with just the partialAddress. - // Not just that, but this instance has been broadcasted, so the pxe should be able to get - // its information from the node directly, excluding private functions, but it's ok because - // 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, { - constructorName: opts.constructorName, - salt: instance.salt, - portalAddress: instance.portalContractAddress, - publicKey, - initArgs, - deployer: opts.deployer, - }); - expect(registered.address).toEqual(instance.address); - const contract = await StatefulTestContract.at(instance.address, wallet); - return { contract, initArgs, instance, publicKey }; - }; - - 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); - expect(deployed!.deployer).toEqual(instance.deployer); - }); - - 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).simulate(); - 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(); - const receipt = await contract.methods - .increment_public_value(whom, 10) - .send({ skipPublicSimulation: true }) - .wait({ dontThrowOnRevert: true }); - expect(receipt.status).toEqual(TxStatus.REVERTED); - - // Meanwhile we check we didn't increment the value - expect(await contract.methods.get_public_value(whom).simulate()).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).prove()).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).simulate(); - 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); - }); - - 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 () => { - const whom = AztecAddress.random(); - const receipt = await contract.methods - .public_constructor(whom, 43) - .send({ skipPublicSimulation: true }) - .wait({ dontThrowOnRevert: true }); - expect(receipt.status).toEqual(TxStatus.REVERTED); - expect(await contract.methods.get_public_value(whom).simulate()).toEqual(0n); - }, 30_000); - - it('initializes the contract and calls a public function', async () => { - await contract.methods - .public_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).simulate(); - 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 => { - // Calls the deployer contract directly from a wallet - await deployInstance(wallet, instance).send().wait(); - }); - - testDeployingAnInstance('from a contract', async instance => { - // Register the instance to be deployed in the pxe - await wallet.registerContract({ artifact, instance }); - // Set up the contract that calls the deployer (which happens to be the TestContract) and call it - const deployer = await TestContract.deploy(wallet).send().deployed(); - await deployer.methods.deploy_contract(instance.address).send().wait(); - }); - - describe('error scenarios in deployment', () => { - it('refuses to call a public function on an undeployed contract', async () => { - const whom = wallet.getAddress(); - const instance = await registerContract(wallet, StatefulTestContract, { initArgs: [whom, 42] }); - await expect( - instance.methods.increment_public_value_no_init_check(whom, 10).send({ skipPublicSimulation: true }).wait(), - ).rejects.toThrow(/dropped/); - }); - - it('refuses to deploy an instance from a different deployer', () => { - const instance = getContractInstanceFromDeployParams(artifact, { - constructorArgs: [AztecAddress.random(), 42], - deployer: AztecAddress.random(), - }); - expect(() => deployInstance(wallet, instance)).toThrow(/does not match/i); - }); - }); - }); - - describe('using the contract deploy method', () => { - // We use a beforeEach hook so we get a fresh pxe and node, so class registrations - // from one test don't influence the others. - // TODO(@spalladino): The above is only true for locally run e2e tests, on the CI this runs - // on a single sandbox instance, so tests are not truly independent. - beforeEach(async () => { - ({ teardown, pxe, logger, wallet, sequencer, aztecNode } = await setup()); - }, 100_000); - afterEach(() => teardown()); - - it('publicly deploys and initializes a contract', async () => { - const owner = wallet.getAddress(); - logger.debug(`Deploying stateful test contract`); - const contract = await StatefulTestContract.deploy(wallet, owner, 42).send().deployed(); - expect(await contract.methods.summed_values(owner).simulate()).toEqual(42n); - logger.debug(`Calling public method on stateful test contract at ${contract.address.toString()}`); - await contract.methods.increment_public_value(owner, 84).send().wait(); - expect(await contract.methods.get_public_value(owner).simulate()).toEqual(84n); - }, 60_000); - - it('publicly universally deploys and initializes a contract', async () => { - const owner = wallet.getAddress(); - const opts = { universalDeploy: true }; - const contract = await StatefulTestContract.deploy(wallet, owner, 42).send(opts).deployed(); - expect(await contract.methods.summed_values(owner).simulate()).toEqual(42n); - await contract.methods.increment_public_value(owner, 84).send().wait(); - expect(await contract.methods.get_public_value(owner).simulate()).toEqual(84n); - }, 60_000); - - it('publicly deploys and calls a public function from the constructor', async () => { - const owner = wallet.getAddress(); - const token = await TokenContract.deploy(wallet, owner, 'TOKEN', 'TKN', 18).send().deployed(); - expect(await token.methods.is_minter(owner).simulate()).toEqual(true); - }, 60_000); - - it('publicly deploys and initializes via a public function', async () => { - const owner = wallet.getAddress(); - logger.debug(`Deploying contract via a public constructor`); - const contract = await StatefulTestContract.deployWithOpts({ wallet, method: 'public_constructor' }, owner, 42) - .send() - .deployed(); - expect(await contract.methods.get_public_value(owner).simulate()).toEqual(42n); - logger.debug(`Calling a private function to ensure the contract was properly initialized`); - await contract.methods.create_note(owner, 30).send().wait(); - expect(await contract.methods.summed_values(owner).simulate()).toEqual(30n); - }, 60_000); - - it('deploys a contract with a default initializer not named constructor', async () => { - logger.debug(`Deploying contract with a default initializer named initialize`); - const opts = { skipClassRegistration: true, skipPublicDeployment: true }; - const contract = await CounterContract.deploy(wallet, 10, wallet.getAddress()).send(opts).deployed(); - logger.debug(`Calling a function to ensure the contract was properly initialized`); - await contract.methods.increment(wallet.getAddress()).send().wait(); - expect(await contract.methods.get_counter(wallet.getAddress()).simulate()).toEqual(11n); - }); - - it('publicly deploys a contract with no constructor', async () => { - logger.debug(`Deploying contract with no constructor`); - const contract = await TestContract.deploy(wallet).send().deployed(); - logger.debug(`Call a public function to check that it was publicly deployed`); - const receipt = await contract.methods.emit_unencrypted(42).send().wait(); - const logs = await pxe.getUnencryptedLogs({ txHash: receipt.txHash }); - expect(logs.logs[0].log.data.toString('hex').replace(/^0+/, '')).toEqual('2a'); - }); - - it('refuses to deploy a contract with no constructor and no public deployment', async () => { - logger.debug(`Deploying contract with no constructor and skipping public deploy`); - const opts = { skipPublicDeployment: true, skipClassRegistration: true }; - await expect(TestContract.deploy(wallet).prove(opts)).rejects.toThrow(/no function calls needed/i); - }); - - it.skip('publicly deploys and calls a public function in the same batched call', async () => { - // 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): Requires being able to read a nullifier on the same block it was emitted. - }); - }); -}); - -type StatefulContractCtorArgs = Parameters; - -async function registerRandomAccount(pxe: PXE): Promise { - const { completeAddress: owner, privateKey } = CompleteAddress.fromRandomPrivateKey(); - await pxe.registerAccount(privateKey, owner.partialAddress); - return owner.address; -} - -type ContractArtifactClass = { - at(address: AztecAddress, wallet: Wallet): Promise; - artifact: ContractArtifact; -}; - -async function registerContract( - wallet: Wallet, - contractArtifact: ContractArtifactClass, - opts: { - salt?: Fr; - publicKey?: Point; - portalAddress?: EthAddress; - initArgs?: any[]; - constructorName?: string; - deployer?: AztecAddress; - } = {}, -): Promise { - const { salt, publicKey, portalAddress, initArgs, constructorName, deployer } = opts; - const instance = getContractInstanceFromDeployParams(contractArtifact.artifact, { - constructorArgs: initArgs ?? [], - constructorArtifact: constructorName, - salt, - publicKey, - portalAddress, - deployer, - }); - await wallet.registerContract({ artifact: contractArtifact.artifact, instance }); - return contractArtifact.at(instance.address, wallet); -} diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/contract_class_registration.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/contract_class_registration.test.ts new file mode 100644 index 00000000000..5ce50aa7369 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/contract_class_registration.test.ts @@ -0,0 +1,268 @@ +import { + AztecAddress, + type AztecNode, + type ContractArtifact, + type ContractClassWithId, + type ContractInstanceWithAddress, + type DebugLogger, + EthAddress, + Fr, + type PXE, + TxStatus, + type Wallet, + getContractClassFromArtifact, + getContractInstanceFromDeployParams, +} from '@aztec/aztec.js'; +import { + broadcastPrivateFunction, + broadcastUnconstrainedFunction, + deployInstance, + registerContractClass, +} from '@aztec/aztec.js/deployment'; +import { type ContractClassIdPreimage, Point } from '@aztec/circuits.js'; +import { FunctionSelector, FunctionType } from '@aztec/foundation/abi'; +import { writeTestData } from '@aztec/foundation/testing'; +import { StatefulTestContract } from '@aztec/noir-contracts.js'; +import { TestContract } from '@aztec/noir-contracts.js/Test'; + +import { DeployTest, type StatefulContractCtorArgs } from './deploy_test.js'; + +describe('e2e_deploy_contract contract class registration', () => { + const t = new DeployTest('contract class'); + + let pxe: PXE; + let logger: DebugLogger; + let wallet: Wallet; + let aztecNode: AztecNode; + + beforeAll(async () => { + ({ pxe, logger, wallet, aztecNode } = await t.setup()); + }); + + afterAll(() => t.teardown()); + + let artifact: ContractArtifact; + let contractClass: ContractClassWithId & ContractClassIdPreimage; + + beforeAll(async () => { + artifact = StatefulTestContract.artifact; + await registerContractClass(wallet, artifact).then(c => c.send().wait()); + contractClass = getContractClassFromArtifact(artifact); + }, 60_000); + + it('registers the contract class on the node', async () => { + const registeredClass = await aztecNode.getContractClass(contractClass.id); + expect(registeredClass).toBeDefined(); + expect(registeredClass!.artifactHash.toString()).toEqual(contractClass.artifactHash.toString()); + expect(registeredClass!.privateFunctionsRoot.toString()).toEqual(contractClass.privateFunctionsRoot.toString()); + expect(registeredClass!.packedBytecode.toString('hex')).toEqual(contractClass.packedBytecode.toString('hex')); + expect(registeredClass!.publicFunctions).toEqual(contractClass.publicFunctions); + expect(registeredClass!.privateFunctions).toEqual([]); + }); + + it('broadcasts a private function', async () => { + const selector = contractClass.privateFunctions[0].selector; + const tx = await broadcastPrivateFunction(wallet, artifact, selector).send().wait(); + const logs = await pxe.getUnencryptedLogs({ txHash: tx.txHash }); + const logData = logs.logs[0].log.data; + writeTestData('yarn-project/circuits.js/fixtures/PrivateFunctionBroadcastedEventData.hex', logData); + + const fetchedClass = await aztecNode.getContractClass(contractClass.id); + const fetchedFunction = fetchedClass!.privateFunctions[0]!; + expect(fetchedFunction).toBeDefined(); + expect(fetchedFunction.selector).toEqual(selector); + }, 60_000); + + it('broadcasts an unconstrained function', async () => { + const functionArtifact = artifact.functions.find(fn => fn.functionType === FunctionType.UNCONSTRAINED)!; + const selector = FunctionSelector.fromNameAndParameters(functionArtifact); + const tx = await broadcastUnconstrainedFunction(wallet, artifact, selector).send().wait(); + const logs = await pxe.getUnencryptedLogs({ txHash: tx.txHash }); + const logData = logs.logs[0].log.data; + writeTestData('yarn-project/circuits.js/fixtures/UnconstrainedFunctionBroadcastedEventData.hex', logData); + + const fetchedClass = await aztecNode.getContractClass(contractClass.id); + const fetchedFunction = fetchedClass!.unconstrainedFunctions[0]!; + expect(fetchedFunction).toBeDefined(); + expect(fetchedFunction.selector).toEqual(selector); + }, 60_000); + + const testDeployingAnInstance = (how: string, deployFn: (toDeploy: ContractInstanceWithAddress) => Promise) => + describe(`deploying a contract instance ${how}`, () => { + let instance: ContractInstanceWithAddress; + let initArgs: StatefulContractCtorArgs; + let contract: StatefulTestContract; + + const deployInstance = async (opts: { constructorName?: string; deployer?: AztecAddress } = {}) => { + const initArgs = [wallet.getAddress(), 42] as StatefulContractCtorArgs; + const salt = Fr.random(); + const portalAddress = EthAddress.random(); + const publicKey = Point.random(); + const instance = getContractInstanceFromDeployParams(artifact, { + constructorArgs: initArgs, + salt, + publicKey, + portalAddress, + constructorArtifact: opts.constructorName, + deployer: opts.deployer, + }); + const { address, contractClassId } = instance; + logger.info(`Deploying contract instance at ${address.toString()} class id ${contractClassId.toString()}`); + await deployFn(instance); + + // TODO(@spalladino) We should **not** need the whole instance, including initArgs and salt, + // in order to interact with a public function for the contract. We may even not need + // all of it for running a private function. Consider removing `instance` as a required + // field in the aztec.js `Contract` class, maybe we can replace it with just the partialAddress. + // Not just that, but this instance has been broadcasted, so the pxe should be able to get + // its information from the node directly, excluding private functions, but it's ok because + // 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 t.registerContract(wallet, StatefulTestContract, { + constructorName: opts.constructorName, + salt: instance.salt, + portalAddress: instance.portalContractAddress, + publicKey, + initArgs, + deployer: opts.deployer, + }); + expect(registered.address).toEqual(instance.address); + const contract = await StatefulTestContract.at(instance.address, wallet); + return { contract, initArgs, instance, publicKey }; + }; + + 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); + expect(deployed!.deployer).toEqual(instance.deployer); + }); + + 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).simulate(); + 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(); + const receipt = await contract.methods + .increment_public_value(whom, 10) + .send({ skipPublicSimulation: true }) + .wait({ dontThrowOnRevert: true }); + expect(receipt.status).toEqual(TxStatus.REVERTED); + + // Meanwhile we check we didn't increment the value + expect(await contract.methods.get_public_value(whom).simulate()).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).prove()).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).simulate(); + 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); + }); + + 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 () => { + const whom = AztecAddress.random(); + const receipt = await contract.methods + .public_constructor(whom, 43) + .send({ skipPublicSimulation: true }) + .wait({ dontThrowOnRevert: true }); + expect(receipt.status).toEqual(TxStatus.REVERTED); + expect(await contract.methods.get_public_value(whom).simulate()).toEqual(0n); + }, 30_000); + + it('initializes the contract and calls a public function', async () => { + await contract.methods + .public_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).simulate(); + 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 => { + // Calls the deployer contract directly from a wallet + await deployInstance(wallet, instance).send().wait(); + }); + + testDeployingAnInstance('from a contract', async instance => { + // Register the instance to be deployed in the pxe + await wallet.registerContract({ artifact, instance }); + // Set up the contract that calls the deployer (which happens to be the TestContract) and call it + const deployer = await TestContract.deploy(wallet).send().deployed(); + await deployer.methods.deploy_contract(instance.address).send().wait(); + }); + + describe('error scenarios in deployment', () => { + it('refuses to call a public function on an undeployed contract', async () => { + const whom = wallet.getAddress(); + const instance = await t.registerContract(wallet, StatefulTestContract, { initArgs: [whom, 42] }); + await expect( + instance.methods.increment_public_value_no_init_check(whom, 10).send({ skipPublicSimulation: true }).wait(), + ).rejects.toThrow(/dropped/); + }); + + it('refuses to deploy an instance from a different deployer', () => { + const instance = getContractInstanceFromDeployParams(artifact, { + constructorArgs: [AztecAddress.random(), 42], + deployer: AztecAddress.random(), + }); + expect(() => deployInstance(wallet, instance)).toThrow(/does not match/i); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts new file mode 100644 index 00000000000..f791681b1cb --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts @@ -0,0 +1,89 @@ +import { type DebugLogger, type PXE, type Wallet } from '@aztec/aztec.js'; +import { CounterContract, StatefulTestContract } from '@aztec/noir-contracts.js'; +import { TestContract } from '@aztec/noir-contracts.js/Test'; +import { TokenContract } from '@aztec/noir-contracts.js/Token'; + +import { DeployTest } from './deploy_test.js'; + +describe('e2e_deploy_contract deploy method', () => { + const t = new DeployTest('deploy method'); + + let pxe: PXE; + let logger: DebugLogger; + let wallet: Wallet; + + beforeAll(async () => { + ({ pxe, logger, wallet } = await t.setup()); + }); + + afterAll(() => t.teardown()); + + it('publicly deploys and initializes a contract', async () => { + const owner = wallet.getAddress(); + logger.debug(`Deploying stateful test contract`); + const contract = await StatefulTestContract.deploy(wallet, owner, 42).send().deployed(); + expect(await contract.methods.summed_values(owner).simulate()).toEqual(42n); + logger.debug(`Calling public method on stateful test contract at ${contract.address.toString()}`); + await contract.methods.increment_public_value(owner, 84).send().wait(); + expect(await contract.methods.get_public_value(owner).simulate()).toEqual(84n); + }, 60_000); + + it('publicly universally deploys and initializes a contract', async () => { + const owner = wallet.getAddress(); + const opts = { universalDeploy: true }; + const contract = await StatefulTestContract.deploy(wallet, owner, 42).send(opts).deployed(); + expect(await contract.methods.summed_values(owner).simulate()).toEqual(42n); + await contract.methods.increment_public_value(owner, 84).send().wait(); + expect(await contract.methods.get_public_value(owner).simulate()).toEqual(84n); + }, 60_000); + + it('publicly deploys and calls a public function from the constructor', async () => { + const owner = wallet.getAddress(); + const token = await TokenContract.deploy(wallet, owner, 'TOKEN', 'TKN', 18).send().deployed(); + expect(await token.methods.is_minter(owner).simulate()).toEqual(true); + }, 60_000); + + it('publicly deploys and initializes via a public function', async () => { + const owner = wallet.getAddress(); + logger.debug(`Deploying contract via a public constructor`); + const contract = await StatefulTestContract.deployWithOpts({ wallet, method: 'public_constructor' }, owner, 42) + .send() + .deployed(); + expect(await contract.methods.get_public_value(owner).simulate()).toEqual(42n); + logger.debug(`Calling a private function to ensure the contract was properly initialized`); + await contract.methods.create_note(owner, 30).send().wait(); + expect(await contract.methods.summed_values(owner).simulate()).toEqual(30n); + }, 60_000); + + it('deploys a contract with a default initializer not named constructor', async () => { + logger.debug(`Deploying contract with a default initializer named initialize`); + const opts = { skipClassRegistration: true, skipPublicDeployment: true }; + const contract = await CounterContract.deploy(wallet, 10, wallet.getAddress()).send(opts).deployed(); + logger.debug(`Calling a function to ensure the contract was properly initialized`); + await contract.methods.increment(wallet.getAddress()).send().wait(); + expect(await contract.methods.get_counter(wallet.getAddress()).simulate()).toEqual(11n); + }); + + it('publicly deploys a contract with no constructor', async () => { + logger.debug(`Deploying contract with no constructor`); + const contract = await TestContract.deploy(wallet).send().deployed(); + logger.debug(`Call a public function to check that it was publicly deployed`); + const receipt = await contract.methods.emit_unencrypted(42).send().wait(); + const logs = await pxe.getUnencryptedLogs({ txHash: receipt.txHash }); + expect(logs.logs[0].log.data.toString('hex').replace(/^0+/, '')).toEqual('2a'); + }); + + it('refuses to deploy a contract with no constructor and no public deployment', async () => { + logger.debug(`Deploying contract with no constructor and skipping public deploy`); + const opts = { skipPublicDeployment: true, skipClassRegistration: true }; + await expect(TestContract.deploy(wallet).prove(opts)).rejects.toThrow(/no function calls needed/i); + }); + + it.skip('publicly deploys and calls a public function in the same batched call', async () => { + // 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): Requires being able to read a nullifier on the same block it was emitted. + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts new file mode 100644 index 00000000000..192853b4786 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts @@ -0,0 +1,100 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { + type AccountWallet, + type AztecAddress, + type AztecNode, + CompleteAddress, + type ContractArtifact, + type ContractBase, + type DebugLogger, + type EthAddress, + type Fr, + type PXE, + type Wallet, + createDebugLogger, + getContractInstanceFromDeployParams, +} from '@aztec/aztec.js'; +import { type Point } from '@aztec/circuits.js'; +import { type StatefulTestContract } from '@aztec/noir-contracts.js'; + +import { SnapshotManager, addAccounts } from '../fixtures/snapshot_manager.js'; + +const { E2E_DATA_PATH: dataPath } = process.env; + +export class DeployTest { + private snapshotManager: SnapshotManager; + private wallets: AccountWallet[] = []; + + public logger: DebugLogger; + public pxe!: PXE; + public wallet!: AccountWallet; + public aztecNode!: AztecNode; + + constructor(testName: string) { + this.logger = createDebugLogger(`aztec:e2e_deploy_contract:${testName}`); + this.snapshotManager = new SnapshotManager(`e2e_deploy_contract/${testName}`, dataPath); + } + + async setup() { + await this.applyInitialAccountSnapshot(); + const context = await this.snapshotManager.setup(); + ({ pxe: this.pxe, aztecNode: this.aztecNode } = context); + return this; + } + + async teardown() { + await this.snapshotManager.teardown(); + } + + private async applyInitialAccountSnapshot() { + await this.snapshotManager.snapshot( + 'initial_account', + addAccounts(1, this.logger), + async ({ accountKeys }, { pxe }) => { + const accountManagers = accountKeys.map(ak => getSchnorrAccount(pxe, ak[0], ak[1], 1)); + this.wallets = await Promise.all(accountManagers.map(a => a.getWallet())); + this.wallets.forEach((w, i) => this.logger.verbose(`Wallet ${i} address: ${w.getAddress()}`)); + this.wallet = this.wallets[0]; + }, + ); + } + + async registerContract( + wallet: Wallet, + contractArtifact: ContractArtifactClass, + opts: { + salt?: Fr; + publicKey?: Point; + portalAddress?: EthAddress; + initArgs?: any[]; + constructorName?: string; + deployer?: AztecAddress; + } = {}, + ): Promise { + const { salt, publicKey, portalAddress, initArgs, constructorName, deployer } = opts; + const instance = getContractInstanceFromDeployParams(contractArtifact.artifact, { + constructorArgs: initArgs ?? [], + constructorArtifact: constructorName, + salt, + publicKey, + portalAddress, + deployer, + }); + await wallet.registerContract({ artifact: contractArtifact.artifact, instance }); + return contractArtifact.at(instance.address, wallet); + } + + async registerRandomAccount(): Promise { + const pxe = this.pxe; + const { completeAddress: owner, privateKey } = CompleteAddress.fromRandomPrivateKey(); + await pxe.registerAccount(privateKey, owner.partialAddress); + return owner.address; + } +} + +export type StatefulContractCtorArgs = Parameters; + +export type ContractArtifactClass = { + at(address: AztecAddress, wallet: Wallet): Promise; + artifact: ContractArtifact; +}; diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts new file mode 100644 index 00000000000..5bb33ab2cad --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts @@ -0,0 +1,131 @@ +import { + AztecAddress, + ContractDeployer, + type DebugLogger, + EthAddress, + Fr, + type PXE, + TxStatus, + type Wallet, + getContractInstanceFromDeployParams, +} from '@aztec/aztec.js'; +import { StatefulTestContract } from '@aztec/noir-contracts.js'; +import { TestContractArtifact } from '@aztec/noir-contracts.js/Test'; +import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; + +import { DeployTest } from './deploy_test.js'; + +describe('e2e_deploy_contract legacy', () => { + const t = new DeployTest('legacy'); + + let pxe: PXE; + let logger: DebugLogger; + let wallet: Wallet; + + beforeAll(async () => { + ({ pxe, logger, wallet } = await t.setup()); + }); + + afterAll(() => t.teardown()); + + /** + * Milestone 1.1. + * https://hackmd.io/ouVCnacHQRq2o1oRc5ksNA#Interfaces-and-Responsibilities + */ + it('should deploy a test contract', async () => { + const salt = Fr.random(); + const publicKey = wallet.getCompleteAddress().publicKey; + const deploymentData = getContractInstanceFromDeployParams(TestContractArtifact, { + salt, + publicKey, + deployer: wallet.getAddress(), + }); + const deployer = new ContractDeployer(TestContractArtifact, wallet, publicKey); + const receipt = await deployer.deploy().send({ contractAddressSalt: salt }).wait({ wallet }); + expect(receipt.contract.address).toEqual(deploymentData.address); + expect(await pxe.getContractInstance(deploymentData.address)).toBeDefined(); + expect(await pxe.isContractPubliclyDeployed(deploymentData.address)).toBeDefined(); + }, 60_000); + + /** + * Verify that we can produce multiple rollups. + */ + it('should deploy one contract after another in consecutive rollups', async () => { + const deployer = new ContractDeployer(TestContractArtifact, wallet); + + for (let index = 0; index < 2; index++) { + logger.info(`Deploying contract ${index + 1}...`); + await deployer.deploy().send({ contractAddressSalt: Fr.random() }).wait({ wallet }); + } + }, 60_000); + + /** + * Verify that we can deploy multiple contracts and interact with all of them. + */ + it('should deploy multiple contracts and interact with them', async () => { + const deployer = new ContractDeployer(TestContractArtifact, wallet); + + for (let index = 0; index < 2; index++) { + logger.info(`Deploying contract ${index + 1}...`); + const receipt = await deployer.deploy().send({ contractAddressSalt: Fr.random() }).wait({ wallet }); + logger.info(`Sending TX to contract ${index + 1}...`); + await receipt.contract.methods.get_public_key(wallet.getAddress()).send().wait(); + } + }, 90_000); + + /** + * Milestone 1.2. + * https://hackmd.io/-a5DjEfHTLaMBR49qy6QkA + */ + it('should not deploy a contract with the same salt twice', async () => { + const contractAddressSalt = Fr.random(); + const deployer = new ContractDeployer(TestContractArtifact, wallet); + + await deployer.deploy().send({ contractAddressSalt }).wait({ wallet }); + await expect(deployer.deploy().send({ contractAddressSalt }).wait()).rejects.toThrow(/dropped/); + }, 60_000); + + it('should deploy a contract connected to a portal contract', async () => { + const deployer = new ContractDeployer(TestContractArtifact, wallet); + const portalContract = EthAddress.random(); + + // ContractDeployer was instantiated with wallet so we don't have to pass it to wait(...) + const receipt = await deployer.deploy().send({ portalContract }).wait(); + const address = receipt.contract.address; + + const expectedPortal = portalContract.toString(); + expect((await pxe.getContractInstance(address))?.portalContractAddress.toString()).toEqual(expectedPortal); + }, 60_000); + + it('should not deploy a contract which failed the public part of the execution', async () => { + // This test requires at least another good transaction to go through in the same block as the bad one. + const artifact = TokenContractArtifact; + const initArgs = ['TokenName', 'TKN', 18] as const; + const goodDeploy = StatefulTestContract.deploy(wallet, wallet.getAddress(), 42); + const badDeploy = new ContractDeployer(artifact, wallet).deploy(AztecAddress.ZERO, ...initArgs); + + const firstOpts = { skipPublicSimulation: true, skipClassRegistration: true, skipInstanceDeploy: true }; + const secondOpts = { skipPublicSimulation: true }; + + await Promise.all([goodDeploy.prove(firstOpts), badDeploy.prove(secondOpts)]); + const [goodTx, badTx] = [goodDeploy.send(firstOpts), badDeploy.send(secondOpts)]; + const [goodTxPromiseResult, badTxReceiptResult] = await Promise.allSettled([ + goodTx.wait(), + badTx.wait({ dontThrowOnRevert: true }), + ]); + + expect(goodTxPromiseResult.status).toBe('fulfilled'); + expect(badTxReceiptResult.status).toBe('fulfilled'); // but reverted + + const [goodTxReceipt, badTxReceipt] = await Promise.all([goodTx.getReceipt(), badTx.getReceipt()]); + + // Both the good and bad transactions are included + expect(goodTxReceipt.blockNumber).toEqual(expect.any(Number)); + expect(badTxReceipt.blockNumber).toEqual(expect.any(Number)); + + expect(badTxReceipt.status).toEqual(TxStatus.REVERTED); + + // But the bad tx did not deploy + await expect(pxe.isContractClassPubliclyRegistered(badDeploy.getInstance().address)).resolves.toBeFalsy(); + }, 90_000); +}); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts new file mode 100644 index 00000000000..5a2883f112a --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts @@ -0,0 +1,124 @@ +import { BatchCall, type DebugLogger, Fr, type PXE, SignerlessWallet, type Wallet } from '@aztec/aztec.js'; +import { siloNullifier } from '@aztec/circuits.js/hash'; +import { StatefulTestContract } from '@aztec/noir-contracts.js'; +import { TestContract } from '@aztec/noir-contracts.js/Test'; + +import { DeployTest, type StatefulContractCtorArgs } from './deploy_test.js'; + +describe('e2e_deploy_contract private initialization', () => { + const t = new DeployTest('private initialization'); + + let pxe: PXE; + let logger: DebugLogger; + let wallet: Wallet; + + beforeAll(async () => { + ({ pxe, logger, wallet } = await t.setup()); + }); + + afterAll(() => t.teardown()); + + // Tests calling a private function in an uninitialized and undeployed contract. Note that + // it still requires registering the contract artifact and instance locally in the pxe. + test.each(['as entrypoint', 'from an account contract'] as const)( + 'executes a function in an undeployed contract %s', + async kind => { + const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet; + const contract = await t.registerContract(testWallet, TestContract); + const receipt = await contract.methods.emit_nullifier(10).send().wait({ debug: true }); + const expected = siloNullifier(contract.address, new Fr(10)); + expect(receipt.debugInfo?.nullifiers[1]).toEqual(expected); + }, + 30_000, + ); + + // Tests privately initializing an undeployed contract. Also requires pxe registration in advance. + test.each(['as entrypoint', 'from an account contract'] as const)( + 'privately initializes an undeployed contract contract %s', + async kind => { + const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet; + const owner = await t.registerRandomAccount(); + const initArgs: StatefulContractCtorArgs = [owner, 42]; + const contract = await t.registerContract(testWallet, StatefulTestContract, { initArgs }); + logger.info(`Calling the constructor for ${contract.address}`); + await contract.methods + .constructor(...initArgs) + .send() + .wait(); + logger.info(`Checking if the constructor was run for ${contract.address}`); + expect(await contract.methods.summed_values(owner).simulate()).toEqual(42n); + logger.info(`Calling a private function that requires initialization on ${contract.address}`); + await contract.methods.create_note(owner, 10).send().wait(); + expect(await contract.methods.summed_values(owner).simulate()).toEqual(52n); + }, + 30_000, + ); + + // 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 t.registerRandomAccount(); + const initArgss: StatefulContractCtorArgs[] = [42, 52].map(value => [owner, value]); + const contracts = await Promise.all( + initArgss.map(initArgs => t.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).simulate()).toEqual(42n); + expect(await contracts[1].methods.summed_values(owner).simulate()).toEqual(52n); + }, 30_000); + + // TODO(@spalladino): This won't work until we can read a nullifier in the same tx in which it was emitted. + it.skip('initializes and calls a private function in a single tx', async () => { + const owner = await t.registerRandomAccount(); + const initArgs: StatefulContractCtorArgs = [owner, 42]; + const contract = await t.registerContract(wallet, StatefulTestContract, { initArgs }); + const batch = new BatchCall(wallet, [ + contract.methods.constructor(...initArgs).request(), + contract.methods.create_note(owner, 10).request(), + ]); + logger.info(`Executing constructor and private function in batch at ${contract.address}`); + await batch.send().wait(); + expect(await contract.methods.summed_values(owner).simulate()).toEqual(52n); + }); + + it('refuses to initialize a contract twice', async () => { + const owner = await t.registerRandomAccount(); + const initArgs: StatefulContractCtorArgs = [owner, 42]; + const contract = await t.registerContract(wallet, StatefulTestContract, { initArgs }); + await contract.methods + .constructor(...initArgs) + .send() + .wait(); + await expect( + contract.methods + .constructor(...initArgs) + .send() + .wait(), + ).rejects.toThrow(/dropped/); + }); + + it('refuses to call a private function that requires initialization', async () => { + const owner = await t.registerRandomAccount(); + const initArgs: StatefulContractCtorArgs = [owner, 42]; + const contract = await t.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 t.registerRandomAccount(); + const contract = await t.registerContract(wallet, StatefulTestContract, { initArgs: [owner, 42] }); + await expect(contract.methods.constructor(owner, 43).prove()).rejects.toThrow(/Initialization hash does not match/); + }); + + it('refuses to initialize an instance from a different deployer', async () => { + const owner = await t.registerRandomAccount(); + const contract = await t.registerContract(wallet, StatefulTestContract, { + initArgs: [owner, 42], + deployer: owner, + }); + await expect(contract.methods.constructor(owner, 42).prove()).rejects.toThrow( + /Initializer address is not the contract deployer/i, + ); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/regressions.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/regressions.test.ts new file mode 100644 index 00000000000..729ed976450 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/regressions.test.ts @@ -0,0 +1,27 @@ +import { getDeployedTestAccountsWallets } from '@aztec/accounts/testing'; +import { createPXEClient, makeFetch } from '@aztec/aztec.js'; +import { StatefulTestContract } from '@aztec/noir-contracts.js'; + +import { DeployTest } from './deploy_test.js'; + +describe('e2e_deploy_contract regressions', () => { + const t = new DeployTest('regressions'); + + beforeAll(async () => { + await t.setup(); + }); + + afterAll(() => t.teardown()); + + it('fails properly when trying to deploy a contract with a failing constructor with a pxe client with retries', async () => { + const { PXE_URL } = process.env; + if (!PXE_URL) { + return; + } + const pxeClient = createPXEClient(PXE_URL, makeFetch([1, 2, 3], false)); + const [wallet] = await getDeployedTestAccountsWallets(pxeClient); + await expect( + StatefulTestContract.deployWithOpts({ wallet, method: 'wrong_constructor' }).send().deployed(), + ).rejects.toThrow(/Unknown function/); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/sheilding.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/shielding.test.ts similarity index 100% rename from yarn-project/end-to-end/src/e2e_token_contract/sheilding.test.ts rename to yarn-project/end-to-end/src/e2e_token_contract/shielding.test.ts diff --git a/yarn-project/end-to-end/src/e2e_token_contract/unsheilding.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/unshielding.test.ts similarity index 100% rename from yarn-project/end-to-end/src/e2e_token_contract/unsheilding.test.ts rename to yarn-project/end-to-end/src/e2e_token_contract/unshielding.test.ts From 3cc4f7b0afdd8828d771954819479aad65385275 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 11 Apr 2024 17:23:28 -0300 Subject: [PATCH 2/3] Bump cores to 8 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index abbe9172527..3b133c21d58 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -695,7 +695,7 @@ jobs: - *setup_env - run: name: "Test" - command: cond_spot_run_container end-to-end 4 ./src/e2e_deploy_contract/ + command: cond_spot_run_container end-to-end 8 ./src/e2e_deploy_contract/ aztec_manifest_key: end-to-end <<: *defaults_e2e_test From 198812d482d0787d8f8a01474f218b9024d4557d Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 11 Apr 2024 17:23:36 -0300 Subject: [PATCH 3/3] Update earthfile --- yarn-project/end-to-end/Earthfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/yarn-project/end-to-end/Earthfile b/yarn-project/end-to-end/Earthfile index 85d6ffb644a..f6a3ac2773a 100644 --- a/yarn-project/end-to-end/Earthfile +++ b/yarn-project/end-to-end/Earthfile @@ -107,8 +107,10 @@ e2e-multiple-accounts-1-enc-key: DO +E2E_TEST --test=e2e_multiple_accounts_1_enc_key.test.ts --e2e_mode=$e2e_mode e2e-deploy-contract: - ARG e2e_mode=local - DO +E2E_TEST --test=e2e_deploy_contract.test.ts --e2e_mode=$e2e_mode + LOCALLY + WITH DOCKER --load end-to-end=../+end-to-end + RUN docker run --rm -e LOG_LEVEL=silent -e DEBUG=aztec:e2e* end-to-end ./src/e2e_deploy_contract/ + END e2e-lending-contract: ARG e2e_mode=local