Skip to content

Commit

Permalink
chore(ci): Break e2e-deploy into multiple test suites (#5704)
Browse files Browse the repository at this point in the history
Follows the pattern introduced for e2e-token in #5526
  • Loading branch information
spalladino committed Apr 12, 2024
1 parent 6b91e27 commit 2522294
Show file tree
Hide file tree
Showing 11 changed files with 744 additions and 646 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 8 ./src/e2e_deploy_contract/
aztec_manifest_key: end-to-end
<<: *defaults_e2e_test

Expand Down
6 changes: 4 additions & 2 deletions yarn-project/end-to-end/Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,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
Expand Down
643 changes: 0 additions & 643 deletions yarn-project/end-to-end/src/e2e_deploy_contract.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<void>) =>
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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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.
});
});
Loading

0 comments on commit 2522294

Please sign in to comment.