From 406af67565508471ef49e8716b06ae104f1219bc Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Mon, 1 Apr 2024 17:10:42 +0000 Subject: [PATCH 01/12] e2e token contract persistence --- yarn-project/aztec.js/src/index.ts | 2 +- yarn-project/circuit-types/src/index.ts | 2 +- .../src/types/grumpkin_private_key.ts | 1 + yarn-project/end-to-end/jest.config.json | 16 ++ yarn-project/end-to-end/package.json | 2 +- .../src/fixtures/get_acvm_config.ts | 48 ++++ yarn-project/end-to-end/src/fixtures/setup.ts | 232 ++++++++++++++++++ .../src/fixtures/setup_l1_contracts.ts | 87 +++++++ yarn-project/end-to-end/src/fixtures/utils.ts | 3 + .../foundation/src/aztec-address/index.ts | 11 + .../foundation/src/eth-address/index.ts | 11 + yarn-project/foundation/src/fields/fields.ts | 14 ++ .../foundation/src/serialize/index.ts | 1 + .../foundation/src/serialize/type_registry.ts | 35 +++ 14 files changed, 462 insertions(+), 3 deletions(-) create mode 100644 yarn-project/end-to-end/jest.config.json create mode 100644 yarn-project/end-to-end/src/fixtures/get_acvm_config.ts create mode 100644 yarn-project/end-to-end/src/fixtures/setup.ts create mode 100644 yarn-project/end-to-end/src/fixtures/setup_l1_contracts.ts create mode 100644 yarn-project/foundation/src/serialize/type_registry.ts diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index e7cc7c0ba20..d25d591a047 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -85,7 +85,7 @@ export { Body, CompleteAddress, ExtendedNote, - FunctionCall, + type FunctionCall, GrumpkinPrivateKey, L1ToL2Message, L1Actor, diff --git a/yarn-project/circuit-types/src/index.ts b/yarn-project/circuit-types/src/index.ts index 43ecbefb9e6..f507bdcee44 100644 --- a/yarn-project/circuit-types/src/index.ts +++ b/yarn-project/circuit-types/src/index.ts @@ -19,4 +19,4 @@ export * from './packed_arguments.js'; export * from './interfaces/index.js'; export * from './auth_witness.js'; export * from './aztec_node/rpc/index.js'; -export { CompleteAddress, PublicKey, PartialAddress, GrumpkinPrivateKey } from '@aztec/circuits.js'; +export { CompleteAddress, type PublicKey, type PartialAddress, GrumpkinPrivateKey } from '@aztec/circuits.js'; diff --git a/yarn-project/circuits.js/src/types/grumpkin_private_key.ts b/yarn-project/circuits.js/src/types/grumpkin_private_key.ts index cb96ce1cd8c..e147ea53d14 100644 --- a/yarn-project/circuits.js/src/types/grumpkin_private_key.ts +++ b/yarn-project/circuits.js/src/types/grumpkin_private_key.ts @@ -2,3 +2,4 @@ import { type GrumpkinScalar } from '@aztec/foundation/fields'; /** A type alias for private key which belongs to the scalar field of Grumpkin curve. */ export type GrumpkinPrivateKey = GrumpkinScalar; +export const GrumpkinPrivateKey = GrumpkinScalar; diff --git a/yarn-project/end-to-end/jest.config.json b/yarn-project/end-to-end/jest.config.json new file mode 100644 index 00000000000..6e667a6f88f --- /dev/null +++ b/yarn-project/end-to-end/jest.config.json @@ -0,0 +1,16 @@ +{ + "preset": "ts-jest/presets/default-esm", + "transform": { + "^.+\\.ts$": [ + "ts-jest", + { + "useESM": true + } + ] + }, + "moduleNameMapper": { + "^(\\.{1,2}/.*)\\.[cm]?js$": "$1" + }, + "testRegex": "./src/.*\\.test\\.ts$", + "rootDir": "./src" +} diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index f0e61fa68a1..26726a633dd 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -15,7 +15,7 @@ "clean": "rm -rf ./dest .tsbuildinfo", "formatting": "run -T prettier --check ./src \"!src/web/main.js\" && run -T eslint ./src", "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src", - "test": "LOG_LEVEL=verbose NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --runInBand --testTimeout=60000 --forceExit", + "test": "LOG_LEVEL=${LOG_LEVEL:-verbose} NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --runInBand --testTimeout=60000 --forceExit", "test:integration": "concurrently -k -s first -c reset,dim -n test,anvil \"yarn test:integration:run\" \"anvil\"", "test:integration:run": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --no-cache --runInBand --config jest.integration.config.json" }, diff --git a/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts b/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts new file mode 100644 index 00000000000..f2ee106043a --- /dev/null +++ b/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts @@ -0,0 +1,48 @@ +import { type DebugLogger, fileURLToPath } from '@aztec/aztec.js'; +import { randomBytes } from '@aztec/foundation/crypto'; + +import * as fs from 'fs/promises'; +import * as path from 'path'; + +export { deployAndInitializeTokenAndBridgeContracts } from '../shared/cross_chain_test_harness.js'; + +const { + // PXE_URL = '', + NOIR_RELEASE_DIR = 'noir-repo/target/release', + TEMP_DIR = '/tmp', + ACVM_BINARY_PATH = '', + ACVM_WORKING_DIRECTORY = '', + // ENABLE_GAS = '', +} = process.env; + +// Determines if we have access to the acvm binary and a tmp folder for temp files +export async function getACVMConfig(logger: DebugLogger) { + try { + const expectedAcvmPath = ACVM_BINARY_PATH + ? ACVM_BINARY_PATH + : `${path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../noir/', NOIR_RELEASE_DIR)}/acvm`; + await fs.access(expectedAcvmPath, fs.constants.R_OK); + const tempWorkingDirectory = `${TEMP_DIR}/${randomBytes(4).toString('hex')}`; + const acvmWorkingDirectory = ACVM_WORKING_DIRECTORY ? ACVM_WORKING_DIRECTORY : `${tempWorkingDirectory}/acvm`; + await fs.mkdir(acvmWorkingDirectory, { recursive: true }); + logger(`Using native ACVM binary at ${expectedAcvmPath} with working directory ${acvmWorkingDirectory}`); + + const directoryToCleanup = ACVM_WORKING_DIRECTORY ? undefined : tempWorkingDirectory; + + const cleanup = async () => { + if (directoryToCleanup) { + logger(`Cleaning up ACVM temp directory ${directoryToCleanup}`); + await fs.rm(directoryToCleanup, { recursive: true, force: true }); + } + }; + + return { + acvmWorkingDirectory, + expectedAcvmPath, + cleanup, + }; + } catch (err) { + logger(`Native ACVM not available, error: ${err}`); + return undefined; + } +} diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts new file mode 100644 index 00000000000..66f29944150 --- /dev/null +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -0,0 +1,232 @@ +import { SchnorrAccountContractArtifact, getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; +import { + AztecAddress, + BatchCall, + CompleteAddress, + EthCheatCodes, + GrumpkinPrivateKey, + Wallet, + createDebugLogger, +} from '@aztec/aztec.js'; +import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; +import { asyncMap } from '@aztec/foundation/async-map'; +import { reviver } from '@aztec/foundation/serialize'; +import { createPXEService, getPXEServiceConfig } from '@aztec/pxe'; + +import { createAnvil } from '@viem/anvil'; +import { mkdirSync, readFileSync, writeFileSync } from 'fs'; +import getPort from 'get-port'; +import { mnemonicToAccount } from 'viem/accounts'; + +import { MNEMONIC } from './fixtures.js'; +import { getACVMConfig } from './get_acvm_config.js'; +import { setupL1Contracts } from './setup_l1_contracts.js'; + +export { deployAndInitializeTokenAndBridgeContracts } from '../shared/cross_chain_test_harness.js'; + +interface EndToEndSnapshotState { + nodeConfig: AztecNodeConfig; + // pxeConfig: PXEServiceConfig; + accountKeys: [`0x${string}`, `0x${string}`][]; + customData: { [key: string]: any }; +} + +export async function setupFromState(statePath: string, testName: string) { + const logger = createDebugLogger('aztec:' + testName); + logger(`Initializing with saved state at ${statePath}...`); + + // Load config. + const { nodeConfig, accountKeys, customData }: EndToEndSnapshotState = JSON.parse( + readFileSync(`${statePath}/config.json`, 'utf-8'), + reviver, + ); + + // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. + const ethereumHostPort = await getPort(); + nodeConfig.rpcUrl = `http://localhost:${ethereumHostPort}`; + const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); + await anvil.start(); + // Load anvil state. + const anvilStateFile = `${statePath}/anvil.dat`; + const ethCheatCodes = new EthCheatCodes(nodeConfig.rpcUrl); + await ethCheatCodes.loadChainState(anvilStateFile); + + // TODO: Encapsulate this in a NativeAcvm impl. + const acvmConfig = await getACVMConfig(logger); + if (acvmConfig) { + nodeConfig.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; + nodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; + } + + logger('Creating aztec node...'); + const aztecNode = await AztecNodeService.createAndSync(nodeConfig); + // const sequencer = aztecNode.getSequencer(); + logger('Creating pxe...'); + const pxeConfig = getPXEServiceConfig(); + pxeConfig.dataDirectory = statePath; + const pxe = await createPXEService(aztecNode, pxeConfig); + + const accountManagers = accountKeys.map(a => + getSchnorrAccount(pxe, GrumpkinPrivateKey.fromString(a[0]), GrumpkinPrivateKey.fromString(a[1])), + ); + const wallets = await Promise.all(accountManagers.map(a => a.getWallet())); + + const teardown = async () => { + await aztecNode.stop(); + await pxe.stop(); + await acvmConfig?.cleanup(); + await anvil.stop(); + }; + + return { + customData, + // aztecNode, + // pxe, + // deployL1ContractsValues: config.deployL1ContractsValues, + accounts: await pxe.getRegisteredAccounts(), + // config: nodeConfig, + // wallet: wallets[0], + wallets, + logger, + // cheatCodes, + // sequencer, + teardown, + }; +} + +/** + * Sets up the environment for the end-to-end tests. + */ +export async function setup(numberOfAccounts = 1, statePath: string, testName: string) { + const logger = createDebugLogger('aztec:' + testName); + logger(`Initializing state...`); + + // Fetch the AztecNode config. + // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. + const config: AztecNodeConfig = getConfigEnvVars(); + config.dataDirectory = statePath; + + // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. + logger('Starting anvil...'); + const ethereumHostPort = await getPort(); + config.rpcUrl = `http://localhost:${ethereumHostPort}`; + const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); + await anvil.start(); + + // Deploy our L1 contracts. + logger('Deploying L1 contracts...'); + const hdAccount = mnemonicToAccount(MNEMONIC); + const privKeyRaw = hdAccount.getHdKey().privateKey; + const publisherPrivKey = privKeyRaw === null ? null : Buffer.from(privKeyRaw); + const deployL1ContractsValues = await setupL1Contracts(config.rpcUrl, hdAccount, logger); + config.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; + config.l1Contracts = deployL1ContractsValues.l1ContractAddresses; + config.l1BlockPublishRetryIntervalMS = 100; + + const acvmConfig = await getACVMConfig(logger); + if (acvmConfig) { + config.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; + config.acvmBinaryPath = acvmConfig.expectedAcvmPath; + } + + logger('Creating and synching an aztec node...'); + const aztecNode = await AztecNodeService.createAndSync(config); + // const sequencer = aztecNode.getSequencer(); + + logger('Creating pxe...'); + const pxeConfig = getPXEServiceConfig(); + pxeConfig.dataDirectory = statePath; + const pxe = await createPXEService(aztecNode, pxeConfig); + + // Generate account keys. + const accountKeys = Array.from({ length: numberOfAccounts }).map(_ => [ + GrumpkinPrivateKey.random(), + GrumpkinPrivateKey.random(), + ]); + + logger('Simulating account deployment...'); + const accounts = await asyncMap(accountKeys, async ([encPk, signPk]) => { + const account = getSchnorrAccount(pxe, encPk, signPk); + // Unfortunately the function below is not stateless and we call it here because it takes a long time to run and + // the results get stored within the account object. By calling it here we increase the probability of all the + // accounts being deployed in the same block because it makes the deploy() method basically instant. + await account.getDeployMethod().then(d => + d.simulate({ + contractAddressSalt: account.salt, + skipClassRegistration: true, + skipPublicDeployment: true, + universalDeploy: true, + }), + ); + return account; + }); + + logger('Deploying accounts...'); + const txs = await Promise.all(accounts.map(account => account.deploy())); + await Promise.all(txs.map(tx => tx.wait({ interval: 0.1 }))); + const wallets = await Promise.all(accounts.map(account => account.getWallet())); + + // const cheatCodes = CheatCodes.create(config.rpcUrl, pxe!); + + const customData: { [key: string]: any } = {}; + + const teardown = async () => { + await aztecNode.stop(); + await pxe.stop(); + await acvmConfig?.cleanup(); + await anvil.stop(); + }; + + const snapshot = async () => { + logger(`Saving setup state to ${statePath}...`); + mkdirSync(statePath, { recursive: true }); + + const ethCheatCodes = new EthCheatCodes(config.rpcUrl); + const anvilStateFile = `${statePath}/anvil.dat`; + await ethCheatCodes.dumpChainState(anvilStateFile); + + const state: EndToEndSnapshotState = { + nodeConfig: config, + // pxeConfig: {}, + accountKeys: accountKeys.map(a => [a[0].toString(), a[1].toString()]), + customData, + }; + + writeFileSync(`${statePath}/config.json`, JSON.stringify(state)); + + // TODO: Copy lmdb state. + }; + + return { + customData, + // aztecNode, + // pxe, + // deployL1ContractsValues, + accounts: await pxe.getRegisteredAccounts(), + // config, + // wallet: wallets[0], + wallets, + logger, + // cheatCodes, + // sequencer, + teardown, + snapshot, + }; +} + +/** + * Registers the contract class used for test accounts and publicly deploys the instances requested. + * Use this when you need to make a public call to an account contract, such as for requesting a public authwit. + * @param sender - Wallet to send the deployment tx. + * @param accountsToDeploy - Which accounts to publicly deploy. + */ +export async function publicDeployAccounts(sender: Wallet, accountsToDeploy: (CompleteAddress | AztecAddress)[]) { + const accountAddressesToDeploy = accountsToDeploy.map(a => ('address' in a ? a.address : a)); + const instances = await Promise.all(accountAddressesToDeploy.map(account => sender.getContractInstance(account))); + const batch = new BatchCall(sender, [ + (await registerContractClass(sender, SchnorrAccountContractArtifact)).request(), + ...instances.map(instance => deployInstance(sender, instance!).request()), + ]); + await batch.send().wait(); +} diff --git a/yarn-project/end-to-end/src/fixtures/setup_l1_contracts.ts b/yarn-project/end-to-end/src/fixtures/setup_l1_contracts.ts new file mode 100644 index 00000000000..1a3d3b35128 --- /dev/null +++ b/yarn-project/end-to-end/src/fixtures/setup_l1_contracts.ts @@ -0,0 +1,87 @@ +import { + type DebugLogger, + type DeployL1Contracts, + type L1ContractArtifactsForDeployment, + deployL1Contracts, +} from '@aztec/aztec.js'; +import { + AvailabilityOracleAbi, + AvailabilityOracleBytecode, + GasPortalAbi, + GasPortalBytecode, + InboxAbi, + InboxBytecode, + OutboxAbi, + OutboxBytecode, + PortalERC20Abi, + PortalERC20Bytecode, + RegistryAbi, + RegistryBytecode, + RollupAbi, + RollupBytecode, +} from '@aztec/l1-artifacts'; +import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; + +import { type HDAccount, type PrivateKeyAccount, getContract } from 'viem'; +import { foundry } from 'viem/chains'; + +export { deployAndInitializeTokenAndBridgeContracts } from '../shared/cross_chain_test_harness.js'; + +export const setupL1Contracts = async ( + l1RpcUrl: string, + account: HDAccount | PrivateKeyAccount, + logger: DebugLogger, +) => { + const l1Artifacts: L1ContractArtifactsForDeployment = { + registry: { + contractAbi: RegistryAbi, + contractBytecode: RegistryBytecode, + }, + inbox: { + contractAbi: InboxAbi, + contractBytecode: InboxBytecode, + }, + outbox: { + contractAbi: OutboxAbi, + contractBytecode: OutboxBytecode, + }, + availabilityOracle: { + contractAbi: AvailabilityOracleAbi, + contractBytecode: AvailabilityOracleBytecode, + }, + rollup: { + contractAbi: RollupAbi, + contractBytecode: RollupBytecode, + }, + gasToken: { + contractAbi: PortalERC20Abi, + contractBytecode: PortalERC20Bytecode, + }, + gasPortal: { + contractAbi: GasPortalAbi, + contractBytecode: GasPortalBytecode, + }, + }; + + const l1Data = await deployL1Contracts(l1RpcUrl, account, foundry, logger, l1Artifacts); + await initGasBridge(l1Data); + + return l1Data; +}; + +async function initGasBridge({ walletClient, l1ContractAddresses }: DeployL1Contracts) { + const gasPortal = getContract({ + address: l1ContractAddresses.gasPortalAddress.toString(), + abi: GasPortalAbi, + client: walletClient, + }); + + await gasPortal.write.initialize( + [ + l1ContractAddresses.registryAddress.toString(), + l1ContractAddresses.gasTokenAddress.toString(), + getCanonicalGasTokenAddress(l1ContractAddresses.gasPortalAddress).toString(), + ], + {} as any, + ); +} diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 39b4fba0df9..b4f492919a2 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -374,10 +374,13 @@ export async function setup( const aztecNode = await AztecNodeService.createAndSync(config); const sequencer = aztecNode.getSequencer(); + logger('Creating a pxe...'); const { pxe, wallets } = await setupPXEService(numberOfAccounts, aztecNode!, pxeOpts, logger); if (['1', 'true'].includes(ENABLE_GAS)) { + logger(`Deploying canonical gas token...`); await deployCanonicalGasToken(new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint())); + logger(`Done.`); } const cheatCodes = CheatCodes.create(config.rpcUrl, pxe!); diff --git a/yarn-project/foundation/src/aztec-address/index.ts b/yarn-project/foundation/src/aztec-address/index.ts index c932c10cdaa..e97fb1f1794 100644 --- a/yarn-project/foundation/src/aztec-address/index.ts +++ b/yarn-project/foundation/src/aztec-address/index.ts @@ -2,6 +2,7 @@ import { inspect } from 'util'; import { Fr, fromBuffer } from '../fields/index.js'; import { type BufferReader, FieldReader } from '../serialize/index.js'; +import { TypeRegistry } from '../serialize/type_registry.js'; /** * AztecAddress represents a 32-byte address in the Aztec Protocol. @@ -53,4 +54,14 @@ export class AztecAddress extends Fr { static random() { return new AztecAddress(super.random().toBuffer()); } + + toJSON() { + return { + type: 'AztecAddress', + value: this.toString(), + }; + } } + +// For deserializing JSON. +TypeRegistry.register('AztecAddress', AztecAddress); diff --git a/yarn-project/foundation/src/eth-address/index.ts b/yarn-project/foundation/src/eth-address/index.ts index f61c1f75b4f..5f28e59f54a 100644 --- a/yarn-project/foundation/src/eth-address/index.ts +++ b/yarn-project/foundation/src/eth-address/index.ts @@ -4,6 +4,7 @@ import { keccak256String } from '../crypto/keccak/index.js'; import { randomBytes } from '../crypto/random/index.js'; import { Fr } from '../fields/index.js'; import { BufferReader, FieldReader } from '../serialize/index.js'; +import { TypeRegistry } from '../serialize/type_registry.js'; /** * Represents an Ethereum address as a 20-byte buffer and provides various utility methods @@ -236,4 +237,14 @@ export class EthAddress { toFriendlyJSON() { return this.toString(); } + + toJSON() { + return { + type: 'EthAddress', + value: this.toString(), + }; + } } + +// For deserializing JSON. +TypeRegistry.register('EthAddress', EthAddress); diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index a28018638f4..9b2a67b55b0 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -257,6 +257,13 @@ export class Fr extends BaseField { return new Fr(this.toBigInt() / rhs.toBigInt()); } + + toJSON() { + return { + type: 'Fr', + value: this.toString(), + }; + } } /** @@ -319,6 +326,13 @@ export class Fq extends BaseField { static fromHighLow(high: Fr, low: Fr): Fq { return new Fq((high.toBigInt() << Fq.HIGH_SHIFT) + low.toBigInt()); } + + toJSON() { + return { + type: 'Fq', + value: this.toString(), + }; + } } // Beware: Performance bottleneck below diff --git a/yarn-project/foundation/src/serialize/index.ts b/yarn-project/foundation/src/serialize/index.ts index 875d37f4410..33cb9a5bb4d 100644 --- a/yarn-project/foundation/src/serialize/index.ts +++ b/yarn-project/foundation/src/serialize/index.ts @@ -3,3 +3,4 @@ export * from './buffer_reader.js'; export * from './field_reader.js'; export * from './types.js'; export * from './serialize.js'; +export * from './type_registry.js'; diff --git a/yarn-project/foundation/src/serialize/type_registry.ts b/yarn-project/foundation/src/serialize/type_registry.ts new file mode 100644 index 00000000000..bc48c60f0fd --- /dev/null +++ b/yarn-project/foundation/src/serialize/type_registry.ts @@ -0,0 +1,35 @@ +type Deserializable = { fromString(str: string): object }; + +/** + * Register a class here that has a toJSON method that returns: + * ``` + * { + * "type": "ExampleClassName", + * "value": + * } + * ``` + * and has an e.g. ExampleClassName.fromString(string) method. + * This means you can then easily serialize/deserialize the type using JSON.stringify and JSON.parse. + */ +export class TypeRegistry { + private static registry: Map = new Map(); + + public static register(typeName: string, constructor: Deserializable): void { + this.registry.set(typeName, constructor); + } + + public static getConstructor(typeName: string): Deserializable | undefined { + return this.registry.get(typeName); + } +} + +// Reviver function that uses TypeRegistry to instantiate objects. +export function reviver(key: string, value: any) { + if (value && typeof value === 'object' && 'type' in value && 'value' in value) { + const Constructor = TypeRegistry.getConstructor(value.type); + if (Constructor) { + return Constructor.fromString(value.value); + } + } + return value; +} From cf5dc08cd0644f77558dd1b760600ff6ac6b4efc Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Mon, 1 Apr 2024 19:29:16 +0000 Subject: [PATCH 02/12] fixes --- .../src/types/grumpkin_private_key.ts | 2 +- yarn-project/end-to-end/src/fixtures/setup.ts | 78 ++---------------- .../src/fixtures/setup_from_state.ts | 80 +++++++++++++++++++ 3 files changed, 87 insertions(+), 73 deletions(-) create mode 100644 yarn-project/end-to-end/src/fixtures/setup_from_state.ts diff --git a/yarn-project/circuits.js/src/types/grumpkin_private_key.ts b/yarn-project/circuits.js/src/types/grumpkin_private_key.ts index e147ea53d14..fa2e53e3b9d 100644 --- a/yarn-project/circuits.js/src/types/grumpkin_private_key.ts +++ b/yarn-project/circuits.js/src/types/grumpkin_private_key.ts @@ -1,4 +1,4 @@ -import { type GrumpkinScalar } from '@aztec/foundation/fields'; +import { GrumpkinScalar } from '@aztec/foundation/fields'; /** A type alias for private key which belongs to the scalar field of Grumpkin curve. */ export type GrumpkinPrivateKey = GrumpkinScalar; diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 66f29944150..d04f3495bbe 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -1,21 +1,20 @@ import { SchnorrAccountContractArtifact, getSchnorrAccount } from '@aztec/accounts/schnorr'; import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; import { - AztecAddress, + type AztecAddress, BatchCall, - CompleteAddress, + type CompleteAddress, EthCheatCodes, GrumpkinPrivateKey, - Wallet, + type Wallet, createDebugLogger, } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; import { asyncMap } from '@aztec/foundation/async-map'; -import { reviver } from '@aztec/foundation/serialize'; import { createPXEService, getPXEServiceConfig } from '@aztec/pxe'; import { createAnvil } from '@viem/anvil'; -import { mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { mkdirSync, writeFileSync } from 'fs'; import getPort from 'get-port'; import { mnemonicToAccount } from 'viem/accounts'; @@ -23,80 +22,15 @@ import { MNEMONIC } from './fixtures.js'; import { getACVMConfig } from './get_acvm_config.js'; import { setupL1Contracts } from './setup_l1_contracts.js'; -export { deployAndInitializeTokenAndBridgeContracts } from '../shared/cross_chain_test_harness.js'; - -interface EndToEndSnapshotState { +export interface EndToEndSnapshotState { nodeConfig: AztecNodeConfig; - // pxeConfig: PXEServiceConfig; accountKeys: [`0x${string}`, `0x${string}`][]; customData: { [key: string]: any }; } -export async function setupFromState(statePath: string, testName: string) { - const logger = createDebugLogger('aztec:' + testName); - logger(`Initializing with saved state at ${statePath}...`); - - // Load config. - const { nodeConfig, accountKeys, customData }: EndToEndSnapshotState = JSON.parse( - readFileSync(`${statePath}/config.json`, 'utf-8'), - reviver, - ); - - // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. - const ethereumHostPort = await getPort(); - nodeConfig.rpcUrl = `http://localhost:${ethereumHostPort}`; - const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); - await anvil.start(); - // Load anvil state. - const anvilStateFile = `${statePath}/anvil.dat`; - const ethCheatCodes = new EthCheatCodes(nodeConfig.rpcUrl); - await ethCheatCodes.loadChainState(anvilStateFile); - - // TODO: Encapsulate this in a NativeAcvm impl. - const acvmConfig = await getACVMConfig(logger); - if (acvmConfig) { - nodeConfig.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; - nodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; - } - - logger('Creating aztec node...'); - const aztecNode = await AztecNodeService.createAndSync(nodeConfig); - // const sequencer = aztecNode.getSequencer(); - logger('Creating pxe...'); - const pxeConfig = getPXEServiceConfig(); - pxeConfig.dataDirectory = statePath; - const pxe = await createPXEService(aztecNode, pxeConfig); - - const accountManagers = accountKeys.map(a => - getSchnorrAccount(pxe, GrumpkinPrivateKey.fromString(a[0]), GrumpkinPrivateKey.fromString(a[1])), - ); - const wallets = await Promise.all(accountManagers.map(a => a.getWallet())); - - const teardown = async () => { - await aztecNode.stop(); - await pxe.stop(); - await acvmConfig?.cleanup(); - await anvil.stop(); - }; - - return { - customData, - // aztecNode, - // pxe, - // deployL1ContractsValues: config.deployL1ContractsValues, - accounts: await pxe.getRegisteredAccounts(), - // config: nodeConfig, - // wallet: wallets[0], - wallets, - logger, - // cheatCodes, - // sequencer, - teardown, - }; -} - /** * Sets up the environment for the end-to-end tests. + * The state will be saved in statePath once the returned snapshot function is called. */ export async function setup(numberOfAccounts = 1, statePath: string, testName: string) { const logger = createDebugLogger('aztec:' + testName); diff --git a/yarn-project/end-to-end/src/fixtures/setup_from_state.ts b/yarn-project/end-to-end/src/fixtures/setup_from_state.ts new file mode 100644 index 00000000000..ac301433fbc --- /dev/null +++ b/yarn-project/end-to-end/src/fixtures/setup_from_state.ts @@ -0,0 +1,80 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { AztecNodeService } from '@aztec/aztec-node'; +import { EthCheatCodes, GrumpkinPrivateKey, createDebugLogger } from '@aztec/aztec.js'; +import { reviver } from '@aztec/foundation/serialize'; +import { createPXEService, getPXEServiceConfig } from '@aztec/pxe'; + +import { createAnvil } from '@viem/anvil'; +import { readFileSync } from 'fs'; +import getPort from 'get-port'; + +import { getACVMConfig } from './get_acvm_config.js'; +import { type EndToEndSnapshotState } from './setup.js'; + +export { deployAndInitializeTokenAndBridgeContracts } from '../shared/cross_chain_test_harness.js'; + +/** + * Given a statePath, setup the system starting from that state. + */ +export async function setupFromState(statePath: string, testName: string) { + const logger = createDebugLogger('aztec:' + testName); + logger(`Initializing with saved state at ${statePath}...`); + + // Load config. + const { nodeConfig, accountKeys, customData }: EndToEndSnapshotState = JSON.parse( + readFileSync(`${statePath}/config.json`, 'utf-8'), + reviver, + ); + + // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. + const ethereumHostPort = await getPort(); + nodeConfig.rpcUrl = `http://localhost:${ethereumHostPort}`; + const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); + await anvil.start(); + // Load anvil state. + const anvilStateFile = `${statePath}/anvil.dat`; + const ethCheatCodes = new EthCheatCodes(nodeConfig.rpcUrl); + await ethCheatCodes.loadChainState(anvilStateFile); + + // TODO: Encapsulate this in a NativeAcvm impl. + const acvmConfig = await getACVMConfig(logger); + if (acvmConfig) { + nodeConfig.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; + nodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; + } + + logger('Creating aztec node...'); + const aztecNode = await AztecNodeService.createAndSync(nodeConfig); + // const sequencer = aztecNode.getSequencer(); + logger('Creating pxe...'); + const pxeConfig = getPXEServiceConfig(); + pxeConfig.dataDirectory = statePath; + const pxe = await createPXEService(aztecNode, pxeConfig); + + const accountManagers = accountKeys.map(a => + getSchnorrAccount(pxe, GrumpkinPrivateKey.fromString(a[0]), GrumpkinPrivateKey.fromString(a[1])), + ); + const wallets = await Promise.all(accountManagers.map(a => a.getWallet())); + + const teardown = async () => { + await aztecNode.stop(); + await pxe.stop(); + await acvmConfig?.cleanup(); + await anvil.stop(); + }; + + return { + customData, + // aztecNode, + // pxe, + // deployL1ContractsValues: config.deployL1ContractsValues, + accounts: await pxe.getRegisteredAccounts(), + // config: nodeConfig, + // wallet: wallets[0], + wallets, + logger, + // cheatCodes, + // sequencer, + teardown, + }; +} From 14f81ede51fb44b7c9a01feb4a68c157fc3a3c10 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Mon, 1 Apr 2024 23:37:03 +0000 Subject: [PATCH 03/12] wip --- .../aztec.js/src/account_manager/index.ts | 2 +- yarn-project/end-to-end/jest.config.json | 16 ---------------- .../end-to-end/src/fixtures/get_acvm_config.ts | 4 +--- yarn-project/end-to-end/src/fixtures/setup.ts | 2 +- .../end-to-end/src/fixtures/setup_from_state.ts | 2 +- 5 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 yarn-project/end-to-end/jest.config.json diff --git a/yarn-project/aztec.js/src/account_manager/index.ts b/yarn-project/aztec.js/src/account_manager/index.ts index d18bc8b38d2..55dbcb772ba 100644 --- a/yarn-project/aztec.js/src/account_manager/index.ts +++ b/yarn-project/aztec.js/src/account_manager/index.ts @@ -34,7 +34,7 @@ export class AccountManager { private accountContract: AccountContract, salt?: Salt, ) { - this.salt = salt ? new Fr(salt) : Fr.random(); + this.salt = salt !== undefined ? new Fr(salt) : Fr.random(); } protected getEncryptionPublicKey() { diff --git a/yarn-project/end-to-end/jest.config.json b/yarn-project/end-to-end/jest.config.json deleted file mode 100644 index 6e667a6f88f..00000000000 --- a/yarn-project/end-to-end/jest.config.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "preset": "ts-jest/presets/default-esm", - "transform": { - "^.+\\.ts$": [ - "ts-jest", - { - "useESM": true - } - ] - }, - "moduleNameMapper": { - "^(\\.{1,2}/.*)\\.[cm]?js$": "$1" - }, - "testRegex": "./src/.*\\.test\\.ts$", - "rootDir": "./src" -} diff --git a/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts b/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts index f2ee106043a..0565f3f0d5c 100644 --- a/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts +++ b/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts @@ -18,9 +18,7 @@ const { // Determines if we have access to the acvm binary and a tmp folder for temp files export async function getACVMConfig(logger: DebugLogger) { try { - const expectedAcvmPath = ACVM_BINARY_PATH - ? ACVM_BINARY_PATH - : `${path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../noir/', NOIR_RELEASE_DIR)}/acvm`; + const expectedAcvmPath = ACVM_BINARY_PATH ? ACVM_BINARY_PATH : `../../noir/${NOIR_RELEASE_DIR}/acvm`; await fs.access(expectedAcvmPath, fs.constants.R_OK); const tempWorkingDirectory = `${TEMP_DIR}/${randomBytes(4).toString('hex')}`; const acvmWorkingDirectory = ACVM_WORKING_DIRECTORY ? ACVM_WORKING_DIRECTORY : `${tempWorkingDirectory}/acvm`; diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index d04f3495bbe..254862464a8 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -81,7 +81,7 @@ export async function setup(numberOfAccounts = 1, statePath: string, testName: s logger('Simulating account deployment...'); const accounts = await asyncMap(accountKeys, async ([encPk, signPk]) => { - const account = getSchnorrAccount(pxe, encPk, signPk); + const account = getSchnorrAccount(pxe, encPk, signPk, 1); // Unfortunately the function below is not stateless and we call it here because it takes a long time to run and // the results get stored within the account object. By calling it here we increase the probability of all the // accounts being deployed in the same block because it makes the deploy() method basically instant. diff --git a/yarn-project/end-to-end/src/fixtures/setup_from_state.ts b/yarn-project/end-to-end/src/fixtures/setup_from_state.ts index ac301433fbc..f03f874de65 100644 --- a/yarn-project/end-to-end/src/fixtures/setup_from_state.ts +++ b/yarn-project/end-to-end/src/fixtures/setup_from_state.ts @@ -52,7 +52,7 @@ export async function setupFromState(statePath: string, testName: string) { const pxe = await createPXEService(aztecNode, pxeConfig); const accountManagers = accountKeys.map(a => - getSchnorrAccount(pxe, GrumpkinPrivateKey.fromString(a[0]), GrumpkinPrivateKey.fromString(a[1])), + getSchnorrAccount(pxe, GrumpkinPrivateKey.fromString(a[0]), GrumpkinPrivateKey.fromString(a[1]), 1), ); const wallets = await Promise.all(accountManagers.map(a => a.getWallet())); From cdb25929c6aeeb4777d64b8cf9bd8b9e006182b4 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Tue, 2 Apr 2024 23:35:10 +0000 Subject: [PATCH 04/12] wip --- yarn-project/end-to-end/package.json | 1 + .../src/fixtures/get_acvm_config.ts | 2 +- yarn-project/end-to-end/src/fixtures/setup.ts | 344 ++++++++++++------ .../src/fixtures/setup_from_state.ts | 80 ---- yarn-project/foundation/src/fields/fields.ts | 7 + yarn-project/yarn.lock | 3 +- 6 files changed, 245 insertions(+), 192 deletions(-) delete mode 100644 yarn-project/end-to-end/src/fixtures/setup_from_state.ts diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index 26726a633dd..f80c480f377 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -57,6 +57,7 @@ "@viem/anvil": "^0.0.9", "buffer": "^6.0.3", "crypto-browserify": "^3.12.0", + "fs-extra": "^11.2.0", "get-port": "^7.1.0", "glob": "^10.3.10", "jest": "^29.5.0", diff --git a/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts b/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts index 0565f3f0d5c..e89585341ec 100644 --- a/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts +++ b/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts @@ -29,7 +29,7 @@ export async function getACVMConfig(logger: DebugLogger) { const cleanup = async () => { if (directoryToCleanup) { - logger(`Cleaning up ACVM temp directory ${directoryToCleanup}`); + // logger(`Cleaning up ACVM temp directory ${directoryToCleanup}`); await fs.rm(directoryToCleanup, { recursive: true, force: true }); } }; diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 254862464a8..4d60026d212 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -4,149 +4,273 @@ import { type AztecAddress, BatchCall, type CompleteAddress, + type DebugLogger, EthCheatCodes, GrumpkinPrivateKey, type Wallet, - createDebugLogger, } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; import { asyncMap } from '@aztec/foundation/async-map'; -import { createPXEService, getPXEServiceConfig } from '@aztec/pxe'; +import { reviver } from '@aztec/foundation/serialize'; +import { type PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; -import { createAnvil } from '@viem/anvil'; -import { mkdirSync, writeFileSync } from 'fs'; +import { type Anvil, createAnvil } from '@viem/anvil'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { copySync, removeSync } from 'fs-extra/esm'; import getPort from 'get-port'; +import { join } from 'path'; import { mnemonicToAccount } from 'viem/accounts'; import { MNEMONIC } from './fixtures.js'; import { getACVMConfig } from './get_acvm_config.js'; import { setupL1Contracts } from './setup_l1_contracts.js'; -export interface EndToEndSnapshotState { - nodeConfig: AztecNodeConfig; - accountKeys: [`0x${string}`, `0x${string}`][]; - customData: { [key: string]: any }; -} +type SubsystemsContext = { + anvil: Anvil; + acvmConfig: any; + aztecNode: AztecNodeService; + aztecNodeConfig: AztecNodeConfig; + pxe: PXEService; +}; -/** - * Sets up the environment for the end-to-end tests. - * The state will be saved in statePath once the returned snapshot function is called. - */ -export async function setup(numberOfAccounts = 1, statePath: string, testName: string) { - const logger = createDebugLogger('aztec:' + testName); - logger(`Initializing state...`); - - // Fetch the AztecNode config. - // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. - const config: AztecNodeConfig = getConfigEnvVars(); - config.dataDirectory = statePath; - - // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. - logger('Starting anvil...'); - const ethereumHostPort = await getPort(); - config.rpcUrl = `http://localhost:${ethereumHostPort}`; - const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); - await anvil.start(); - - // Deploy our L1 contracts. - logger('Deploying L1 contracts...'); - const hdAccount = mnemonicToAccount(MNEMONIC); - const privKeyRaw = hdAccount.getHdKey().privateKey; - const publisherPrivKey = privKeyRaw === null ? null : Buffer.from(privKeyRaw); - const deployL1ContractsValues = await setupL1Contracts(config.rpcUrl, hdAccount, logger); - config.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; - config.l1Contracts = deployL1ContractsValues.l1ContractAddresses; - config.l1BlockPublishRetryIntervalMS = 100; - - const acvmConfig = await getACVMConfig(logger); - if (acvmConfig) { - config.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; - config.acvmBinaryPath = acvmConfig.expectedAcvmPath; +type SnapshotEntry = { + name: string; + apply: (context: SubsystemsContext, logger: DebugLogger) => Promise; + restore: (snapshotData: any, context: SubsystemsContext, logger: DebugLogger) => Promise; + snapshotPath: string; +}; + +export class SnapshotManager { + private snapshotStack: SnapshotEntry[] = []; + private context?: SubsystemsContext; + private livePath: string; + + constructor(private testName: string, private dataPath: string, private logger: DebugLogger) { + this.livePath = join(this.dataPath, 'live', this.testName); } - logger('Creating and synching an aztec node...'); - const aztecNode = await AztecNodeService.createAndSync(config); - // const sequencer = aztecNode.getSequencer(); + public async snapshot( + name: string, + apply: (context: SubsystemsContext, logger: DebugLogger) => Promise, + restore: (snapshotData: T, context: SubsystemsContext, logger: DebugLogger) => Promise, + ) { + const snapshotPath = join(this.dataPath, 'snapshots', ...this.snapshotStack.map(e => e.name), name, 'snapshot'); - logger('Creating pxe...'); - const pxeConfig = getPXEServiceConfig(); - pxeConfig.dataDirectory = statePath; - const pxe = await createPXEService(aztecNode, pxeConfig); + if (existsSync(snapshotPath)) { + // Snapshot exists. Delay creating subsystems as we're probably still descending the tree. + this.logger(`Snapshot exists at ${snapshotPath}. Continuing...`); + this.snapshotStack.push({ name, apply, restore, snapshotPath }); + return; + } - // Generate account keys. - const accountKeys = Array.from({ length: numberOfAccounts }).map(_ => [ - GrumpkinPrivateKey.random(), - GrumpkinPrivateKey.random(), - ]); + // Snapshot doesn't exist, and by definition none of the child snapshots can exist. + if (!this.context) { + // We have no context yet, create from the previous snapshot if it exists. + this.context = await this.setup(); + } - logger('Simulating account deployment...'); - const accounts = await asyncMap(accountKeys, async ([encPk, signPk]) => { - const account = getSchnorrAccount(pxe, encPk, signPk, 1); - // Unfortunately the function below is not stateless and we call it here because it takes a long time to run and - // the results get stored within the account object. By calling it here we increase the probability of all the - // accounts being deployed in the same block because it makes the deploy() method basically instant. - await account.getDeployMethod().then(d => - d.simulate({ - contractAddressSalt: account.salt, - skipClassRegistration: true, - skipPublicDeployment: true, - universalDeploy: true, - }), - ); - return account; - }); + this.snapshotStack.push({ name, apply, restore, snapshotPath }); - logger('Deploying accounts...'); - const txs = await Promise.all(accounts.map(account => account.deploy())); - await Promise.all(txs.map(tx => tx.wait({ interval: 0.1 }))); - const wallets = await Promise.all(accounts.map(account => account.getWallet())); + // Apply current state transition. + this.logger(`Applying state transition for ${name}...`); + const snapshotData = await apply(this.context, this.logger); - // const cheatCodes = CheatCodes.create(config.rpcUrl, pxe!); + // Execute the restoration function. + await restore(snapshotData, this.context, this.logger); - const customData: { [key: string]: any } = {}; + // Save the snapshot data. + const ethCheatCodes = new EthCheatCodes(this.context.aztecNodeConfig.rpcUrl); + const anvilStateFile = `${this.livePath}/anvil.dat`; + await ethCheatCodes.dumpChainState(anvilStateFile); + writeFileSync(`${this.livePath}/${name}.json`, JSON.stringify(snapshotData)); - const teardown = async () => { - await aztecNode.stop(); - await pxe.stop(); - await acvmConfig?.cleanup(); - await anvil.stop(); - }; + // Copy everything to snapshot path. + this.logger(`Saving snapshot to ${snapshotPath}...`); + mkdirSync(snapshotPath, { recursive: true }); + copySync(this.livePath, snapshotPath); + } + + public async setup() { + // We have no context yet, create from the last snapshot if it exists. + if (!this.context) { + removeSync(this.livePath); + mkdirSync(this.livePath, { recursive: true }); + const previousSnapshotPath = this.snapshotStack[this.snapshotStack.length - 1]?.snapshotPath; + if (previousSnapshotPath) { + this.logger(`Copying snapshot from ${previousSnapshotPath} to ${this.livePath}...`); + copySync(previousSnapshotPath, this.livePath); + this.context = await this.setupFromState(this.livePath); + // Execute each of the previous snapshots restoration functions in turn. + await asyncMap(this.snapshotStack, async e => { + const snapshotData = JSON.parse(readFileSync(`${e.snapshotPath}/${e.name}.json`, 'utf-8'), reviver); + this.logger(`Executing restoration function for ${e.name}...`); + await e.restore(snapshotData, this.context!, this.logger); + }); + } else { + this.context = await this.setupFromFresh(this.livePath); + } + } + return this.context; + } + + public async pop() { + this.snapshotStack.pop(); + await this.teardown(); + } + + public async teardown() { + if (!this.context) { + return; + } + await this.context.aztecNode.stop(); + await this.context.pxe.stop(); + await this.context.acvmConfig?.cleanup(); + await this.context.anvil.stop(); + this.context = undefined; + } + + private async setupFromFresh(livePath?: string): Promise { + this.logger(`Initializing state...`); + + // Fetch the AztecNode config. + // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. + const aztecNodeConfig: AztecNodeConfig = getConfigEnvVars(); + aztecNodeConfig.dataDirectory = livePath; + + // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. + this.logger('Starting anvil...'); + const ethereumHostPort = await getPort(); + aztecNodeConfig.rpcUrl = `http://localhost:${ethereumHostPort}`; + const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); + await anvil.start(); - const snapshot = async () => { - logger(`Saving setup state to ${statePath}...`); - mkdirSync(statePath, { recursive: true }); + // Deploy our L1 contracts. + this.logger('Deploying L1 contracts...'); + const hdAccount = mnemonicToAccount(MNEMONIC); + const privKeyRaw = hdAccount.getHdKey().privateKey; + const publisherPrivKey = privKeyRaw === null ? null : Buffer.from(privKeyRaw); + const deployL1ContractsValues = await setupL1Contracts(aztecNodeConfig.rpcUrl, hdAccount, this.logger); + aztecNodeConfig.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; + aztecNodeConfig.l1Contracts = deployL1ContractsValues.l1ContractAddresses; + aztecNodeConfig.l1BlockPublishRetryIntervalMS = 100; - const ethCheatCodes = new EthCheatCodes(config.rpcUrl); + const acvmConfig = await getACVMConfig(this.logger); + if (acvmConfig) { + aztecNodeConfig.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; + aztecNodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; + } + + this.logger('Creating and synching an aztec node...'); + const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); + + this.logger('Creating pxe...'); + const pxeConfig = getPXEServiceConfig(); + pxeConfig.dataDirectory = livePath; + const pxe = await createPXEService(aztecNode, pxeConfig); + + writeFileSync(`${livePath}/aztec_node_config.json`, JSON.stringify(aztecNodeConfig)); + + return { + aztecNodeConfig, + anvil, + aztecNode, + pxe, + acvmConfig, + }; + } + + /** + * Given a statePath, setup the system starting from that state. + */ + private async setupFromState(statePath: string): Promise { + this.logger(`Initializing with saved state at ${statePath}...`); + + // Load config. + // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. + const aztecNodeConfig: AztecNodeConfig = JSON.parse( + readFileSync(`${statePath}/aztec_node_config.json`, 'utf-8'), + reviver, + ); + aztecNodeConfig.dataDirectory = statePath; + + // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. + const ethereumHostPort = await getPort(); + aztecNodeConfig.rpcUrl = `http://localhost:${ethereumHostPort}`; + const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); + await anvil.start(); + // Load anvil state. const anvilStateFile = `${statePath}/anvil.dat`; - await ethCheatCodes.dumpChainState(anvilStateFile); + const ethCheatCodes = new EthCheatCodes(aztecNodeConfig.rpcUrl); + await ethCheatCodes.loadChainState(anvilStateFile); + + // TODO: Encapsulate this in a NativeAcvm impl. + const acvmConfig = await getACVMConfig(this.logger); + if (acvmConfig) { + aztecNodeConfig.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; + aztecNodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; + } + + this.logger('Creating aztec node...'); + const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); - const state: EndToEndSnapshotState = { - nodeConfig: config, - // pxeConfig: {}, - accountKeys: accountKeys.map(a => [a[0].toString(), a[1].toString()]), - customData, + this.logger('Creating pxe...'); + const pxeConfig = getPXEServiceConfig(); + pxeConfig.dataDirectory = statePath; + const pxe = await createPXEService(aztecNode, pxeConfig); + + return { + aztecNodeConfig, + anvil, + aztecNode, + pxe, + acvmConfig, }; + } +} - writeFileSync(`${statePath}/config.json`, JSON.stringify(state)); +export const addAccounts = + (numberOfAccounts: number) => + async ({ pxe }: SubsystemsContext, logger: DebugLogger) => { + // Generate account keys. + const accountKeys: [GrumpkinPrivateKey, GrumpkinPrivateKey][] = Array.from({ length: numberOfAccounts }).map(_ => [ + GrumpkinPrivateKey.random(), + GrumpkinPrivateKey.random(), + ]); - // TODO: Copy lmdb state. - }; + logger('Simulating account deployment...'); + const accountManagers = await asyncMap(accountKeys, async ([encPk, signPk]) => { + const account = getSchnorrAccount(pxe, encPk, signPk, 1); + // Unfortunately the function below is not stateless and we call it here because it takes a long time to run and + // the results get stored within the account object. By calling it here we increase the probability of all the + // accounts being deployed in the same block because it makes the deploy() method basically instant. + await account.getDeployMethod().then(d => + d.simulate({ + contractAddressSalt: account.salt, + skipClassRegistration: true, + skipPublicDeployment: true, + universalDeploy: true, + }), + ); + return account; + }); - return { - customData, - // aztecNode, - // pxe, - // deployL1ContractsValues, - accounts: await pxe.getRegisteredAccounts(), - // config, - // wallet: wallets[0], - wallets, - logger, - // cheatCodes, - // sequencer, - teardown, - snapshot, + logger('Deploying accounts...'); + const txs = await Promise.all(accountManagers.map(account => account.deploy())); + await Promise.all(txs.map(tx => tx.wait({ interval: 0.1 }))); + + return { accountKeys }; }; + +export async function restoreAccounts( + { accountKeys }: Awaited>>, + { pxe }: SubsystemsContext, + logger: DebugLogger, +) { + const accountManagers = accountKeys.map(ak => getSchnorrAccount(pxe, ak[0], ak[1], 1)); + const wallets = await Promise.all(accountManagers.map(a => a.getWallet())); + const accounts = await pxe.getRegisteredAccounts(); + logger(`Restored ${accounts.length} accounts.`); + return { wallets, accounts }; } /** diff --git a/yarn-project/end-to-end/src/fixtures/setup_from_state.ts b/yarn-project/end-to-end/src/fixtures/setup_from_state.ts deleted file mode 100644 index f03f874de65..00000000000 --- a/yarn-project/end-to-end/src/fixtures/setup_from_state.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { getSchnorrAccount } from '@aztec/accounts/schnorr'; -import { AztecNodeService } from '@aztec/aztec-node'; -import { EthCheatCodes, GrumpkinPrivateKey, createDebugLogger } from '@aztec/aztec.js'; -import { reviver } from '@aztec/foundation/serialize'; -import { createPXEService, getPXEServiceConfig } from '@aztec/pxe'; - -import { createAnvil } from '@viem/anvil'; -import { readFileSync } from 'fs'; -import getPort from 'get-port'; - -import { getACVMConfig } from './get_acvm_config.js'; -import { type EndToEndSnapshotState } from './setup.js'; - -export { deployAndInitializeTokenAndBridgeContracts } from '../shared/cross_chain_test_harness.js'; - -/** - * Given a statePath, setup the system starting from that state. - */ -export async function setupFromState(statePath: string, testName: string) { - const logger = createDebugLogger('aztec:' + testName); - logger(`Initializing with saved state at ${statePath}...`); - - // Load config. - const { nodeConfig, accountKeys, customData }: EndToEndSnapshotState = JSON.parse( - readFileSync(`${statePath}/config.json`, 'utf-8'), - reviver, - ); - - // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. - const ethereumHostPort = await getPort(); - nodeConfig.rpcUrl = `http://localhost:${ethereumHostPort}`; - const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); - await anvil.start(); - // Load anvil state. - const anvilStateFile = `${statePath}/anvil.dat`; - const ethCheatCodes = new EthCheatCodes(nodeConfig.rpcUrl); - await ethCheatCodes.loadChainState(anvilStateFile); - - // TODO: Encapsulate this in a NativeAcvm impl. - const acvmConfig = await getACVMConfig(logger); - if (acvmConfig) { - nodeConfig.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; - nodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; - } - - logger('Creating aztec node...'); - const aztecNode = await AztecNodeService.createAndSync(nodeConfig); - // const sequencer = aztecNode.getSequencer(); - logger('Creating pxe...'); - const pxeConfig = getPXEServiceConfig(); - pxeConfig.dataDirectory = statePath; - const pxe = await createPXEService(aztecNode, pxeConfig); - - const accountManagers = accountKeys.map(a => - getSchnorrAccount(pxe, GrumpkinPrivateKey.fromString(a[0]), GrumpkinPrivateKey.fromString(a[1]), 1), - ); - const wallets = await Promise.all(accountManagers.map(a => a.getWallet())); - - const teardown = async () => { - await aztecNode.stop(); - await pxe.stop(); - await acvmConfig?.cleanup(); - await anvil.stop(); - }; - - return { - customData, - // aztecNode, - // pxe, - // deployL1ContractsValues: config.deployL1ContractsValues, - accounts: await pxe.getRegisteredAccounts(), - // config: nodeConfig, - // wallet: wallets[0], - wallets, - logger, - // cheatCodes, - // sequencer, - teardown, - }; -} diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index 9b2a67b55b0..bc180fa9ecb 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -3,6 +3,7 @@ import { inspect } from 'util'; import { toBigIntBE, toBufferBE } from '../bigint-buffer/index.js'; import { randomBytes } from '../crypto/random/index.js'; import { BufferReader } from '../serialize/buffer_reader.js'; +import { TypeRegistry } from '../serialize/type_registry.js'; const ZERO_BUFFER = Buffer.alloc(32); @@ -266,6 +267,9 @@ export class Fr extends BaseField { } } +// For deserializing JSON. +TypeRegistry.register('Fr', Fr); + /** * Branding to ensure fields are not interchangeable types. */ @@ -335,6 +339,9 @@ export class Fq extends BaseField { } } +// For deserializing JSON. +TypeRegistry.register('Fq', Fq); + // Beware: Performance bottleneck below /** diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 722f749f9e6..b251c25c6b7 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -404,6 +404,7 @@ __metadata: buffer: ^6.0.3 concurrently: ^7.6.0 crypto-browserify: ^3.12.0 + fs-extra: ^11.2.0 get-port: ^7.1.0 glob: ^10.3.10 jest: ^29.5.0 @@ -7254,7 +7255,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^11.1.1": +"fs-extra@npm:^11.1.1, fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" dependencies: From 07bcf0ffed0c600dddd6c1ebae12b117a5d70bdb Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Wed, 3 Apr 2024 15:35:04 +0000 Subject: [PATCH 05/12] works with snapshots. --- yarn-project/end-to-end/package.json | 3 +- .../e2e_token_contract/access_control.test.ts | 53 ++++ .../src/e2e_token_contract/burn.test.ts | 232 +++++++++++++++ .../src/e2e_token_contract/minting.test.ts | 140 +++++++++ .../reading_constants.test.ts | 131 ++++++++ .../src/e2e_token_contract/sheilding.test.ts | 144 +++++++++ .../src/e2e_token_contract/test_class.ts | 142 +++++++++ .../transfer_private.test.ts | 243 +++++++++++++++ .../transfer_public.test.ts | 280 ++++++++++++++++++ .../e2e_token_contract/unsheilding.test.ts | 136 +++++++++ yarn-project/end-to-end/src/fixtures/setup.ts | 28 +- .../foundation/src/serialize/type_registry.ts | 8 + 12 files changed, 1521 insertions(+), 19 deletions(-) create mode 100644 yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_token_contract/sheilding.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_token_contract/test_class.ts create mode 100644 yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts create mode 100644 yarn-project/end-to-end/src/e2e_token_contract/unsheilding.test.ts diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index f80c480f377..20264ff1960 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -15,7 +15,7 @@ "clean": "rm -rf ./dest .tsbuildinfo", "formatting": "run -T prettier --check ./src \"!src/web/main.js\" && run -T eslint ./src", "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src", - "test": "LOG_LEVEL=${LOG_LEVEL:-verbose} NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --runInBand --testTimeout=60000 --forceExit", + "test": "LOG_LEVEL=${LOG_LEVEL:-verbose} NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --testTimeout=60000 --forceExit", "test:integration": "concurrently -k -s first -c reset,dim -n test,anvil \"yarn test:integration:run\" \"anvil\"", "test:integration:run": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --no-cache --runInBand --config jest.integration.config.json" }, @@ -102,6 +102,7 @@ "node": ">=18" }, "jest": { + "slowTestThreshold": 60, "extensionsToTreatAsEsm": [ ".ts" ], diff --git a/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts new file mode 100644 index 00000000000..8563a2c1900 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts @@ -0,0 +1,53 @@ +import { TestClass } from './test_class.js'; + +// const { E2E_DATA_PATH: dataPath = './data' } = process.env; + +describe('e2e_token_contract', () => { + const t = new TestClass('access_control'); + + beforeAll(async () => { + await t.setup(); + }); + + beforeEach(async () => { + await t.snapshotManager.setup(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + afterAll(async () => { + await t.snapshotManager.teardown(); + }); + + describe('Access controlled functions', () => { + it('Set admin', async () => { + await t.asset.methods.set_admin(t.accounts[1].address).send().wait(); + expect(await t.asset.methods.admin().view()).toBe(t.accounts[1].address.toBigInt()); + }); + + it('Add minter as admin', async () => { + await t.asset.withWallet(t.wallets[1]).methods.set_minter(t.accounts[1].address, true).send().wait(); + expect(await t.asset.methods.is_minter(t.accounts[1].address).view()).toBe(true); + }); + + it('Revoke minter as admin', async () => { + await t.asset.withWallet(t.wallets[1]).methods.set_minter(t.accounts[1].address, false).send().wait(); + expect(await t.asset.methods.is_minter(t.accounts[1].address).view()).toBe(false); + }); + + describe('failure cases', () => { + it('Set admin (not admin)', async () => { + await expect(t.asset.methods.set_admin(t.accounts[0].address).simulate()).rejects.toThrow( + 'Assertion failed: caller is not admin', + ); + }); + it('Revoke minter not as admin', async () => { + await expect(t.asset.methods.set_minter(t.accounts[0].address, false).simulate()).rejects.toThrow( + 'Assertion failed: caller is not admin', + ); + }); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts new file mode 100644 index 00000000000..96fdf7d9944 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts @@ -0,0 +1,232 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { U128_UNDERFLOW_ERROR } from '../fixtures/index.js'; +import { TestClass } from './test_class.js'; + +// const { E2E_DATA_PATH: dataPath = './data' } = process.env; + +describe('e2e_token_contract', () => { + const t = new TestClass('burn'); + let { asset, accounts, tokenSim, wallets } = t; + + beforeAll(async () => { + await t.setup(); + await t.addTransferSnapshot(); + }); + + beforeEach(async () => { + await t.snapshotManager.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, accounts, tokenSim, wallets } = t); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + afterAll(async () => { + await t.snapshotManager.teardown(); + }); + + describe('Burn', () => { + describe('public', () => { + it('burn less than balance', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.burn_public(accounts[0].address, amount, 0).send().wait(); + + tokenSim.burnPublic(accounts[0].address, amount); + }); + + it('burn on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + await action.send().wait(); + + tokenSim.burnPublic(accounts[0].address, amount); + + // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txReplay = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce).send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('burn more than balance', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 + 1n; + const nonce = 0; + await expect(asset.methods.burn_public(accounts[0].address, amount, nonce).simulate()).rejects.toThrow( + U128_UNDERFLOW_ERROR, + ); + }); + + it('burn on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + const nonce = 1; + await expect(asset.methods.burn_public(accounts[0].address, amount, nonce).simulate()).rejects.toThrow( + 'Assertion failed: invalid nonce', + ); + }); + + it('burn on behalf of other without "approval"', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + await expect( + asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce).simulate(), + ).rejects.toThrow('Assertion failed: Message not authorized by account'); + }); + + it('burn more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + await expect(action.simulate()).rejects.toThrow(U128_UNDERFLOW_ERROR); + }); + + it('burn on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[0].address, action }, true).send().wait(); + + await expect( + asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce).simulate(), + ).rejects.toThrow('Assertion failed: Message not authorized by account'); + }); + }); + }); + + describe('private', () => { + it('burn less than balance', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.burn(accounts[0].address, amount, 0).send().wait(); + tokenSim.burnPrivate(accounts[0].address, amount); + }); + + it('burn on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + await asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce).send().wait(); + tokenSim.burnPrivate(accounts[0].address, amount); + + // Perform the transfer again, should fail + const txReplay = asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce).send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('burn more than balance', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 + 1n; + expect(amount).toBeGreaterThan(0n); + await expect(asset.methods.burn(accounts[0].address, amount, 0).simulate()).rejects.toThrow( + 'Assertion failed: Balance too low', + ); + }); + + it('burn on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + await expect(asset.methods.burn(accounts[0].address, amount, 1).simulate()).rejects.toThrow( + 'Assertion failed: invalid nonce', + ); + }); + + it('burn more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + await expect(action.simulate()).rejects.toThrow('Assertion failed: Balance too low'); + }); + + it('burn on behalf of other without approval', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce); + const messageHash = computeAuthWitMessageHash( + accounts[1].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + await expect(action.simulate()).rejects.toThrow( + `Unknown auth witness for message hash ${messageHash.toString()}`, + ); + }); + + it('on behalf of other (invalid designated caller)', async () => { + const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[2]).methods.burn(accounts[0].address, amount, nonce); + const expectedMessageHash = computeAuthWitMessageHash( + accounts[2].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[2].addAuthWitness(witness); + + await expect(action.simulate()).rejects.toThrow( + `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, + ); + }); + }); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts new file mode 100644 index 00000000000..23fa7f1916e --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts @@ -0,0 +1,140 @@ +import { Fr, type TxHash, computeMessageSecretHash } from '@aztec/aztec.js'; + +import { BITSIZE_TOO_BIG_ERROR, U128_OVERFLOW_ERROR } from '../fixtures/fixtures.js'; +import { TestClass } from './test_class.js'; + +// const { E2E_DATA_PATH: dataPath = './data' } = process.env; + +describe('e2e_token_contract', () => { + const t = new TestClass('minting'); + let { asset, accounts, tokenSim, wallets } = t; + + beforeAll(async () => { + await t.setup(); + }); + + beforeEach(async () => { + await t.snapshotManager.setup(); + ({ asset, accounts, tokenSim, wallets } = t); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + afterAll(async () => { + await t.snapshotManager.teardown(); + }); + + describe('Minting', () => { + describe('Public', () => { + it('as minter', async () => { + const amount = 10000n; + await asset.methods.mint_public(accounts[0].address, amount).send().wait(); + + tokenSim.mintPublic(accounts[0].address, amount); + expect(await asset.methods.balance_of_public(accounts[0].address).view()).toEqual( + tokenSim.balanceOfPublic(accounts[0].address), + ); + expect(await asset.methods.total_supply().view()).toEqual(tokenSim.totalSupply); + }); + + describe('failure cases', () => { + it('as non-minter', async () => { + const amount = 10000n; + await expect( + asset.withWallet(wallets[1]).methods.mint_public(accounts[0].address, amount).simulate(), + ).rejects.toThrow('Assertion failed: caller is not minter'); + }); + + it('mint >u128 tokens to overflow', async () => { + const amount = 2n ** 128n; // U128::max() + 1; + await expect(asset.methods.mint_public(accounts[0].address, amount).simulate()).rejects.toThrow( + BITSIZE_TOO_BIG_ERROR, + ); + }); + + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.balanceOfPublic(accounts[0].address); + await expect(asset.methods.mint_public(accounts[0].address, amount).simulate()).rejects.toThrow( + U128_OVERFLOW_ERROR, + ); + }); + + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.balanceOfPublic(accounts[0].address); + await expect(asset.methods.mint_public(accounts[1].address, amount).simulate()).rejects.toThrow( + U128_OVERFLOW_ERROR, + ); + }); + }); + }); + + describe('Private', () => { + const secret = Fr.random(); + const amount = 10000n; + let secretHash: Fr; + let txHash: TxHash; + + beforeAll(() => { + secretHash = computeMessageSecretHash(secret); + }); + + describe('Mint flow', () => { + it('mint_private as minter', async () => { + const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); + tokenSim.mintPrivate(amount); + txHash = receipt.txHash; + }); + + it('redeem as recipient', async () => { + await t.addPendingShieldNoteToPXE(0, amount, secretHash, txHash); + const txClaim = asset.methods.redeem_shield(accounts[0].address, amount, secret).send(); + // docs:start:debug + const receiptClaim = await txClaim.wait({ debug: true }); + // docs:end:debug + tokenSim.redeemShield(accounts[0].address, amount); + // 1 note should be created containing `amount` of tokens + const { visibleNotes } = receiptClaim.debugInfo!; + expect(visibleNotes.length).toBe(1); + expect(visibleNotes[0].note.items[0].toBigInt()).toBe(amount); + }); + }); + + describe('failure cases', () => { + it('try to redeem as recipient (double-spend) [REVERTS]', async () => { + await expect(t.addPendingShieldNoteToPXE(0, amount, secretHash, txHash)).rejects.toThrow( + 'The note has been destroyed.', + ); + await expect(asset.methods.redeem_shield(accounts[0].address, amount, secret).simulate()).rejects.toThrow( + `Assertion failed: Cannot return zero notes`, + ); + }); + + it('mint_private as non-minter', async () => { + await expect( + asset.withWallet(wallets[1]).methods.mint_private(amount, secretHash).simulate(), + ).rejects.toThrow('Assertion failed: caller is not minter'); + }); + + it('mint >u128 tokens to overflow', async () => { + const amount = 2n ** 128n; // U128::max() + 1; + await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrow( + BITSIZE_TOO_BIG_ERROR, + ); + }); + + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.balanceOfPrivate(accounts[0].address); + expect(amount).toBeLessThan(2n ** 128n); + await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrow(U128_OVERFLOW_ERROR); + }); + + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.totalSupply; + await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrow(U128_OVERFLOW_ERROR); + }); + }); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts new file mode 100644 index 00000000000..73603a099c5 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts @@ -0,0 +1,131 @@ +import { ReaderContract } from '@aztec/noir-contracts.js'; + +import { TestClass } from './test_class.js'; + +// const { E2E_DATA_PATH: dataPath = './data' } = process.env; + +describe('e2e_token_contract', () => { + const t = new TestClass('reading_constants'); + const { TOKEN_DECIMALS, TOKEN_NAME, TOKEN_SYMBOL } = TestClass; + // Do not destructure anything mutable. + const { logger } = t; + + beforeAll(async () => { + await t.setup(); + }); + + beforeEach(async () => { + await t.snapshotManager.setup(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + afterAll(async () => { + await t.snapshotManager.teardown(); + }); + + const toString = (val: bigint[]) => { + let str = ''; + for (let i = 0; i < val.length; i++) { + if (val[i] != 0n) { + str += String.fromCharCode(Number(val[i])); + } + } + return str; + }; + + describe('Reading constants', () => { + let reader: ReaderContract; + + beforeAll(async () => { + await t.snapshotManager.snapshot( + 'reading_constants', + async () => { + logger('Deploying ReaderContract...'); + const reader = await ReaderContract.deploy(t.wallets[0]).send().deployed(); + logger(`Deployed ReaderContract to ${reader.address}.`); + return { readerAddress: reader.address }; + }, + async ({ readerAddress }) => { + reader = await ReaderContract.at(readerAddress, t.wallets[0]); + logger(`Reader contract restored to ${readerAddress}.`); + }, + ); + }); + + afterAll(async () => { + await t.snapshotManager.pop(); + }); + + describe('name', () => { + it('check name private', async () => { + const name = toString(await t.asset.methods.un_get_name().view()); + expect(name).toBe(TOKEN_NAME); + + await reader.methods.check_name_private(t.asset.address, TOKEN_NAME).send().wait(); + await expect(reader.methods.check_name_private(t.asset.address, 'WRONG_NAME').simulate()).rejects.toThrow( + 'name.is_eq(_what)', + ); + }); + + it('check name public', async () => { + const name = toString(await t.asset.methods.un_get_name().view()); + expect(name).toBe(TOKEN_NAME); + + await reader.methods.check_name_public(t.asset.address, TOKEN_NAME).send().wait(); + await expect(reader.methods.check_name_public(t.asset.address, 'WRONG_NAME').simulate()).rejects.toThrow( + 'name.is_eq(_what)', + ); + }); + }); + + describe('symbol', () => { + it('private', async () => { + const sym = toString(await t.asset.methods.un_get_symbol().view()); + expect(sym).toBe(TOKEN_SYMBOL); + + await reader.methods.check_symbol_private(t.asset.address, TOKEN_SYMBOL).send().wait(); + + await expect(reader.methods.check_symbol_private(t.asset.address, 'WRONG_SYMBOL').simulate()).rejects.toThrow( + "Cannot satisfy constraint 'symbol.is_eq(_what)'", + ); + }); + it('public', async () => { + const sym = toString(await t.asset.methods.un_get_symbol().view()); + expect(sym).toBe(TOKEN_SYMBOL); + + await reader.methods.check_symbol_public(t.asset.address, TOKEN_SYMBOL).send().wait(); + + await expect(reader.methods.check_symbol_public(t.asset.address, 'WRONG_SYMBOL').simulate()).rejects.toThrow( + "Failed to solve brillig function, reason: explicit trap hit in brillig 'symbol.is_eq(_what)'", + ); + }); + }); + + describe('decimals', () => { + it('private', async () => { + const dec = await t.asset.methods.un_get_decimals().view(); + expect(dec).toBe(TOKEN_DECIMALS); + + await reader.methods.check_decimals_private(t.asset.address, TOKEN_DECIMALS).send().wait(); + + await expect(reader.methods.check_decimals_private(t.asset.address, 99).simulate()).rejects.toThrow( + "Cannot satisfy constraint 'ret[0] as u8 == what'", + ); + }); + + it('public', async () => { + const dec = await t.asset.methods.un_get_decimals().view(); + expect(dec).toBe(TOKEN_DECIMALS); + + await reader.methods.check_decimals_public(t.asset.address, TOKEN_DECIMALS).send().wait(); + + await expect(reader.methods.check_decimals_public(t.asset.address, 99).simulate()).rejects.toThrow( + "Failed to solve brillig function, reason: explicit trap hit in brillig 'ret[0] as u8 == what'", + ); + }); + }); + }); +}); 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/sheilding.test.ts new file mode 100644 index 00000000000..0cd20070894 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/sheilding.test.ts @@ -0,0 +1,144 @@ +import { Fr, computeMessageSecretHash } from '@aztec/aztec.js'; + +import { U128_UNDERFLOW_ERROR } from '../fixtures/fixtures.js'; +import { TestClass } from './test_class.js'; + +// const { E2E_DATA_PATH: dataPath = './data' } = process.env; + +describe('e2e_token_contract', () => { + const t = new TestClass('shielding'); + let { asset, accounts, tokenSim, wallets } = t; + + beforeAll(async () => { + await t.setup(); + }); + + beforeEach(async () => { + await t.snapshotManager.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, accounts, tokenSim, wallets } = t); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + afterAll(async () => { + await t.snapshotManager.teardown(); + }); + + describe('Shielding (shield + redeem_shield)', () => { + const secret = Fr.random(); + let secretHash: Fr; + + beforeAll(async () => { + await t.addTransferSnapshot(); + secretHash = computeMessageSecretHash(secret); + }); + + it('on behalf of self', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balancePub / 2n; + expect(amount).toBeGreaterThan(0n); + + const receipt = await asset.methods.shield(accounts[0].address, amount, secretHash, 0).send().wait(); + + tokenSim.shield(accounts[0].address, amount); + await tokenSim.check(); + + // Redeem it + await t.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + await asset.methods.redeem_shield(accounts[0].address, amount, secret).send().wait(); + + tokenSim.redeemShield(accounts[0].address, amount); + }); + + it('on behalf of other', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balancePub / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.shield(accounts[0].address, amount, secretHash, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + const receipt = await action.send().wait(); + + tokenSim.shield(accounts[0].address, amount); + await tokenSim.check(); + + // Check that replaying the shield should fail! + const txReplay = asset + .withWallet(wallets[1]) + .methods.shield(accounts[0].address, amount, secretHash, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + + // Redeem it + await t.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + await asset.methods.redeem_shield(accounts[0].address, amount, secret).send().wait(); + + tokenSim.redeemShield(accounts[0].address, amount); + }); + + describe('failure cases', () => { + it('on behalf of self (more than balance)', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balancePub + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect(asset.methods.shield(accounts[0].address, amount, secretHash, 0).simulate()).rejects.toThrow( + U128_UNDERFLOW_ERROR, + ); + }); + + it('on behalf of self (invalid nonce)', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balancePub + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect(asset.methods.shield(accounts[0].address, amount, secretHash, 1).simulate()).rejects.toThrow( + 'Assertion failed: invalid nonce', + ); + }); + + it('on behalf of other (more than balance)', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balancePub + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.shield(accounts[0].address, amount, secretHash, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + await expect(action.simulate()).rejects.toThrow(U128_UNDERFLOW_ERROR); + }); + + it('on behalf of other (wrong designated caller)', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balancePub + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[2]).methods.shield(accounts[0].address, amount, secretHash, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + await expect(action.simulate()).rejects.toThrow('Assertion failed: Message not authorized by account'); + }); + + it('on behalf of other (without approval)', async () => { + const balance = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.withWallet(wallets[1]).methods.shield(accounts[0].address, amount, secretHash, nonce).simulate(), + ).rejects.toThrow(`Assertion failed: Message not authorized by account`); + }); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/test_class.ts b/yarn-project/end-to-end/src/e2e_token_contract/test_class.ts new file mode 100644 index 00000000000..76b8958847b --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/test_class.ts @@ -0,0 +1,142 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { + type AccountWallet, + type CompleteAddress, + ExtendedNote, + Fr, + Note, + type TxHash, + computeMessageSecretHash, + createDebugLogger, +} from '@aztec/aztec.js'; +import { DocsExampleContract, TokenContract } from '@aztec/noir-contracts.js'; + +import { SnapshotManager, addAccounts, publicDeployAccounts } from '../fixtures/setup.js'; +import { TokenSimulator } from '../simulators/token_simulator.js'; + +const { E2E_DATA_PATH: dataPath = './data' } = process.env; + +export class TestClass { + static TOKEN_NAME = 'Aztec Token'; + static TOKEN_SYMBOL = 'AZT'; + static TOKEN_DECIMALS = 18n; + logger = createDebugLogger('aztec:e2e_token_contract'); + snapshotManager = new SnapshotManager('e2e_token_contract', dataPath, this.logger); + wallets: AccountWallet[] = []; + accounts: CompleteAddress[] = []; + asset!: TokenContract; + tokenSim!: TokenSimulator; + badAccount!: DocsExampleContract; + + constructor(testName: string) { + this.snapshotManager = new SnapshotManager(`e2e_token_contract/${testName}`, dataPath, this.logger); + } + + async setup() { + await this.snapshotManager.snapshot('3-accounts', addAccounts(3), 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.accounts = await pxe.getRegisteredAccounts(); + this.logger(`Restored ${this.accounts.length} accounts.`); + this.logger(`Wallet 0 address: ${this.wallets[0].getAddress()}`); + }); + + await this.snapshotManager.snapshot( + 'e2e_token_contract', + async () => { + // Create the token contract state. + // Move this account thing to addAccounts above? + this.logger(`Public deploy accounts...`); + await publicDeployAccounts(this.wallets[0], this.accounts.slice(0, 2)); + + this.logger(`Deploying TokenContract...`); + const asset = await TokenContract.deploy( + this.wallets[0], + this.accounts[0], + TestClass.TOKEN_NAME, + TestClass.TOKEN_SYMBOL, + TestClass.TOKEN_DECIMALS, + ) + .send() + .deployed(); + this.logger(`Token deployed to ${asset.address}`); + + this.logger(`Deploying "bad account"...`); + this.badAccount = await DocsExampleContract.deploy(this.wallets[0]).send().deployed(); + this.logger(`Deployed to ${this.badAccount.address}.`); + + return { tokenContractAddress: asset.address, badAccountAddress: this.badAccount.address }; + }, + async ({ tokenContractAddress, badAccountAddress }) => { + // Restore the token contract state. + this.asset = await TokenContract.at(tokenContractAddress, this.wallets[0]); + this.logger(`Token contract restored to ${this.asset.address}.`); + + this.tokenSim = new TokenSimulator( + this.asset, + this.logger, + this.accounts.map(a => a.address), + ); + + this.badAccount = await DocsExampleContract.at(badAccountAddress, this.wallets[0]); + this.logger(`Bad account restored to ${this.badAccount.address}.`); + + expect(await this.asset.methods.admin().view()).toBe(this.accounts[0].address.toBigInt()); + }, + ); + + // TokenContract.artifact.functions.forEach(fn => { + // const sig = decodeFunctionSignature(fn.name, fn.parameters); + // logger(`Function ${sig} and the selector: ${FunctionSelector.fromNameAndParameters(fn.name, fn.parameters)}`); + // }); + } + + async addPendingShieldNoteToPXE(accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) { + const storageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. + const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote + + const note = new Note([new Fr(amount), secretHash]); + const extendedNote = new ExtendedNote( + note, + this.accounts[accountIndex].address, + this.asset.address, + storageSlot, + noteTypeId, + txHash, + ); + await this.wallets[accountIndex].addNote(extendedNote); + } + + async addTransferSnapshot() { + await this.snapshotManager.snapshot( + 'transfer', + async () => { + // Have to destructure again to ensure we have latest refs. + const { asset, accounts } = this; + const amount = 10000n; + await asset.methods.mint_public(accounts[0].address, amount).send().wait(); + + const secret = Fr.random(); + const secretHash = computeMessageSecretHash(secret); + const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); + + await this.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + const txClaim = asset.methods.redeem_shield(accounts[0].address, amount, secret).send(); + await txClaim.wait({ debug: true }); + + return { amount }; + }, + async ({ amount }) => { + const { asset, accounts, tokenSim } = this; + tokenSim.mintPublic(accounts[0].address, amount); + expect(await asset.methods.balance_of_public(accounts[0].address).view()).toEqual( + tokenSim.balanceOfPublic(accounts[0].address), + ); + tokenSim.mintPrivate(amount); + tokenSim.redeemShield(accounts[0].address, amount); + expect(await asset.methods.total_supply().view()).toEqual(tokenSim.totalSupply); + return Promise.resolve(); + }, + ); + } +} diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts new file mode 100644 index 00000000000..822eb98b7e5 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts @@ -0,0 +1,243 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { TestClass } from './test_class.js'; + +// const { E2E_DATA_PATH: dataPath = './data' } = process.env; + +describe('e2e_token_contract', () => { + const t = new TestClass('transfer_private'); + let { asset, accounts, tokenSim, wallets, badAccount } = t; + + beforeAll(async () => { + await t.setup(); + }); + + beforeEach(async () => { + await t.snapshotManager.setup(); + ({ asset, accounts, tokenSim, wallets, badAccount } = t); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + afterAll(async () => { + await t.snapshotManager.teardown(); + }); + + describe('Transfer', () => { + beforeAll(async () => await t.addTransferSnapshot()); + + describe('private', () => { + it('transfer less than balance', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer(accounts[0].address, accounts[1].address, amount, 0).send().wait(); + tokenSim.transferPrivate(accounts[0].address, accounts[1].address, amount); + }); + + it('transfer to self', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer(accounts[0].address, accounts[0].address, amount, 0).send().wait(); + tokenSim.transferPrivate(accounts[0].address, accounts[0].address, amount); + }); + + it('transfer on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + // docs:start:authwit_transfer_example + const action = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + expect( + await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action }), + ).toEqual({ + isValidInPrivate: true, + isValidInPublic: false, + }); + // docs:end:authwit_transfer_example + + // Perform the transfer + await action.send().wait(); + tokenSim.transferPrivate(accounts[0].address, accounts[1].address, amount); + + // Perform the transfer again, should fail + const txReplay = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('transfer more than balance', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 + 1n; + expect(amount).toBeGreaterThan(0n); + await expect( + asset.methods.transfer(accounts[0].address, accounts[1].address, amount, 0).simulate(), + ).rejects.toThrow('Assertion failed: Balance too low'); + }); + + it('transfer on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + await expect( + asset.methods.transfer(accounts[0].address, accounts[1].address, amount, 1).simulate(), + ).rejects.toThrow('Assertion failed: invalid nonce'); + }); + + it('transfer more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const balance1 = await asset.methods.balance_of_private(accounts[1].address).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly using + // await wallet.signAndAddAuthWitness(messageHash, ); + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + // Perform the transfer + await expect(action.simulate()).rejects.toThrow('Assertion failed: Balance too low'); + expect(await asset.methods.balance_of_private(accounts[0].address).view()).toEqual(balance0); + expect(await asset.methods.balance_of_private(accounts[1].address).view()).toEqual(balance1); + }); + + it.skip('transfer into account to overflow', () => { + // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not + // a way to get funds enough to overflow. + // Require direct storage manipulation for us to perform a nice explicit case though. + // See https://github.com/AztecProtocol/aztec-packages/issues/1259 + }); + + it('transfer on behalf of other without approval', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + const messageHash = computeAuthWitMessageHash( + accounts[1].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + await expect(action.simulate()).rejects.toThrow( + `Unknown auth witness for message hash ${messageHash.toString()}`, + ); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[2]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + const expectedMessageHash = computeAuthWitMessageHash( + accounts[2].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[2].addAuthWitness(witness); + + await expect(action.simulate()).rejects.toThrow( + `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, + ); + expect(await asset.methods.balance_of_private(accounts[0].address).view()).toEqual(balance0); + }); + + it('transfer on behalf of other, cancelled authwit', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + await wallets[0].cancelAuthWit(witness.requestHash).send().wait(); + + // Perform the transfer, should fail because nullifier already emitted + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrowError('Transaction '); + }); + + it('transfer on behalf of other, cancelled authwit, flow 2', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + await wallets[0].cancelAuthWit({ caller: accounts[1].address, action }).send().wait(); + + // Perform the transfer, should fail because nullifier already emitted + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrow('Transaction '); + }); + + it('transfer on behalf of other, invalid spend_private_authwit on "from"', async () => { + const nonce = Fr.random(); + + // Should fail as the returned value from the badAccount is malformed + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer(badAccount.address, accounts[1].address, 0, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrow( + "Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'", + ); + }); + }); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts new file mode 100644 index 00000000000..a1e7ca6a6a2 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts @@ -0,0 +1,280 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { U128_UNDERFLOW_ERROR } from '../fixtures/fixtures.js'; +import { TestClass } from './test_class.js'; + +// const { E2E_DATA_PATH: dataPath = './data' } = process.env; + +describe('e2e_token_contract', () => { + const t = new TestClass('transfer_public'); + let { asset, accounts, tokenSim, wallets, badAccount } = t; + + beforeAll(async () => { + await t.setup(); + }); + + beforeEach(async () => { + await t.snapshotManager.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, accounts, tokenSim, wallets, badAccount } = t); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + afterAll(async () => { + await t.snapshotManager.teardown(); + }); + + describe('Transfer', () => { + beforeAll(async () => await t.addTransferSnapshot()); + + describe('public', () => { + it('transfer less than balance', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer_public(accounts[0].address, accounts[1].address, amount, 0).send().wait(); + + tokenSim.transferPublic(accounts[0].address, accounts[1].address, amount); + }); + + it('transfer to self', async () => { + const balance = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer_public(accounts[0].address, accounts[0].address, amount, 0).send().wait(); + + tokenSim.transferPublic(accounts[0].address, accounts[0].address, amount); + }); + + it('transfer on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + // docs:start:authwit_public_transfer_example + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + // docs:end:authwit_public_transfer_example + + // Perform the transfer + await action.send().wait(); + + tokenSim.transferPublic(accounts[0].address, accounts[1].address, amount); + + // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txReplay = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('transfer more than balance', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 + 1n; + const nonce = 0; + await expect( + asset.methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce).simulate(), + ).rejects.toThrow(U128_UNDERFLOW_ERROR); + }); + + it('transfer on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 - 1n; + const nonce = 1; + await expect( + asset.methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce).simulate(), + ).rejects.toThrow('Assertion failed: invalid nonce'); + }); + + it('transfer on behalf of other without "approval"', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + await expect( + asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) + .simulate(), + ).rejects.toThrow('Assertion failed: Message not authorized by account'); + }); + + it('transfer more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const balance1 = await asset.methods.balance_of_public(accounts[1].address).view(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + + expect( + await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action }), + ).toEqual({ + isValidInPrivate: false, + isValidInPublic: false, + }); + + // We need to compute the message we want to sign and add it to the wallet as approved + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + expect( + await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action }), + ).toEqual({ + isValidInPrivate: false, + isValidInPublic: true, + }); + + // Perform the transfer + await expect(action.simulate()).rejects.toThrow(U128_UNDERFLOW_ERROR); + + expect(await asset.methods.balance_of_public(accounts[0].address).view()).toEqual(balance0); + expect(await asset.methods.balance_of_public(accounts[1].address).view()).toEqual(balance1); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const balance1 = await asset.methods.balance_of_public(accounts[1].address).view(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + + await wallets[0].setPublicAuthWit({ caller: accounts[0].address, action }, true).send().wait(); + + // Perform the transfer + await expect(action.simulate()).rejects.toThrow('Assertion failed: Message not authorized by account'); + + expect(await asset.methods.balance_of_public(accounts[0].address).view()).toEqual(balance0); + expect(await asset.methods.balance_of_public(accounts[1].address).view()).toEqual(balance1); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const balance1 = await asset.methods.balance_of_public(accounts[1].address).view(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[0].address, action }, true).send().wait(); + + // Perform the transfer + await expect(action.simulate()).rejects.toThrow('Assertion failed: Message not authorized by account'); + + expect(await asset.methods.balance_of_public(accounts[0].address).view()).toEqual(balance0); + expect(await asset.methods.balance_of_public(accounts[1].address).view()).toEqual(balance1); + }); + + it('transfer on behalf of other, cancelled authwit', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + await wallets[0].cancelAuthWit({ caller: accounts[1].address, action }).send().wait(); + + // Check that the authwit is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrowError('Transaction '); + }); + + it('transfer on behalf of other, cancelled authwit, flow 2', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, false).send().wait(); + + // Check that the authwit is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrowError('Transaction '); + }); + + it('transfer on behalf of other, cancelled authwit, flow 3', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + const messageHash = computeAuthWitMessageHash( + accounts[1].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + await wallets[0].setPublicAuthWit(messageHash, true).send().wait(); + + await wallets[0].cancelAuthWit(messageHash).send().wait(); + + // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrow('Transaction '); + }); + + it('transfer on behalf of other, invalid spend_public_authwit on "from"', async () => { + const nonce = Fr.random(); + + // Should fail as the returned value from the badAccount is malformed + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer_public(badAccount.address, accounts[1].address, 0, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrow( + "Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'", + ); + }); + + it.skip('transfer into account to overflow', () => { + // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not + // a way to get funds enough to overflow. + // Require direct storage manipulation for us to perform a nice explicit case though. + // See https://github.com/AztecProtocol/aztec-packages/issues/1259 + }); + }); + }); + }); +}); 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/unsheilding.test.ts new file mode 100644 index 00000000000..433150340c8 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/unsheilding.test.ts @@ -0,0 +1,136 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { TestClass } from './test_class.js'; + +// const { E2E_DATA_PATH: dataPath = './data' } = process.env; + +describe('e2e_token_contract', () => { + const t = new TestClass('unshielding'); + let { asset, accounts, tokenSim, wallets } = t; + + beforeAll(async () => { + await t.setup(); + await t.addTransferSnapshot(); + }); + + beforeEach(async () => { + await t.snapshotManager.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, accounts, tokenSim, wallets } = t); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + afterAll(async () => { + await t.snapshotManager.teardown(); + }); + + describe('Unshielding', () => { + it('on behalf of self', async () => { + const balancePriv = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balancePriv / 2n; + expect(amount).toBeGreaterThan(0n); + + await asset.methods.unshield(accounts[0].address, accounts[0].address, amount, 0).send().wait(); + + tokenSim.unshield(accounts[0].address, accounts[0].address, amount); + }); + + it('on behalf of other', async () => { + const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balancePriv0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + await action.send().wait(); + tokenSim.unshield(accounts[0].address, accounts[1].address, amount); + + // Perform the transfer again, should fail + const txReplay = asset + .withWallet(wallets[1]) + .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('on behalf of self (more than balance)', async () => { + const balancePriv = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balancePriv + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.methods.unshield(accounts[0].address, accounts[0].address, amount, 0).simulate(), + ).rejects.toThrow('Assertion failed: Balance too low'); + }); + + it('on behalf of self (invalid nonce)', async () => { + const balancePriv = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balancePriv + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.methods.unshield(accounts[0].address, accounts[0].address, amount, 1).simulate(), + ).rejects.toThrow('Assertion failed: invalid nonce'); + }); + + it('on behalf of other (more than balance)', async () => { + const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + await expect(action.simulate()).rejects.toThrow('Assertion failed: Balance too low'); + }); + + it('on behalf of other (invalid designated caller)', async () => { + const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).view(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[2]) + .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce); + const expectedMessageHash = computeAuthWitMessageHash( + accounts[2].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[2].addAuthWitness(witness); + + await expect(action.simulate()).rejects.toThrow( + `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, + ); + }); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 4d60026d212..36b82000d86 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -11,7 +11,7 @@ import { } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; import { asyncMap } from '@aztec/foundation/async-map'; -import { reviver } from '@aztec/foundation/serialize'; +import { resolver, reviver } from '@aztec/foundation/serialize'; import { type PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; import { type Anvil, createAnvil } from '@viem/anvil'; @@ -45,14 +45,15 @@ export class SnapshotManager { private context?: SubsystemsContext; private livePath: string; - constructor(private testName: string, private dataPath: string, private logger: DebugLogger) { - this.livePath = join(this.dataPath, 'live', this.testName); + constructor(testName: string, private dataPath: string, private logger: DebugLogger) { + this.livePath = join(this.dataPath, 'live', testName); } - public async snapshot( + public async snapshot( name: string, apply: (context: SubsystemsContext, logger: DebugLogger) => Promise, - restore: (snapshotData: T, context: SubsystemsContext, logger: DebugLogger) => Promise, + restore: (snapshotData: T, context: SubsystemsContext, logger: DebugLogger) => Promise = () => + Promise.resolve(), ) { const snapshotPath = join(this.dataPath, 'snapshots', ...this.snapshotStack.map(e => e.name), name, 'snapshot'); @@ -74,6 +75,7 @@ export class SnapshotManager { // Apply current state transition. this.logger(`Applying state transition for ${name}...`); const snapshotData = await apply(this.context, this.logger); + this.logger(`State transition for ${name} complete.`); // Execute the restoration function. await restore(snapshotData, this.context, this.logger); @@ -82,7 +84,7 @@ export class SnapshotManager { const ethCheatCodes = new EthCheatCodes(this.context.aztecNodeConfig.rpcUrl); const anvilStateFile = `${this.livePath}/anvil.dat`; await ethCheatCodes.dumpChainState(anvilStateFile); - writeFileSync(`${this.livePath}/${name}.json`, JSON.stringify(snapshotData)); + writeFileSync(`${this.livePath}/${name}.json`, JSON.stringify(snapshotData || {}, resolver)); // Copy everything to snapshot path. this.logger(`Saving snapshot to ${snapshotPath}...`); @@ -105,6 +107,7 @@ export class SnapshotManager { const snapshotData = JSON.parse(readFileSync(`${e.snapshotPath}/${e.name}.json`, 'utf-8'), reviver); this.logger(`Executing restoration function for ${e.name}...`); await e.restore(snapshotData, this.context!, this.logger); + this.logger(`Restoration of ${e.name} complete.`); }); } else { this.context = await this.setupFromFresh(this.livePath); @@ -127,6 +130,7 @@ export class SnapshotManager { await this.context.acvmConfig?.cleanup(); await this.context.anvil.stop(); this.context = undefined; + removeSync(this.livePath); } private async setupFromFresh(livePath?: string): Promise { @@ -261,18 +265,6 @@ export const addAccounts = return { accountKeys }; }; -export async function restoreAccounts( - { accountKeys }: Awaited>>, - { pxe }: SubsystemsContext, - logger: DebugLogger, -) { - const accountManagers = accountKeys.map(ak => getSchnorrAccount(pxe, ak[0], ak[1], 1)); - const wallets = await Promise.all(accountManagers.map(a => a.getWallet())); - const accounts = await pxe.getRegisteredAccounts(); - logger(`Restored ${accounts.length} accounts.`); - return { wallets, accounts }; -} - /** * Registers the contract class used for test accounts and publicly deploys the instances requested. * Use this when you need to make a public call to an account contract, such as for requesting a public authwit. diff --git a/yarn-project/foundation/src/serialize/type_registry.ts b/yarn-project/foundation/src/serialize/type_registry.ts index bc48c60f0fd..85146710ed8 100644 --- a/yarn-project/foundation/src/serialize/type_registry.ts +++ b/yarn-project/foundation/src/serialize/type_registry.ts @@ -23,8 +23,16 @@ export class TypeRegistry { } } +// Resolver function that enables JSON serialization of BigInts. +export function resolver(_: any, value: any) { + return typeof value === 'bigint' ? value.toString() + 'n' : value; +} + // Reviver function that uses TypeRegistry to instantiate objects. export function reviver(key: string, value: any) { + if (typeof value === 'string' && /^\d+n$/.test(value)) { + return BigInt(value.slice(0, -1)); + } if (value && typeof value === 'object' && 'type' in value && 'value' in value) { const Constructor = TypeRegistry.getConstructor(value.type); if (Constructor) { From 0a9a82bca7097cc2a7527f5be2c2b6f46e7c2ce8 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Thu, 4 Apr 2024 22:01:24 +0000 Subject: [PATCH 06/12] some cleaning --- yarn-project/end-to-end/package.json | 2 +- .../e2e_token_contract/access_control.test.ts | 60 +-- .../src/e2e_token_contract/burn.test.ts | 347 +++++++------ .../src/e2e_token_contract/minting.test.ts | 202 ++++---- .../reading_constants.test.ts | 203 ++++---- .../src/e2e_token_contract/sheilding.test.ts | 190 ++++--- .../{test_class.ts => token_contract_test.ts} | 39 +- .../transfer_private.test.ts | 434 ++++++++-------- .../transfer_public.test.ts | 482 +++++++++--------- .../e2e_token_contract/unsheilding.test.ts | 183 ++++--- yarn-project/end-to-end/src/fixtures/setup.ts | 42 +- 11 files changed, 1082 insertions(+), 1102 deletions(-) rename yarn-project/end-to-end/src/e2e_token_contract/{test_class.ts => token_contract_test.ts} (83%) diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index 20264ff1960..0aa48dd09a2 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -102,7 +102,7 @@ "node": ">=18" }, "jest": { - "slowTestThreshold": 60, + "slowTestThreshold": 180, "extensionsToTreatAsEsm": [ ".ts" ], diff --git a/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts index 8563a2c1900..1cccd8813a7 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts @@ -1,12 +1,14 @@ -import { TestClass } from './test_class.js'; +import { TokenContractTest } from './token_contract_test.js'; -// const { E2E_DATA_PATH: dataPath = './data' } = process.env; - -describe('e2e_token_contract', () => { - const t = new TestClass('access_control'); +describe('e2e_token_contract access control', () => { + const t = new TokenContractTest('access_control'); beforeAll(async () => { - await t.setup(); + await t.pushBaseSnapshots(); + }); + + afterAll(async () => { + await t.popBaseSnapshots(); }); beforeEach(async () => { @@ -17,37 +19,31 @@ describe('e2e_token_contract', () => { await t.tokenSim.check(); }); - afterAll(async () => { - await t.snapshotManager.teardown(); + it('Set admin', async () => { + await t.asset.methods.set_admin(t.accounts[1].address).send().wait(); + expect(await t.asset.methods.admin().simulate()).toBe(t.accounts[1].address.toBigInt()); }); - describe('Access controlled functions', () => { - it('Set admin', async () => { - await t.asset.methods.set_admin(t.accounts[1].address).send().wait(); - expect(await t.asset.methods.admin().view()).toBe(t.accounts[1].address.toBigInt()); - }); + it('Add minter as admin', async () => { + await t.asset.withWallet(t.wallets[1]).methods.set_minter(t.accounts[1].address, true).send().wait(); + expect(await t.asset.methods.is_minter(t.accounts[1].address).simulate()).toBe(true); + }); - it('Add minter as admin', async () => { - await t.asset.withWallet(t.wallets[1]).methods.set_minter(t.accounts[1].address, true).send().wait(); - expect(await t.asset.methods.is_minter(t.accounts[1].address).view()).toBe(true); - }); + it('Revoke minter as admin', async () => { + await t.asset.withWallet(t.wallets[1]).methods.set_minter(t.accounts[1].address, false).send().wait(); + expect(await t.asset.methods.is_minter(t.accounts[1].address).simulate()).toBe(false); + }); - it('Revoke minter as admin', async () => { - await t.asset.withWallet(t.wallets[1]).methods.set_minter(t.accounts[1].address, false).send().wait(); - expect(await t.asset.methods.is_minter(t.accounts[1].address).view()).toBe(false); + describe('failure cases', () => { + it('Set admin (not admin)', async () => { + await expect(t.asset.methods.set_admin(t.accounts[0].address).simulate()).rejects.toThrow( + 'Assertion failed: caller is not admin', + ); }); - - describe('failure cases', () => { - it('Set admin (not admin)', async () => { - await expect(t.asset.methods.set_admin(t.accounts[0].address).simulate()).rejects.toThrow( - 'Assertion failed: caller is not admin', - ); - }); - it('Revoke minter not as admin', async () => { - await expect(t.asset.methods.set_minter(t.accounts[0].address, false).simulate()).rejects.toThrow( - 'Assertion failed: caller is not admin', - ); - }); + it('Revoke minter not as admin', async () => { + await expect(t.asset.methods.set_minter(t.accounts[0].address, false).simulate()).rejects.toThrow( + 'Assertion failed: caller is not admin', + ); }); }); }); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts index 96fdf7d9944..c40740ee763 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts @@ -1,17 +1,20 @@ import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; import { U128_UNDERFLOW_ERROR } from '../fixtures/index.js'; -import { TestClass } from './test_class.js'; +import { TokenContractTest } from './token_contract_test.js'; -// const { E2E_DATA_PATH: dataPath = './data' } = process.env; - -describe('e2e_token_contract', () => { - const t = new TestClass('burn'); +describe('e2e_token_contract burn', () => { + const t = new TokenContractTest('burn'); let { asset, accounts, tokenSim, wallets } = t; beforeAll(async () => { - await t.setup(); - await t.addTransferSnapshot(); + await t.pushBaseSnapshots(); + await t.pushMintSnapshot(); + }); + + afterAll(async () => { + await t.snapshotManager.pop(); // mint + await t.popBaseSnapshots(); }); beforeEach(async () => { @@ -24,111 +27,147 @@ describe('e2e_token_contract', () => { await t.tokenSim.check(); }); - afterAll(async () => { - await t.snapshotManager.teardown(); - }); + describe('public', () => { + it('burn less than balance', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.burn_public(accounts[0].address, amount, 0).send().wait(); - describe('Burn', () => { - describe('public', () => { - it('burn less than balance', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.burn_public(accounts[0].address, amount, 0).send().wait(); + tokenSim.burnPublic(accounts[0].address, amount); + }); + + it('burn on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + await action.send().wait(); + + tokenSim.burnPublic(accounts[0].address, amount); + + // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txReplay = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce).send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); - tokenSim.burnPublic(accounts[0].address, amount); + describe('failure cases', () => { + it('burn more than balance', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 + 1n; + const nonce = 0; + await expect(asset.methods.burn_public(accounts[0].address, amount, nonce).simulate()).rejects.toThrow( + U128_UNDERFLOW_ERROR, + ); }); - it('burn on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 / 2n; + it('burn on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 - 1n; expect(amount).toBeGreaterThan(0n); + const nonce = 1; + await expect(asset.methods.burn_public(accounts[0].address, amount, nonce).simulate()).rejects.toThrow( + 'Assertion failed: invalid nonce', + ); + }); + + it('burn on behalf of other without "approval"', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + await expect( + asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce).simulate(), + ).rejects.toThrow('Assertion failed: Message not authorized by account'); + }); + + it('burn more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 + 1n; const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); // We need to compute the message we want to sign and add it to the wallet as approved const action = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce); await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - await action.send().wait(); + await expect(action.simulate()).rejects.toThrow(U128_UNDERFLOW_ERROR); + }); - tokenSim.burnPublic(accounts[0].address, amount); + it('burn on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); - // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. - const txReplay = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce).send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); - }); + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[0].address, action }, true).send().wait(); - describe('failure cases', () => { - it('burn more than balance', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 + 1n; - const nonce = 0; - await expect(asset.methods.burn_public(accounts[0].address, amount, nonce).simulate()).rejects.toThrow( - U128_UNDERFLOW_ERROR, - ); - }); - - it('burn on behalf of self with non-zero nonce', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 - 1n; - expect(amount).toBeGreaterThan(0n); - const nonce = 1; - await expect(asset.methods.burn_public(accounts[0].address, amount, nonce).simulate()).rejects.toThrow( - 'Assertion failed: invalid nonce', - ); - }); - - it('burn on behalf of other without "approval"', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - await expect( - asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce).simulate(), - ).rejects.toThrow('Assertion failed: Message not authorized by account'); - }); - - it('burn more than balance on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce); - await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - - await expect(action.simulate()).rejects.toThrow(U128_UNDERFLOW_ERROR); - }); - - it('burn on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce); - await wallets[0].setPublicAuthWit({ caller: accounts[0].address, action }, true).send().wait(); - - await expect( - asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce).simulate(), - ).rejects.toThrow('Assertion failed: Message not authorized by account'); - }); + await expect( + asset.withWallet(wallets[1]).methods.burn_public(accounts[0].address, amount, nonce).simulate(), + ).rejects.toThrow('Assertion failed: Message not authorized by account'); }); }); + }); - describe('private', () => { - it('burn less than balance', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 / 2n; + describe('private', () => { + it('burn less than balance', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.burn(accounts[0].address, amount, 0).send().wait(); + tokenSim.burnPrivate(accounts[0].address, amount); + }); + + it('burn on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + await asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce).send().wait(); + tokenSim.burnPrivate(accounts[0].address, amount); + + // Perform the transfer again, should fail + const txReplay = asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce).send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('burn more than balance', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 + 1n; expect(amount).toBeGreaterThan(0n); - await asset.methods.burn(accounts[0].address, amount, 0).send().wait(); - tokenSim.burnPrivate(accounts[0].address, amount); + await expect(asset.methods.burn(accounts[0].address, amount, 0).simulate()).rejects.toThrow( + 'Assertion failed: Balance too low', + ); }); - it('burn on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 / 2n; + it('burn on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + await expect(asset.methods.burn(accounts[0].address, amount, 1).simulate()).rejects.toThrow( + 'Assertion failed: invalid nonce', + ); + }); + + it('burn more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 + 1n; const nonce = Fr.random(); expect(amount).toBeGreaterThan(0n); @@ -140,92 +179,50 @@ describe('e2e_token_contract', () => { const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); await wallets[1].addAuthWitness(witness); - await asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce).send().wait(); - tokenSim.burnPrivate(accounts[0].address, amount); + await expect(action.simulate()).rejects.toThrow('Assertion failed: Balance too low'); + }); - // Perform the transfer again, should fail - const txReplay = asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce).send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); + it('burn on behalf of other without approval', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce); + const messageHash = computeAuthWitMessageHash( + accounts[1].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + await expect(action.simulate()).rejects.toThrow( + `Unknown auth witness for message hash ${messageHash.toString()}`, + ); }); - describe('failure cases', () => { - it('burn more than balance', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 + 1n; - expect(amount).toBeGreaterThan(0n); - await expect(asset.methods.burn(accounts[0].address, amount, 0).simulate()).rejects.toThrow( - 'Assertion failed: Balance too low', - ); - }); - - it('burn on behalf of self with non-zero nonce', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 - 1n; - expect(amount).toBeGreaterThan(0n); - await expect(asset.methods.burn(accounts[0].address, amount, 1).simulate()).rejects.toThrow( - 'Assertion failed: invalid nonce', - ); - }); - - it('burn more than balance on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce); - - // Both wallets are connected to same node and PXE so we could just insert directly - // But doing it in two actions to show the flow. - const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); - await wallets[1].addAuthWitness(witness); - - await expect(action.simulate()).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('burn on behalf of other without approval', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.burn(accounts[0].address, amount, nonce); - const messageHash = computeAuthWitMessageHash( - accounts[1].address, - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - await expect(action.simulate()).rejects.toThrow( - `Unknown auth witness for message hash ${messageHash.toString()}`, - ); - }); - - it('on behalf of other (invalid designated caller)', async () => { - const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balancePriv0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[2]).methods.burn(accounts[0].address, amount, nonce); - const expectedMessageHash = computeAuthWitMessageHash( - accounts[2].address, - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); - await wallets[2].addAuthWitness(witness); - - await expect(action.simulate()).rejects.toThrow( - `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, - ); - }); + it('on behalf of other (invalid designated caller)', async () => { + const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[2]).methods.burn(accounts[0].address, amount, nonce); + const expectedMessageHash = computeAuthWitMessageHash( + accounts[2].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[2].addAuthWitness(witness); + + await expect(action.simulate()).rejects.toThrow( + `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, + ); }); }); }); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts index 23fa7f1916e..9bff43deee2 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts @@ -1,16 +1,18 @@ import { Fr, type TxHash, computeMessageSecretHash } from '@aztec/aztec.js'; import { BITSIZE_TOO_BIG_ERROR, U128_OVERFLOW_ERROR } from '../fixtures/fixtures.js'; -import { TestClass } from './test_class.js'; +import { TokenContractTest } from './token_contract_test.js'; -// const { E2E_DATA_PATH: dataPath = './data' } = process.env; - -describe('e2e_token_contract', () => { - const t = new TestClass('minting'); +describe('e2e_token_contract minting', () => { + const t = new TokenContractTest('minting'); let { asset, accounts, tokenSim, wallets } = t; beforeAll(async () => { - await t.setup(); + await t.pushBaseSnapshots(); + }); + + afterAll(async () => { + await t.popBaseSnapshots(); }); beforeEach(async () => { @@ -22,118 +24,110 @@ describe('e2e_token_contract', () => { await t.tokenSim.check(); }); - afterAll(async () => { - await t.snapshotManager.teardown(); - }); + describe('Public', () => { + it('as minter', async () => { + const amount = 10000n; + await asset.methods.mint_public(accounts[0].address, amount).send().wait(); - describe('Minting', () => { - describe('Public', () => { - it('as minter', async () => { + tokenSim.mintPublic(accounts[0].address, amount); + expect(await asset.methods.balance_of_public(accounts[0].address).simulate()).toEqual( + tokenSim.balanceOfPublic(accounts[0].address), + ); + expect(await asset.methods.total_supply().simulate()).toEqual(tokenSim.totalSupply); + }); + + describe('failure cases', () => { + it('as non-minter', async () => { const amount = 10000n; - await asset.methods.mint_public(accounts[0].address, amount).send().wait(); + await expect( + asset.withWallet(wallets[1]).methods.mint_public(accounts[0].address, amount).simulate(), + ).rejects.toThrow('Assertion failed: caller is not minter'); + }); + + it('mint >u128 tokens to overflow', async () => { + const amount = 2n ** 128n; // U128::max() + 1; + await expect(asset.methods.mint_public(accounts[0].address, amount).simulate()).rejects.toThrow( + BITSIZE_TOO_BIG_ERROR, + ); + }); + + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.balanceOfPublic(accounts[0].address); + await expect(asset.methods.mint_public(accounts[0].address, amount).simulate()).rejects.toThrow( + U128_OVERFLOW_ERROR, + ); + }); - tokenSim.mintPublic(accounts[0].address, amount); - expect(await asset.methods.balance_of_public(accounts[0].address).view()).toEqual( - tokenSim.balanceOfPublic(accounts[0].address), + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.balanceOfPublic(accounts[0].address); + await expect(asset.methods.mint_public(accounts[1].address, amount).simulate()).rejects.toThrow( + U128_OVERFLOW_ERROR, ); - expect(await asset.methods.total_supply().view()).toEqual(tokenSim.totalSupply); + }); + }); + }); + + describe('Private', () => { + const secret = Fr.random(); + const amount = 10000n; + let secretHash: Fr; + let txHash: TxHash; + + beforeAll(() => { + secretHash = computeMessageSecretHash(secret); + }); + + describe('Mint flow', () => { + it('mint_private as minter', async () => { + const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); + tokenSim.mintPrivate(amount); + txHash = receipt.txHash; }); - describe('failure cases', () => { - it('as non-minter', async () => { - const amount = 10000n; - await expect( - asset.withWallet(wallets[1]).methods.mint_public(accounts[0].address, amount).simulate(), - ).rejects.toThrow('Assertion failed: caller is not minter'); - }); - - it('mint >u128 tokens to overflow', async () => { - const amount = 2n ** 128n; // U128::max() + 1; - await expect(asset.methods.mint_public(accounts[0].address, amount).simulate()).rejects.toThrow( - BITSIZE_TOO_BIG_ERROR, - ); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.balanceOfPublic(accounts[0].address); - await expect(asset.methods.mint_public(accounts[0].address, amount).simulate()).rejects.toThrow( - U128_OVERFLOW_ERROR, - ); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.balanceOfPublic(accounts[0].address); - await expect(asset.methods.mint_public(accounts[1].address, amount).simulate()).rejects.toThrow( - U128_OVERFLOW_ERROR, - ); - }); + it('redeem as recipient', async () => { + await t.addPendingShieldNoteToPXE(0, amount, secretHash, txHash); + const txClaim = asset.methods.redeem_shield(accounts[0].address, amount, secret).send(); + // docs:start:debug + const receiptClaim = await txClaim.wait({ debug: true }); + // docs:end:debug + tokenSim.redeemShield(accounts[0].address, amount); + // 1 note should be created containing `amount` of tokens + const { visibleNotes } = receiptClaim.debugInfo!; + expect(visibleNotes.length).toBe(1); + expect(visibleNotes[0].note.items[0].toBigInt()).toBe(amount); }); }); - describe('Private', () => { - const secret = Fr.random(); - const amount = 10000n; - let secretHash: Fr; - let txHash: TxHash; + describe('failure cases', () => { + it('try to redeem as recipient (double-spend) [REVERTS]', async () => { + await expect(t.addPendingShieldNoteToPXE(0, amount, secretHash, txHash)).rejects.toThrow( + 'The note has been destroyed.', + ); + await expect(asset.methods.redeem_shield(accounts[0].address, amount, secret).simulate()).rejects.toThrow( + `Assertion failed: Cannot return zero notes`, + ); + }); + + it('mint_private as non-minter', async () => { + await expect(asset.withWallet(wallets[1]).methods.mint_private(amount, secretHash).simulate()).rejects.toThrow( + 'Assertion failed: caller is not minter', + ); + }); - beforeAll(() => { - secretHash = computeMessageSecretHash(secret); + it('mint >u128 tokens to overflow', async () => { + const amount = 2n ** 128n; // U128::max() + 1; + await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrow(BITSIZE_TOO_BIG_ERROR); }); - describe('Mint flow', () => { - it('mint_private as minter', async () => { - const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); - tokenSim.mintPrivate(amount); - txHash = receipt.txHash; - }); - - it('redeem as recipient', async () => { - await t.addPendingShieldNoteToPXE(0, amount, secretHash, txHash); - const txClaim = asset.methods.redeem_shield(accounts[0].address, amount, secret).send(); - // docs:start:debug - const receiptClaim = await txClaim.wait({ debug: true }); - // docs:end:debug - tokenSim.redeemShield(accounts[0].address, amount); - // 1 note should be created containing `amount` of tokens - const { visibleNotes } = receiptClaim.debugInfo!; - expect(visibleNotes.length).toBe(1); - expect(visibleNotes[0].note.items[0].toBigInt()).toBe(amount); - }); + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.balanceOfPrivate(accounts[0].address); + expect(amount).toBeLessThan(2n ** 128n); + await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrow(U128_OVERFLOW_ERROR); }); - describe('failure cases', () => { - it('try to redeem as recipient (double-spend) [REVERTS]', async () => { - await expect(t.addPendingShieldNoteToPXE(0, amount, secretHash, txHash)).rejects.toThrow( - 'The note has been destroyed.', - ); - await expect(asset.methods.redeem_shield(accounts[0].address, amount, secret).simulate()).rejects.toThrow( - `Assertion failed: Cannot return zero notes`, - ); - }); - - it('mint_private as non-minter', async () => { - await expect( - asset.withWallet(wallets[1]).methods.mint_private(amount, secretHash).simulate(), - ).rejects.toThrow('Assertion failed: caller is not minter'); - }); - - it('mint >u128 tokens to overflow', async () => { - const amount = 2n ** 128n; // U128::max() + 1; - await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrow( - BITSIZE_TOO_BIG_ERROR, - ); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.balanceOfPrivate(accounts[0].address); - expect(amount).toBeLessThan(2n ** 128n); - await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrow(U128_OVERFLOW_ERROR); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.totalSupply; - await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrow(U128_OVERFLOW_ERROR); - }); + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.totalSupply; + await expect(asset.methods.mint_private(amount, secretHash).simulate()).rejects.toThrow(U128_OVERFLOW_ERROR); }); }); }); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts index 73603a099c5..9bb2d1084f3 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts @@ -1,17 +1,45 @@ import { ReaderContract } from '@aztec/noir-contracts.js'; -import { TestClass } from './test_class.js'; +import { TokenContractTest } from './token_contract_test.js'; -// const { E2E_DATA_PATH: dataPath = './data' } = process.env; +const toString = (val: bigint[]) => { + let str = ''; + for (let i = 0; i < val.length; i++) { + if (val[i] != 0n) { + str += String.fromCharCode(Number(val[i])); + } + } + return str; +}; -describe('e2e_token_contract', () => { - const t = new TestClass('reading_constants'); - const { TOKEN_DECIMALS, TOKEN_NAME, TOKEN_SYMBOL } = TestClass; +describe('e2e_token_contract reading constants', () => { + const t = new TokenContractTest('reading_constants'); + const { TOKEN_DECIMALS, TOKEN_NAME, TOKEN_SYMBOL } = TokenContractTest; // Do not destructure anything mutable. const { logger } = t; + let reader: ReaderContract; beforeAll(async () => { - await t.setup(); + await t.pushBaseSnapshots(); + + await t.snapshotManager.snapshot( + 'reading_constants', + async () => { + logger('Deploying ReaderContract...'); + const reader = await ReaderContract.deploy(t.wallets[0]).send().deployed(); + logger(`Deployed ReaderContract to ${reader.address}.`); + return { readerAddress: reader.address }; + }, + async ({ readerAddress }) => { + reader = await ReaderContract.at(readerAddress, t.wallets[0]); + logger(`Reader contract restored to ${readerAddress}.`); + }, + ); + }); + + afterAll(async () => { + await t.snapshotManager.pop(); // reading_constants + await t.popBaseSnapshots(); }); beforeEach(async () => { @@ -22,110 +50,67 @@ describe('e2e_token_contract', () => { await t.tokenSim.check(); }); - afterAll(async () => { - await t.snapshotManager.teardown(); + it('check name private', async () => { + const name = toString(await t.asset.methods.un_get_name().simulate()); + expect(name).toBe(TOKEN_NAME); + + await reader.methods.check_name_private(t.asset.address, TOKEN_NAME).send().wait(); + await expect(reader.methods.check_name_private(t.asset.address, 'WRONG_NAME').simulate()).rejects.toThrow( + 'name.is_eq(_what)', + ); }); - const toString = (val: bigint[]) => { - let str = ''; - for (let i = 0; i < val.length; i++) { - if (val[i] != 0n) { - str += String.fromCharCode(Number(val[i])); - } - } - return str; - }; - - describe('Reading constants', () => { - let reader: ReaderContract; - - beforeAll(async () => { - await t.snapshotManager.snapshot( - 'reading_constants', - async () => { - logger('Deploying ReaderContract...'); - const reader = await ReaderContract.deploy(t.wallets[0]).send().deployed(); - logger(`Deployed ReaderContract to ${reader.address}.`); - return { readerAddress: reader.address }; - }, - async ({ readerAddress }) => { - reader = await ReaderContract.at(readerAddress, t.wallets[0]); - logger(`Reader contract restored to ${readerAddress}.`); - }, - ); - }); - - afterAll(async () => { - await t.snapshotManager.pop(); - }); - - describe('name', () => { - it('check name private', async () => { - const name = toString(await t.asset.methods.un_get_name().view()); - expect(name).toBe(TOKEN_NAME); - - await reader.methods.check_name_private(t.asset.address, TOKEN_NAME).send().wait(); - await expect(reader.methods.check_name_private(t.asset.address, 'WRONG_NAME').simulate()).rejects.toThrow( - 'name.is_eq(_what)', - ); - }); - - it('check name public', async () => { - const name = toString(await t.asset.methods.un_get_name().view()); - expect(name).toBe(TOKEN_NAME); - - await reader.methods.check_name_public(t.asset.address, TOKEN_NAME).send().wait(); - await expect(reader.methods.check_name_public(t.asset.address, 'WRONG_NAME').simulate()).rejects.toThrow( - 'name.is_eq(_what)', - ); - }); - }); - - describe('symbol', () => { - it('private', async () => { - const sym = toString(await t.asset.methods.un_get_symbol().view()); - expect(sym).toBe(TOKEN_SYMBOL); - - await reader.methods.check_symbol_private(t.asset.address, TOKEN_SYMBOL).send().wait(); - - await expect(reader.methods.check_symbol_private(t.asset.address, 'WRONG_SYMBOL').simulate()).rejects.toThrow( - "Cannot satisfy constraint 'symbol.is_eq(_what)'", - ); - }); - it('public', async () => { - const sym = toString(await t.asset.methods.un_get_symbol().view()); - expect(sym).toBe(TOKEN_SYMBOL); - - await reader.methods.check_symbol_public(t.asset.address, TOKEN_SYMBOL).send().wait(); - - await expect(reader.methods.check_symbol_public(t.asset.address, 'WRONG_SYMBOL').simulate()).rejects.toThrow( - "Failed to solve brillig function, reason: explicit trap hit in brillig 'symbol.is_eq(_what)'", - ); - }); - }); - - describe('decimals', () => { - it('private', async () => { - const dec = await t.asset.methods.un_get_decimals().view(); - expect(dec).toBe(TOKEN_DECIMALS); - - await reader.methods.check_decimals_private(t.asset.address, TOKEN_DECIMALS).send().wait(); - - await expect(reader.methods.check_decimals_private(t.asset.address, 99).simulate()).rejects.toThrow( - "Cannot satisfy constraint 'ret[0] as u8 == what'", - ); - }); - - it('public', async () => { - const dec = await t.asset.methods.un_get_decimals().view(); - expect(dec).toBe(TOKEN_DECIMALS); - - await reader.methods.check_decimals_public(t.asset.address, TOKEN_DECIMALS).send().wait(); - - await expect(reader.methods.check_decimals_public(t.asset.address, 99).simulate()).rejects.toThrow( - "Failed to solve brillig function, reason: explicit trap hit in brillig 'ret[0] as u8 == what'", - ); - }); - }); + it('check name public', async () => { + const name = toString(await t.asset.methods.un_get_name().simulate()); + expect(name).toBe(TOKEN_NAME); + + await reader.methods.check_name_public(t.asset.address, TOKEN_NAME).send().wait(); + await expect(reader.methods.check_name_public(t.asset.address, 'WRONG_NAME').simulate()).rejects.toThrow( + 'name.is_eq(_what)', + ); + }); + + it('check symbol private', async () => { + const sym = toString(await t.asset.methods.un_get_symbol().simulate()); + expect(sym).toBe(TOKEN_SYMBOL); + + await reader.methods.check_symbol_private(t.asset.address, TOKEN_SYMBOL).send().wait(); + + await expect(reader.methods.check_symbol_private(t.asset.address, 'WRONG_SYMBOL').simulate()).rejects.toThrow( + "Cannot satisfy constraint 'symbol.is_eq(_what)'", + ); + }); + + it('check symbol public', async () => { + const sym = toString(await t.asset.methods.un_get_symbol().simulate()); + expect(sym).toBe(TOKEN_SYMBOL); + + await reader.methods.check_symbol_public(t.asset.address, TOKEN_SYMBOL).send().wait(); + + await expect(reader.methods.check_symbol_public(t.asset.address, 'WRONG_SYMBOL').simulate()).rejects.toThrow( + "Failed to solve brillig function, reason: explicit trap hit in brillig 'symbol.is_eq(_what)'", + ); + }); + + it('check decimals private', async () => { + const dec = await t.asset.methods.un_get_decimals().simulate(); + expect(dec).toBe(TOKEN_DECIMALS); + + await reader.methods.check_decimals_private(t.asset.address, TOKEN_DECIMALS).send().wait(); + + await expect(reader.methods.check_decimals_private(t.asset.address, 99).simulate()).rejects.toThrow( + "Cannot satisfy constraint 'ret[0] as u8 == what'", + ); + }); + + it('check decimals public', async () => { + const dec = await t.asset.methods.un_get_decimals().simulate(); + expect(dec).toBe(TOKEN_DECIMALS); + + await reader.methods.check_decimals_public(t.asset.address, TOKEN_DECIMALS).send().wait(); + + await expect(reader.methods.check_decimals_public(t.asset.address, 99).simulate()).rejects.toThrow( + "Failed to solve brillig function, reason: explicit trap hit in brillig 'ret[0] as u8 == what'", + ); }); }); 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/sheilding.test.ts index 0cd20070894..14da530347a 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/sheilding.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/sheilding.test.ts @@ -1,16 +1,23 @@ import { Fr, computeMessageSecretHash } from '@aztec/aztec.js'; import { U128_UNDERFLOW_ERROR } from '../fixtures/fixtures.js'; -import { TestClass } from './test_class.js'; +import { TokenContractTest } from './token_contract_test.js'; -// const { E2E_DATA_PATH: dataPath = './data' } = process.env; - -describe('e2e_token_contract', () => { - const t = new TestClass('shielding'); +describe('e2e_token_contract shield + redeem shield', () => { + const t = new TokenContractTest('shielding'); let { asset, accounts, tokenSim, wallets } = t; + const secret = Fr.random(); + let secretHash: Fr; beforeAll(async () => { - await t.setup(); + await t.pushBaseSnapshots(); + await t.pushMintSnapshot(); + secretHash = computeMessageSecretHash(secret); + }); + + afterAll(async () => { + await t.snapshotManager.pop(); // mint + await t.popBaseSnapshots(); }); beforeEach(async () => { @@ -23,39 +30,73 @@ describe('e2e_token_contract', () => { await t.tokenSim.check(); }); - afterAll(async () => { - await t.snapshotManager.teardown(); + it('on behalf of self', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balancePub / 2n; + expect(amount).toBeGreaterThan(0n); + + const receipt = await asset.methods.shield(accounts[0].address, amount, secretHash, 0).send().wait(); + + tokenSim.shield(accounts[0].address, amount); + await tokenSim.check(); + + // Redeem it + await t.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + await asset.methods.redeem_shield(accounts[0].address, amount, secret).send().wait(); + + tokenSim.redeemShield(accounts[0].address, amount); }); - describe('Shielding (shield + redeem_shield)', () => { - const secret = Fr.random(); - let secretHash: Fr; + it('on behalf of other', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balancePub / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); - beforeAll(async () => { - await t.addTransferSnapshot(); - secretHash = computeMessageSecretHash(secret); - }); + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.shield(accounts[0].address, amount, secretHash, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - it('on behalf of self', async () => { - const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balancePub / 2n; - expect(amount).toBeGreaterThan(0n); + const receipt = await action.send().wait(); + + tokenSim.shield(accounts[0].address, amount); + await tokenSim.check(); + + // Check that replaying the shield should fail! + const txReplay = asset.withWallet(wallets[1]).methods.shield(accounts[0].address, amount, secretHash, nonce).send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + + // Redeem it + await t.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + await asset.methods.redeem_shield(accounts[0].address, amount, secret).send().wait(); - const receipt = await asset.methods.shield(accounts[0].address, amount, secretHash, 0).send().wait(); + tokenSim.redeemShield(accounts[0].address, amount); + }); - tokenSim.shield(accounts[0].address, amount); - await tokenSim.check(); + describe('failure cases', () => { + it('on behalf of self (more than balance)', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balancePub + 1n; + expect(amount).toBeGreaterThan(0n); - // Redeem it - await t.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); - await asset.methods.redeem_shield(accounts[0].address, amount, secret).send().wait(); + await expect(asset.methods.shield(accounts[0].address, amount, secretHash, 0).simulate()).rejects.toThrow( + U128_UNDERFLOW_ERROR, + ); + }); - tokenSim.redeemShield(accounts[0].address, amount); + it('on behalf of self (invalid nonce)', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balancePub + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect(asset.methods.shield(accounts[0].address, amount, secretHash, 1).simulate()).rejects.toThrow( + 'Assertion failed: invalid nonce', + ); }); - it('on behalf of other', async () => { - const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balancePub / 2n; + it('on behalf of other (more than balance)', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balancePub + 1n; const nonce = Fr.random(); expect(amount).toBeGreaterThan(0n); @@ -63,82 +104,31 @@ describe('e2e_token_contract', () => { const action = asset.withWallet(wallets[1]).methods.shield(accounts[0].address, amount, secretHash, nonce); await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - const receipt = await action.send().wait(); - - tokenSim.shield(accounts[0].address, amount); - await tokenSim.check(); + await expect(action.simulate()).rejects.toThrow(U128_UNDERFLOW_ERROR); + }); - // Check that replaying the shield should fail! - const txReplay = asset - .withWallet(wallets[1]) - .methods.shield(accounts[0].address, amount, secretHash, nonce) - .send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); + it('on behalf of other (wrong designated caller)', async () => { + const balancePub = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balancePub + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); - // Redeem it - await t.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); - await asset.methods.redeem_shield(accounts[0].address, amount, secret).send().wait(); + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[2]).methods.shield(accounts[0].address, amount, secretHash, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - tokenSim.redeemShield(accounts[0].address, amount); + await expect(action.simulate()).rejects.toThrow('Assertion failed: Message not authorized by account'); }); - describe('failure cases', () => { - it('on behalf of self (more than balance)', async () => { - const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balancePub + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect(asset.methods.shield(accounts[0].address, amount, secretHash, 0).simulate()).rejects.toThrow( - U128_UNDERFLOW_ERROR, - ); - }); - - it('on behalf of self (invalid nonce)', async () => { - const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balancePub + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect(asset.methods.shield(accounts[0].address, amount, secretHash, 1).simulate()).rejects.toThrow( - 'Assertion failed: invalid nonce', - ); - }); - - it('on behalf of other (more than balance)', async () => { - const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balancePub + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.shield(accounts[0].address, amount, secretHash, nonce); - await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - - await expect(action.simulate()).rejects.toThrow(U128_UNDERFLOW_ERROR); - }); - - it('on behalf of other (wrong designated caller)', async () => { - const balancePub = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balancePub + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[2]).methods.shield(accounts[0].address, amount, secretHash, nonce); - await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - - await expect(action.simulate()).rejects.toThrow('Assertion failed: Message not authorized by account'); - }); - - it('on behalf of other (without approval)', async () => { - const balance = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.withWallet(wallets[1]).methods.shield(accounts[0].address, amount, secretHash, nonce).simulate(), - ).rejects.toThrow(`Assertion failed: Message not authorized by account`); - }); + it('on behalf of other (without approval)', async () => { + const balance = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.withWallet(wallets[1]).methods.shield(accounts[0].address, amount, secretHash, nonce).simulate(), + ).rejects.toThrow(`Assertion failed: Message not authorized by account`); }); }); }); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/test_class.ts b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts similarity index 83% rename from yarn-project/end-to-end/src/e2e_token_contract/test_class.ts rename to yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts index 76b8958847b..9402a70b63f 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/test_class.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts @@ -2,6 +2,7 @@ import { getSchnorrAccount } from '@aztec/accounts/schnorr'; import { type AccountWallet, type CompleteAddress, + type DebugLogger, ExtendedNote, Fr, Note, @@ -16,12 +17,12 @@ import { TokenSimulator } from '../simulators/token_simulator.js'; const { E2E_DATA_PATH: dataPath = './data' } = process.env; -export class TestClass { +export class TokenContractTest { static TOKEN_NAME = 'Aztec Token'; static TOKEN_SYMBOL = 'AZT'; static TOKEN_DECIMALS = 18n; - logger = createDebugLogger('aztec:e2e_token_contract'); - snapshotManager = new SnapshotManager('e2e_token_contract', dataPath, this.logger); + logger: DebugLogger; + snapshotManager: SnapshotManager; wallets: AccountWallet[] = []; accounts: CompleteAddress[] = []; asset!: TokenContract; @@ -29,11 +30,17 @@ export class TestClass { badAccount!: DocsExampleContract; constructor(testName: string) { + this.logger = createDebugLogger(`aztec:e2e_token_contract:${testName}`); this.snapshotManager = new SnapshotManager(`e2e_token_contract/${testName}`, dataPath, this.logger); } - async setup() { - await this.snapshotManager.snapshot('3-accounts', addAccounts(3), async ({ accountKeys }, { pxe }) => { + /** + * Adds two state shifts to snapshot manager. + * 1. Add 3 accounts. + * 2. Publicly deploy accounts, deploy token contract and a "bad account". + */ + async pushBaseSnapshots() { + await this.snapshotManager.snapshot('3_accounts', addAccounts(3), 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.accounts = await pxe.getRegisteredAccounts(); @@ -53,9 +60,9 @@ export class TestClass { const asset = await TokenContract.deploy( this.wallets[0], this.accounts[0], - TestClass.TOKEN_NAME, - TestClass.TOKEN_SYMBOL, - TestClass.TOKEN_DECIMALS, + TokenContractTest.TOKEN_NAME, + TokenContractTest.TOKEN_SYMBOL, + TokenContractTest.TOKEN_DECIMALS, ) .send() .deployed(); @@ -81,7 +88,7 @@ export class TestClass { this.badAccount = await DocsExampleContract.at(badAccountAddress, this.wallets[0]); this.logger(`Bad account restored to ${this.badAccount.address}.`); - expect(await this.asset.methods.admin().view()).toBe(this.accounts[0].address.toBigInt()); + expect(await this.asset.methods.admin().simulate()).toBe(this.accounts[0].address.toBigInt()); }, ); @@ -91,6 +98,11 @@ export class TestClass { // }); } + async popBaseSnapshots() { + await this.snapshotManager.pop(); // e2e_token_contract + await this.snapshotManager.pop(); // 3_accounts + } + async addPendingShieldNoteToPXE(accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) { const storageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote @@ -107,11 +119,10 @@ export class TestClass { await this.wallets[accountIndex].addNote(extendedNote); } - async addTransferSnapshot() { + async pushMintSnapshot() { await this.snapshotManager.snapshot( - 'transfer', + 'mint', async () => { - // Have to destructure again to ensure we have latest refs. const { asset, accounts } = this; const amount = 10000n; await asset.methods.mint_public(accounts[0].address, amount).send().wait(); @@ -129,12 +140,12 @@ export class TestClass { async ({ amount }) => { const { asset, accounts, tokenSim } = this; tokenSim.mintPublic(accounts[0].address, amount); - expect(await asset.methods.balance_of_public(accounts[0].address).view()).toEqual( + expect(await asset.methods.balance_of_public(accounts[0].address).simulate()).toEqual( tokenSim.balanceOfPublic(accounts[0].address), ); tokenSim.mintPrivate(amount); tokenSim.redeemShield(accounts[0].address, amount); - expect(await asset.methods.total_supply().view()).toEqual(tokenSim.totalSupply); + expect(await asset.methods.total_supply().simulate()).toEqual(tokenSim.totalSupply); return Promise.resolve(); }, ); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts index 822eb98b7e5..4df010374a9 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts @@ -1,15 +1,19 @@ import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; -import { TestClass } from './test_class.js'; +import { TokenContractTest } from './token_contract_test.js'; -// const { E2E_DATA_PATH: dataPath = './data' } = process.env; - -describe('e2e_token_contract', () => { - const t = new TestClass('transfer_private'); +describe('e2e_token_contract transfer private', () => { + const t = new TokenContractTest('transfer_private'); let { asset, accounts, tokenSim, wallets, badAccount } = t; beforeAll(async () => { - await t.setup(); + await t.pushBaseSnapshots(); + await t.pushMintSnapshot(); + }); + + afterAll(async () => { + await t.snapshotManager.pop(); // mint + await t.popBaseSnapshots(); }); beforeEach(async () => { @@ -21,223 +25,211 @@ describe('e2e_token_contract', () => { await t.tokenSim.check(); }); - afterAll(async () => { - await t.snapshotManager.teardown(); + it('transfer less than balance', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer(accounts[0].address, accounts[1].address, amount, 0).send().wait(); + tokenSim.transferPrivate(accounts[0].address, accounts[1].address, amount); }); - describe('Transfer', () => { - beforeAll(async () => await t.addTransferSnapshot()); - - describe('private', () => { - it('transfer less than balance', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer(accounts[0].address, accounts[1].address, amount, 0).send().wait(); - tokenSim.transferPrivate(accounts[0].address, accounts[1].address, amount); - }); - - it('transfer to self', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer(accounts[0].address, accounts[0].address, amount, 0).send().wait(); - tokenSim.transferPrivate(accounts[0].address, accounts[0].address, amount); - }); - - it('transfer on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - // docs:start:authwit_transfer_example - const action = asset - .withWallet(wallets[1]) - .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); - - const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); - await wallets[1].addAuthWitness(witness); - expect( - await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action }), - ).toEqual({ - isValidInPrivate: true, - isValidInPublic: false, - }); - // docs:end:authwit_transfer_example - - // Perform the transfer - await action.send().wait(); - tokenSim.transferPrivate(accounts[0].address, accounts[1].address, amount); - - // Perform the transfer again, should fail - const txReplay = asset - .withWallet(wallets[1]) - .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce) - .send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); - }); - - describe('failure cases', () => { - it('transfer more than balance', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 + 1n; - expect(amount).toBeGreaterThan(0n); - await expect( - asset.methods.transfer(accounts[0].address, accounts[1].address, amount, 0).simulate(), - ).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('transfer on behalf of self with non-zero nonce', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 - 1n; - expect(amount).toBeGreaterThan(0n); - await expect( - asset.methods.transfer(accounts[0].address, accounts[1].address, amount, 1).simulate(), - ).rejects.toThrow('Assertion failed: invalid nonce'); - }); - - it('transfer more than balance on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const balance1 = await asset.methods.balance_of_private(accounts[1].address).view(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); - - // Both wallets are connected to same node and PXE so we could just insert directly using - // await wallet.signAndAddAuthWitness(messageHash, ); - // But doing it in two actions to show the flow. - const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); - await wallets[1].addAuthWitness(witness); - - // Perform the transfer - await expect(action.simulate()).rejects.toThrow('Assertion failed: Balance too low'); - expect(await asset.methods.balance_of_private(accounts[0].address).view()).toEqual(balance0); - expect(await asset.methods.balance_of_private(accounts[1].address).view()).toEqual(balance1); - }); - - it.skip('transfer into account to overflow', () => { - // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not - // a way to get funds enough to overflow. - // Require direct storage manipulation for us to perform a nice explicit case though. - // See https://github.com/AztecProtocol/aztec-packages/issues/1259 - }); - - it('transfer on behalf of other without approval', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); - const messageHash = computeAuthWitMessageHash( - accounts[1].address, - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - await expect(action.simulate()).rejects.toThrow( - `Unknown auth witness for message hash ${messageHash.toString()}`, - ); - }); - - it('transfer on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[2]) - .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); - const expectedMessageHash = computeAuthWitMessageHash( - accounts[2].address, - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); - await wallets[2].addAuthWitness(witness); - - await expect(action.simulate()).rejects.toThrow( - `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, - ); - expect(await asset.methods.balance_of_private(accounts[0].address).view()).toEqual(balance0); - }); - - it('transfer on behalf of other, cancelled authwit', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); - - const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); - await wallets[1].addAuthWitness(witness); - - await wallets[0].cancelAuthWit(witness.requestHash).send().wait(); - - // Perform the transfer, should fail because nullifier already emitted - const txCancelledAuthwit = asset - .withWallet(wallets[1]) - .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce) - .send(); - await expect(txCancelledAuthwit.wait()).rejects.toThrowError('Transaction '); - }); - - it('transfer on behalf of other, cancelled authwit, flow 2', async () => { - const balance0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); - - const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); - await wallets[1].addAuthWitness(witness); - - await wallets[0].cancelAuthWit({ caller: accounts[1].address, action }).send().wait(); - - // Perform the transfer, should fail because nullifier already emitted - const txCancelledAuthwit = asset - .withWallet(wallets[1]) - .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce) - .send(); - await expect(txCancelledAuthwit.wait()).rejects.toThrow('Transaction '); - }); - - it('transfer on behalf of other, invalid spend_private_authwit on "from"', async () => { - const nonce = Fr.random(); - - // Should fail as the returned value from the badAccount is malformed - const txCancelledAuthwit = asset - .withWallet(wallets[1]) - .methods.transfer(badAccount.address, accounts[1].address, 0, nonce) - .send(); - await expect(txCancelledAuthwit.wait()).rejects.toThrow( - "Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'", - ); - }); - }); + it('transfer to self', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer(accounts[0].address, accounts[0].address, amount, 0).send().wait(); + tokenSim.transferPrivate(accounts[0].address, accounts[0].address, amount); + }); + + it('transfer on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + // docs:start:authwit_transfer_example + const action = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + expect(await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action })).toEqual({ + isValidInPrivate: true, + isValidInPublic: false, + }); + // docs:end:authwit_transfer_example + + // Perform the transfer + await action.send().wait(); + tokenSim.transferPrivate(accounts[0].address, accounts[1].address, amount); + + // Perform the transfer again, should fail + const txReplay = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('transfer more than balance', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 + 1n; + expect(amount).toBeGreaterThan(0n); + await expect( + asset.methods.transfer(accounts[0].address, accounts[1].address, amount, 0).simulate(), + ).rejects.toThrow('Assertion failed: Balance too low'); + }); + + it('transfer on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + await expect( + asset.methods.transfer(accounts[0].address, accounts[1].address, amount, 1).simulate(), + ).rejects.toThrow('Assertion failed: invalid nonce'); + }); + + it('transfer more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const balance1 = await asset.methods.balance_of_private(accounts[1].address).simulate(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly using + // await wallet.signAndAddAuthWitness(messageHash, ); + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + // Perform the transfer + await expect(action.simulate()).rejects.toThrow('Assertion failed: Balance too low'); + expect(await asset.methods.balance_of_private(accounts[0].address).simulate()).toEqual(balance0); + expect(await asset.methods.balance_of_private(accounts[1].address).simulate()).toEqual(balance1); + }); + + it.skip('transfer into account to overflow', () => { + // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not + // a way to get funds enough to overflow. + // Require direct storage manipulation for us to perform a nice explicit case though. + // See https://github.com/AztecProtocol/aztec-packages/issues/1259 + }); + + it('transfer on behalf of other without approval', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + const messageHash = computeAuthWitMessageHash( + accounts[1].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + await expect(action.simulate()).rejects.toThrow( + `Unknown auth witness for message hash ${messageHash.toString()}`, + ); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[2]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + const expectedMessageHash = computeAuthWitMessageHash( + accounts[2].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[2].addAuthWitness(witness); + + await expect(action.simulate()).rejects.toThrow( + `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, + ); + expect(await asset.methods.balance_of_private(accounts[0].address).simulate()).toEqual(balance0); + }); + + it('transfer on behalf of other, cancelled authwit', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + await wallets[0].cancelAuthWit(witness.requestHash).send().wait(); + + // Perform the transfer, should fail because nullifier already emitted + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrowError('Transaction '); + }); + + it('transfer on behalf of other, cancelled authwit, flow 2', async () => { + const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce); + + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + await wallets[0].cancelAuthWit({ caller: accounts[1].address, action }).send().wait(); + + // Perform the transfer, should fail because nullifier already emitted + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrow('Transaction '); + }); + + it('transfer on behalf of other, invalid spend_private_authwit on "from"', async () => { + const nonce = Fr.random(); + + // Should fail as the returned value from the badAccount is malformed + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer(badAccount.address, accounts[1].address, 0, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrow( + "Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'", + ); }); }); }); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts index a1e7ca6a6a2..f29b1c91afd 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts @@ -1,16 +1,20 @@ import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; import { U128_UNDERFLOW_ERROR } from '../fixtures/fixtures.js'; -import { TestClass } from './test_class.js'; +import { TokenContractTest } from './token_contract_test.js'; -// const { E2E_DATA_PATH: dataPath = './data' } = process.env; - -describe('e2e_token_contract', () => { - const t = new TestClass('transfer_public'); +describe('e2e_token_contract transfer public', () => { + const t = new TokenContractTest('transfer_public'); let { asset, accounts, tokenSim, wallets, badAccount } = t; beforeAll(async () => { - await t.setup(); + await t.pushBaseSnapshots(); + await t.pushMintSnapshot(); + }); + + afterAll(async () => { + await t.snapshotManager.pop(); // mint + await t.popBaseSnapshots(); }); beforeEach(async () => { @@ -23,258 +27,248 @@ describe('e2e_token_contract', () => { await t.tokenSim.check(); }); - afterAll(async () => { - await t.snapshotManager.teardown(); + it('transfer less than balance', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer_public(accounts[0].address, accounts[1].address, amount, 0).send().wait(); + + tokenSim.transferPublic(accounts[0].address, accounts[1].address, amount); }); - describe('Transfer', () => { - beforeAll(async () => await t.addTransferSnapshot()); + it('transfer to self', async () => { + const balance = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer_public(accounts[0].address, accounts[0].address, amount, 0).send().wait(); - describe('public', () => { - it('transfer less than balance', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer_public(accounts[0].address, accounts[1].address, amount, 0).send().wait(); + tokenSim.transferPublic(accounts[0].address, accounts[0].address, amount); + }); - tokenSim.transferPublic(accounts[0].address, accounts[1].address, amount); - }); + it('transfer on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); - it('transfer to self', async () => { - const balance = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer_public(accounts[0].address, accounts[0].address, amount, 0).send().wait(); + // docs:start:authwit_public_transfer_example + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); - tokenSim.transferPublic(accounts[0].address, accounts[0].address, amount); - }); + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + // docs:end:authwit_public_transfer_example - it('transfer on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - const nonce = Fr.random(); + // Perform the transfer + await action.send().wait(); - // docs:start:authwit_public_transfer_example - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + tokenSim.transferPublic(accounts[0].address, accounts[1].address, amount); - await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - // docs:end:authwit_public_transfer_example + // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txReplay = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); - // Perform the transfer - await action.send().wait(); + describe('failure cases', () => { + it('transfer more than balance', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 + 1n; + const nonce = 0; + await expect( + asset.methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce).simulate(), + ).rejects.toThrow(U128_UNDERFLOW_ERROR); + }); - tokenSim.transferPublic(accounts[0].address, accounts[1].address, amount); + it('transfer on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 - 1n; + const nonce = 1; + await expect( + asset.methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce).simulate(), + ).rejects.toThrow('Assertion failed: invalid nonce'); + }); - // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. - const txReplay = asset + it('transfer on behalf of other without "approval"', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + await expect( + asset .withWallet(wallets[1]) .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) - .send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); - }); - - describe('failure cases', () => { - it('transfer more than balance', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 + 1n; - const nonce = 0; - await expect( - asset.methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce).simulate(), - ).rejects.toThrow(U128_UNDERFLOW_ERROR); - }); - - it('transfer on behalf of self with non-zero nonce', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 - 1n; - const nonce = 1; - await expect( - asset.methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce).simulate(), - ).rejects.toThrow('Assertion failed: invalid nonce'); - }); - - it('transfer on behalf of other without "approval"', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - await expect( - asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) - .simulate(), - ).rejects.toThrow('Assertion failed: Message not authorized by account'); - }); - - it('transfer more than balance on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const balance1 = await asset.methods.balance_of_public(accounts[1].address).view(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); - - expect( - await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action }), - ).toEqual({ - isValidInPrivate: false, - isValidInPublic: false, - }); - - // We need to compute the message we want to sign and add it to the wallet as approved - await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - - expect( - await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action }), - ).toEqual({ - isValidInPrivate: false, - isValidInPublic: true, - }); - - // Perform the transfer - await expect(action.simulate()).rejects.toThrow(U128_UNDERFLOW_ERROR); - - expect(await asset.methods.balance_of_public(accounts[0].address).view()).toEqual(balance0); - expect(await asset.methods.balance_of_public(accounts[1].address).view()).toEqual(balance1); - }); - - it('transfer on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const balance1 = await asset.methods.balance_of_public(accounts[1].address).view(); - const amount = balance0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); - - await wallets[0].setPublicAuthWit({ caller: accounts[0].address, action }, true).send().wait(); - - // Perform the transfer - await expect(action.simulate()).rejects.toThrow('Assertion failed: Message not authorized by account'); - - expect(await asset.methods.balance_of_public(accounts[0].address).view()).toEqual(balance0); - expect(await asset.methods.balance_of_public(accounts[1].address).view()).toEqual(balance1); - }); - - it('transfer on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const balance1 = await asset.methods.balance_of_public(accounts[1].address).view(); - const amount = balance0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); - await wallets[0].setPublicAuthWit({ caller: accounts[0].address, action }, true).send().wait(); - - // Perform the transfer - await expect(action.simulate()).rejects.toThrow('Assertion failed: Message not authorized by account'); - - expect(await asset.methods.balance_of_public(accounts[0].address).view()).toEqual(balance0); - expect(await asset.methods.balance_of_public(accounts[1].address).view()).toEqual(balance1); - }); - - it('transfer on behalf of other, cancelled authwit', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - const nonce = Fr.random(); - - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); - - await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - - await wallets[0].cancelAuthWit({ caller: accounts[1].address, action }).send().wait(); - - // Check that the authwit is no longer valid. Need to try to send since nullifiers are handled by sequencer. - const txCancelledAuthwit = asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) - .send(); - await expect(txCancelledAuthwit.wait()).rejects.toThrowError('Transaction '); - }); - - it('transfer on behalf of other, cancelled authwit, flow 2', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - const nonce = Fr.random(); - - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); - - await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); - - await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, false).send().wait(); - - // Check that the authwit is no longer valid. Need to try to send since nullifiers are handled by sequencer. - const txCancelledAuthwit = asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) - .send(); - await expect(txCancelledAuthwit.wait()).rejects.toThrowError('Transaction '); - }); - - it('transfer on behalf of other, cancelled authwit, flow 3', async () => { - const balance0 = await asset.methods.balance_of_public(accounts[0].address).view(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - const nonce = Fr.random(); - - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); - const messageHash = computeAuthWitMessageHash( - accounts[1].address, - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - await wallets[0].setPublicAuthWit(messageHash, true).send().wait(); - - await wallets[0].cancelAuthWit(messageHash).send().wait(); - - // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. - const txCancelledAuthwit = asset - .withWallet(wallets[1]) - .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) - .send(); - await expect(txCancelledAuthwit.wait()).rejects.toThrow('Transaction '); - }); - - it('transfer on behalf of other, invalid spend_public_authwit on "from"', async () => { - const nonce = Fr.random(); - - // Should fail as the returned value from the badAccount is malformed - const txCancelledAuthwit = asset - .withWallet(wallets[1]) - .methods.transfer_public(badAccount.address, accounts[1].address, 0, nonce) - .send(); - await expect(txCancelledAuthwit.wait()).rejects.toThrow( - "Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'", - ); - }); - - it.skip('transfer into account to overflow', () => { - // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not - // a way to get funds enough to overflow. - // Require direct storage manipulation for us to perform a nice explicit case though. - // See https://github.com/AztecProtocol/aztec-packages/issues/1259 - }); - }); + .simulate(), + ).rejects.toThrow('Assertion failed: Message not authorized by account'); + }); + + it('transfer more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const balance1 = await asset.methods.balance_of_public(accounts[1].address).simulate(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + + expect(await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action })).toEqual( + { + isValidInPrivate: false, + isValidInPublic: false, + }, + ); + + // We need to compute the message we want to sign and add it to the wallet as approved + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + expect(await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action })).toEqual( + { + isValidInPrivate: false, + isValidInPublic: true, + }, + ); + + // Perform the transfer + await expect(action.simulate()).rejects.toThrow(U128_UNDERFLOW_ERROR); + + expect(await asset.methods.balance_of_public(accounts[0].address).simulate()).toEqual(balance0); + expect(await asset.methods.balance_of_public(accounts[1].address).simulate()).toEqual(balance1); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const balance1 = await asset.methods.balance_of_public(accounts[1].address).simulate(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + + await wallets[0].setPublicAuthWit({ caller: accounts[0].address, action }, true).send().wait(); + + // Perform the transfer + await expect(action.simulate()).rejects.toThrow('Assertion failed: Message not authorized by account'); + + expect(await asset.methods.balance_of_public(accounts[0].address).simulate()).toEqual(balance0); + expect(await asset.methods.balance_of_public(accounts[1].address).simulate()).toEqual(balance1); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const balance1 = await asset.methods.balance_of_public(accounts[1].address).simulate(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + await wallets[0].setPublicAuthWit({ caller: accounts[0].address, action }, true).send().wait(); + + // Perform the transfer + await expect(action.simulate()).rejects.toThrow('Assertion failed: Message not authorized by account'); + + expect(await asset.methods.balance_of_public(accounts[0].address).simulate()).toEqual(balance0); + expect(await asset.methods.balance_of_public(accounts[1].address).simulate()).toEqual(balance1); + }); + + it('transfer on behalf of other, cancelled authwit', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + await wallets[0].cancelAuthWit({ caller: accounts[1].address, action }).send().wait(); + + // Check that the authwit is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrowError('Transaction '); + }); + + it('transfer on behalf of other, cancelled authwit, flow 2', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); + + await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, false).send().wait(); + + // Check that the authwit is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrowError('Transaction '); + }); + + it('transfer on behalf of other, cancelled authwit, flow 3', async () => { + const balance0 = await asset.methods.balance_of_public(accounts[0].address).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce); + const messageHash = computeAuthWitMessageHash( + accounts[1].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + await wallets[0].setPublicAuthWit(messageHash, true).send().wait(); + + await wallets[0].cancelAuthWit(messageHash).send().wait(); + + // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrow('Transaction '); + }); + + it('transfer on behalf of other, invalid spend_public_authwit on "from"', async () => { + const nonce = Fr.random(); + + // Should fail as the returned value from the badAccount is malformed + const txCancelledAuthwit = asset + .withWallet(wallets[1]) + .methods.transfer_public(badAccount.address, accounts[1].address, 0, nonce) + .send(); + await expect(txCancelledAuthwit.wait()).rejects.toThrow( + "Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'", + ); + }); + + it.skip('transfer into account to overflow', () => { + // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not + // a way to get funds enough to overflow. + // Require direct storage manipulation for us to perform a nice explicit case though. + // See https://github.com/AztecProtocol/aztec-packages/issues/1259 }); }); }); 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/unsheilding.test.ts index 433150340c8..38d7a627deb 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/unsheilding.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/unsheilding.test.ts @@ -1,16 +1,19 @@ import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; -import { TestClass } from './test_class.js'; +import { TokenContractTest } from './token_contract_test.js'; -// const { E2E_DATA_PATH: dataPath = './data' } = process.env; - -describe('e2e_token_contract', () => { - const t = new TestClass('unshielding'); +describe('e2e_token_contract unshielding', () => { + const t = new TokenContractTest('unshielding'); let { asset, accounts, tokenSim, wallets } = t; beforeAll(async () => { - await t.setup(); - await t.addTransferSnapshot(); + await t.pushBaseSnapshots(); + await t.pushMintSnapshot(); + }); + + afterAll(async () => { + await t.snapshotManager.pop(); // mint + await t.popBaseSnapshots(); }); beforeEach(async () => { @@ -23,24 +26,67 @@ describe('e2e_token_contract', () => { await t.tokenSim.check(); }); - afterAll(async () => { - await t.snapshotManager.teardown(); + it('on behalf of self', async () => { + const balancePriv = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balancePriv / 2n; + expect(amount).toBeGreaterThan(0n); + + await asset.methods.unshield(accounts[0].address, accounts[0].address, amount, 0).send().wait(); + + tokenSim.unshield(accounts[0].address, accounts[0].address, amount); }); - describe('Unshielding', () => { - it('on behalf of self', async () => { - const balancePriv = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balancePriv / 2n; + it('on behalf of other', async () => { + const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balancePriv0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[1].addAuthWitness(witness); + + await action.send().wait(); + tokenSim.unshield(accounts[0].address, accounts[1].address, amount); + + // Perform the transfer again, should fail + const txReplay = asset + .withWallet(wallets[1]) + .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('on behalf of self (more than balance)', async () => { + const balancePriv = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balancePriv + 1n; expect(amount).toBeGreaterThan(0n); - await asset.methods.unshield(accounts[0].address, accounts[0].address, amount, 0).send().wait(); + await expect( + asset.methods.unshield(accounts[0].address, accounts[0].address, amount, 0).simulate(), + ).rejects.toThrow('Assertion failed: Balance too low'); + }); + + it('on behalf of self (invalid nonce)', async () => { + const balancePriv = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balancePriv + 1n; + expect(amount).toBeGreaterThan(0n); - tokenSim.unshield(accounts[0].address, accounts[0].address, amount); + await expect( + asset.methods.unshield(accounts[0].address, accounts[0].address, amount, 1).simulate(), + ).rejects.toThrow('Assertion failed: invalid nonce'); }); - it('on behalf of other', async () => { - const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balancePriv0 / 2n; + it('on behalf of other (more than balance)', async () => { + const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balancePriv0 + 2n; const nonce = Fr.random(); expect(amount).toBeGreaterThan(0n); @@ -54,83 +100,34 @@ describe('e2e_token_contract', () => { const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); await wallets[1].addAuthWitness(witness); - await action.send().wait(); - tokenSim.unshield(accounts[0].address, accounts[1].address, amount); - - // Perform the transfer again, should fail - const txReplay = asset - .withWallet(wallets[1]) - .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce) - .send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); + await expect(action.simulate()).rejects.toThrow('Assertion failed: Balance too low'); }); - describe('failure cases', () => { - it('on behalf of self (more than balance)', async () => { - const balancePriv = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balancePriv + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.methods.unshield(accounts[0].address, accounts[0].address, amount, 0).simulate(), - ).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('on behalf of self (invalid nonce)', async () => { - const balancePriv = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balancePriv + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.methods.unshield(accounts[0].address, accounts[0].address, amount, 1).simulate(), - ).rejects.toThrow('Assertion failed: invalid nonce'); - }); - - it('on behalf of other (more than balance)', async () => { - const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balancePriv0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce); - - // Both wallets are connected to same node and PXE so we could just insert directly - // But doing it in two actions to show the flow. - const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); - await wallets[1].addAuthWitness(witness); - - await expect(action.simulate()).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('on behalf of other (invalid designated caller)', async () => { - const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).view(); - const amount = balancePriv0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[2]) - .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce); - const expectedMessageHash = computeAuthWitMessageHash( - accounts[2].address, - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - // Both wallets are connected to same node and PXE so we could just insert directly - // But doing it in two actions to show the flow. - const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); - await wallets[2].addAuthWitness(witness); - - await expect(action.simulate()).rejects.toThrow( - `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, - ); - }); + it('on behalf of other (invalid designated caller)', async () => { + const balancePriv0 = await asset.methods.balance_of_private(accounts[0].address).simulate(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[2]) + .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce); + const expectedMessageHash = computeAuthWitMessageHash( + accounts[2].address, + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); + await wallets[2].addAuthWitness(witness); + + await expect(action.simulate()).rejects.toThrow( + `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, + ); }); }); }); diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 36b82000d86..de120354161 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -58,15 +58,17 @@ export class SnapshotManager { const snapshotPath = join(this.dataPath, 'snapshots', ...this.snapshotStack.map(e => e.name), name, 'snapshot'); if (existsSync(snapshotPath)) { - // Snapshot exists. Delay creating subsystems as we're probably still descending the tree. + // Snapshot exists. Record entry on stack but do nothing else as we're probably still descending the tree. + // It's the tests responsibility to call setup() before a test to ensure subsystems get created. this.logger(`Snapshot exists at ${snapshotPath}. Continuing...`); this.snapshotStack.push({ name, apply, restore, snapshotPath }); return; } - // Snapshot doesn't exist, and by definition none of the child snapshots can exist. + // Snapshot didn't exist at snapshotPath, and by definition none of the child snapshots can exist. + if (!this.context) { - // We have no context yet, create from the previous snapshot if it exists. + // We have no subsystem context yet, create it from the top of the snapshot stack (if it exists). this.context = await this.setup(); } @@ -92,8 +94,15 @@ export class SnapshotManager { copySync(this.livePath, snapshotPath); } + /** + * Creates and returns the subsystem context based on the current snapshot stack. + * If the subsystem context already exists, just return it. + * If you want to be sure to get a clean snapshot, be sure to call teardown() before calling setup(). + */ public async setup() { - // We have no context yet, create from the last snapshot if it exists. + // We have no subsystem context yet. + // If one exists on the snapshot stack, create one from that snapshot. + // Otherwise create a fresh one. if (!this.context) { removeSync(this.livePath); mkdirSync(this.livePath, { recursive: true }); @@ -116,11 +125,17 @@ export class SnapshotManager { return this.context; } + /** + * Destroy the current subsystem context and pop off the top of the snapshot stack. + */ public async pop() { this.snapshotStack.pop(); await this.teardown(); } + /** + * Destroys the current subsystem context. + */ public async teardown() { if (!this.context) { return; @@ -133,13 +148,18 @@ export class SnapshotManager { removeSync(this.livePath); } - private async setupFromFresh(livePath?: string): Promise { + /** + * Initializes a fresh set of subsystems. + * If given a statePath, the state will be written to the path. + * If there is no statePath, in-memory and temporary state locations will be used. + */ + private async setupFromFresh(statePath?: string): Promise { this.logger(`Initializing state...`); // Fetch the AztecNode config. // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. const aztecNodeConfig: AztecNodeConfig = getConfigEnvVars(); - aztecNodeConfig.dataDirectory = livePath; + aztecNodeConfig.dataDirectory = statePath; // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. this.logger('Starting anvil...'); @@ -169,10 +189,10 @@ export class SnapshotManager { this.logger('Creating pxe...'); const pxeConfig = getPXEServiceConfig(); - pxeConfig.dataDirectory = livePath; + pxeConfig.dataDirectory = statePath; const pxe = await createPXEService(aztecNode, pxeConfig); - writeFileSync(`${livePath}/aztec_node_config.json`, JSON.stringify(aztecNodeConfig)); + writeFileSync(`${statePath}/aztec_node_config.json`, JSON.stringify(aztecNodeConfig)); return { aztecNodeConfig, @@ -232,6 +252,10 @@ export class SnapshotManager { } } +/** + * Snapshot 'apply' helper function to add accounts. + * The 'restore' function is not provided, as it must be a closure within the test context to capture the results. + */ export const addAccounts = (numberOfAccounts: number) => async ({ pxe }: SubsystemsContext, logger: DebugLogger) => { @@ -248,7 +272,7 @@ export const addAccounts = // the results get stored within the account object. By calling it here we increase the probability of all the // accounts being deployed in the same block because it makes the deploy() method basically instant. await account.getDeployMethod().then(d => - d.simulate({ + d.prove({ contractAddressSalt: account.salt, skipClassRegistration: true, skipPublicDeployment: true, From 2dc65a95cb6193c130f8047e6bcc8521b0d93143 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Thu, 4 Apr 2024 22:03:18 +0000 Subject: [PATCH 07/12] rename --- .../end-to-end/src/e2e_token_contract/token_contract_test.ts | 2 +- .../end-to-end/src/fixtures/{setup.ts => snapshot_manager.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename yarn-project/end-to-end/src/fixtures/{setup.ts => snapshot_manager.ts} (100%) diff --git a/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts index 9402a70b63f..1ff7131f274 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts @@ -12,7 +12,7 @@ import { } from '@aztec/aztec.js'; import { DocsExampleContract, TokenContract } from '@aztec/noir-contracts.js'; -import { SnapshotManager, addAccounts, publicDeployAccounts } from '../fixtures/setup.js'; +import { SnapshotManager, addAccounts, publicDeployAccounts } from '../fixtures/snapshot_manager.js'; import { TokenSimulator } from '../simulators/token_simulator.js'; const { E2E_DATA_PATH: dataPath = './data' } = process.env; diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts similarity index 100% rename from yarn-project/end-to-end/src/fixtures/setup.ts rename to yarn-project/end-to-end/src/fixtures/snapshot_manager.ts From 31d4490cb144ff622b60f6d764a57349bc0c9ec7 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Fri, 5 Apr 2024 11:06:30 +0000 Subject: [PATCH 08/12] atomic snapshots --- yarn-project/end-to-end/package.json | 2 +- .../end-to-end/src/fixtures/snapshot_manager.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index 0aa48dd09a2..b5df23580ee 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -15,7 +15,7 @@ "clean": "rm -rf ./dest .tsbuildinfo", "formatting": "run -T prettier --check ./src \"!src/web/main.js\" && run -T eslint ./src", "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src", - "test": "LOG_LEVEL=${LOG_LEVEL:-verbose} NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --testTimeout=60000 --forceExit", + "test": "LOG_LEVEL=${LOG_LEVEL:-verbose} NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --testTimeout=120000 --forceExit", "test:integration": "concurrently -k -s first -c reset,dim -n test,anvil \"yarn test:integration:run\" \"anvil\"", "test:integration:run": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --no-cache --runInBand --config jest.integration.config.json" }, diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index de120354161..88d78d4a9cd 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -89,9 +89,15 @@ export class SnapshotManager { writeFileSync(`${this.livePath}/${name}.json`, JSON.stringify(snapshotData || {}, resolver)); // Copy everything to snapshot path. + // We want it to be atomic, in case multiple processes are racing to create the snapshot. this.logger(`Saving snapshot to ${snapshotPath}...`); - mkdirSync(snapshotPath, { recursive: true }); - copySync(this.livePath, snapshotPath); + if (mkdirSync(snapshotPath, { recursive: true })) { + copySync(this.livePath, snapshotPath); + this.logger(`Snapshot copied to ${snapshotPath}.`); + } else { + this.logger(`Snapshot already exists at ${snapshotPath}. Discarding our version.`); + await this.teardown(); + } } /** From 6f531d560ac56ba519ef81bd239fbd149d1ebfab Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Fri, 5 Apr 2024 11:52:59 +0000 Subject: [PATCH 09/12] better logging --- .../e2e_token_contract/token_contract_test.ts | 44 +++++++++++++------ .../src/fixtures/snapshot_manager.ts | 23 +++++----- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts index 1ff7131f274..72896148d07 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts @@ -31,7 +31,7 @@ export class TokenContractTest { constructor(testName: string) { this.logger = createDebugLogger(`aztec:e2e_token_contract:${testName}`); - this.snapshotManager = new SnapshotManager(`e2e_token_contract/${testName}`, dataPath, this.logger); + this.snapshotManager = new SnapshotManager(`e2e_token_contract/${testName}`, dataPath); } /** @@ -40,12 +40,11 @@ export class TokenContractTest { * 2. Publicly deploy accounts, deploy token contract and a "bad account". */ async pushBaseSnapshots() { - await this.snapshotManager.snapshot('3_accounts', addAccounts(3), async ({ accountKeys }, { pxe }) => { + await this.snapshotManager.snapshot('3_accounts', addAccounts(3, 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.accounts = await pxe.getRegisteredAccounts(); - this.logger(`Restored ${this.accounts.length} accounts.`); - this.logger(`Wallet 0 address: ${this.wallets[0].getAddress()}`); + this.wallets.forEach((w, i) => this.logger(`Wallet ${i} address: ${w.getAddress()}`)); }); await this.snapshotManager.snapshot( @@ -68,7 +67,7 @@ export class TokenContractTest { .deployed(); this.logger(`Token deployed to ${asset.address}`); - this.logger(`Deploying "bad account"...`); + this.logger(`Deploying bad account...`); this.badAccount = await DocsExampleContract.deploy(this.wallets[0]).send().deployed(); this.logger(`Deployed to ${this.badAccount.address}.`); @@ -77,7 +76,7 @@ export class TokenContractTest { async ({ tokenContractAddress, badAccountAddress }) => { // Restore the token contract state. this.asset = await TokenContract.at(tokenContractAddress, this.wallets[0]); - this.logger(`Token contract restored to ${this.asset.address}.`); + this.logger(`Token contract address: ${this.asset.address}`); this.tokenSim = new TokenSimulator( this.asset, @@ -86,7 +85,7 @@ export class TokenContractTest { ); this.badAccount = await DocsExampleContract.at(badAccountAddress, this.wallets[0]); - this.logger(`Bad account restored to ${this.badAccount.address}.`); + this.logger(`Bad account address: ${this.badAccount.address}`); expect(await this.asset.methods.admin().simulate()).toBe(this.accounts[0].address.toBigInt()); }, @@ -125,8 +124,11 @@ export class TokenContractTest { async () => { const { asset, accounts } = this; const amount = 10000n; + + this.logger(`Minting ${amount} publicly...`); await asset.methods.mint_public(accounts[0].address, amount).send().wait(); + this.logger(`Minting ${amount} privately...`); const secret = Fr.random(); const secretHash = computeMessageSecretHash(secret); const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); @@ -134,18 +136,32 @@ export class TokenContractTest { await this.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); const txClaim = asset.methods.redeem_shield(accounts[0].address, amount, secret).send(); await txClaim.wait({ debug: true }); + this.logger(`Minting complete.`); return { amount }; }, async ({ amount }) => { - const { asset, accounts, tokenSim } = this; - tokenSim.mintPublic(accounts[0].address, amount); - expect(await asset.methods.balance_of_public(accounts[0].address).simulate()).toEqual( - tokenSim.balanceOfPublic(accounts[0].address), - ); + const { + asset, + accounts: [{ address }], + tokenSim, + } = this; + tokenSim.mintPublic(address, amount); + + const publicBalance = await asset.methods.balance_of_public(address).simulate(); + this.logger(`Public balance of wallet 0: ${publicBalance}`); + expect(publicBalance).toEqual(this.tokenSim.balanceOfPublic(address)); + tokenSim.mintPrivate(amount); - tokenSim.redeemShield(accounts[0].address, amount); - expect(await asset.methods.total_supply().simulate()).toEqual(tokenSim.totalSupply); + tokenSim.redeemShield(address, amount); + const privateBalance = await asset.methods.balance_of_private(address).simulate(); + this.logger(`Private balance of wallet 0: ${privateBalance}`); + expect(privateBalance).toEqual(tokenSim.balanceOfPrivate(address)); + + const totalSupply = await asset.methods.total_supply().simulate(); + this.logger(`Total supply: ${totalSupply}`); + expect(totalSupply).toEqual(tokenSim.totalSupply); + return Promise.resolve(); }, ); diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index 88d78d4a9cd..f08725fa497 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -12,6 +12,7 @@ import { import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; import { asyncMap } from '@aztec/foundation/async-map'; import { resolver, reviver } from '@aztec/foundation/serialize'; +import { createDebugLogger } from '@aztec/foundation/log'; import { type PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; import { type Anvil, createAnvil } from '@viem/anvil'; @@ -35,8 +36,8 @@ type SubsystemsContext = { type SnapshotEntry = { name: string; - apply: (context: SubsystemsContext, logger: DebugLogger) => Promise; - restore: (snapshotData: any, context: SubsystemsContext, logger: DebugLogger) => Promise; + apply: (context: SubsystemsContext) => Promise; + restore: (snapshotData: any, context: SubsystemsContext) => Promise; snapshotPath: string; }; @@ -44,15 +45,17 @@ export class SnapshotManager { private snapshotStack: SnapshotEntry[] = []; private context?: SubsystemsContext; private livePath: string; + private logger: DebugLogger; - constructor(testName: string, private dataPath: string, private logger: DebugLogger) { + constructor(testName: string, private dataPath: string) { this.livePath = join(this.dataPath, 'live', testName); + this.logger = createDebugLogger(`aztec:snapshot_manager:${testName}`); } public async snapshot( name: string, - apply: (context: SubsystemsContext, logger: DebugLogger) => Promise, - restore: (snapshotData: T, context: SubsystemsContext, logger: DebugLogger) => Promise = () => + apply: (context: SubsystemsContext) => Promise, + restore: (snapshotData: T, context: SubsystemsContext) => Promise = () => Promise.resolve(), ) { const snapshotPath = join(this.dataPath, 'snapshots', ...this.snapshotStack.map(e => e.name), name, 'snapshot'); @@ -76,11 +79,11 @@ export class SnapshotManager { // Apply current state transition. this.logger(`Applying state transition for ${name}...`); - const snapshotData = await apply(this.context, this.logger); + const snapshotData = await apply(this.context); this.logger(`State transition for ${name} complete.`); // Execute the restoration function. - await restore(snapshotData, this.context, this.logger); + await restore(snapshotData, this.context); // Save the snapshot data. const ethCheatCodes = new EthCheatCodes(this.context.aztecNodeConfig.rpcUrl); @@ -121,7 +124,7 @@ export class SnapshotManager { await asyncMap(this.snapshotStack, async e => { const snapshotData = JSON.parse(readFileSync(`${e.snapshotPath}/${e.name}.json`, 'utf-8'), reviver); this.logger(`Executing restoration function for ${e.name}...`); - await e.restore(snapshotData, this.context!, this.logger); + await e.restore(snapshotData, this.context!); this.logger(`Restoration of ${e.name} complete.`); }); } else { @@ -263,8 +266,8 @@ export class SnapshotManager { * The 'restore' function is not provided, as it must be a closure within the test context to capture the results. */ export const addAccounts = - (numberOfAccounts: number) => - async ({ pxe }: SubsystemsContext, logger: DebugLogger) => { + (numberOfAccounts: number, logger: DebugLogger) => + async ({ pxe }: SubsystemsContext) => { // Generate account keys. const accountKeys: [GrumpkinPrivateKey, GrumpkinPrivateKey][] = Array.from({ length: numberOfAccounts }).map(_ => [ GrumpkinPrivateKey.random(), From a5a9260bcf1a7abd0a65aabd5ba831c74464c3f5 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Tue, 9 Apr 2024 17:32:02 +0000 Subject: [PATCH 10/12] remove concept of pop. complex, inhibits pure mem mode. just use separate test files. now requires explicit setting of E2E_DATA_PATH. --- .../e2e_token_contract/access_control.test.ts | 9 ++--- .../src/e2e_token_contract/burn.test.ts | 16 +++----- .../src/e2e_token_contract/minting.test.ts | 11 ++---- .../reading_constants.test.ts | 13 +++---- .../src/e2e_token_contract/sheilding.test.ts | 16 +++----- .../e2e_token_contract/token_contract_test.ts | 37 ++++++++++++------- .../transfer_private.test.ts | 14 +++---- .../transfer_public.test.ts | 16 +++----- .../e2e_token_contract/unsheilding.test.ts | 16 +++----- .../src/fixtures/snapshot_manager.ts | 35 ++++++++++-------- 10 files changed, 86 insertions(+), 97 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts index 1cccd8813a7..8efc7d88277 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts @@ -4,15 +4,12 @@ describe('e2e_token_contract access control', () => { const t = new TokenContractTest('access_control'); beforeAll(async () => { - await t.pushBaseSnapshots(); + await t.applyBaseSnapshots(); + await t.setup(); }); afterAll(async () => { - await t.popBaseSnapshots(); - }); - - beforeEach(async () => { - await t.snapshotManager.setup(); + await t.teardown(); }); afterEach(async () => { diff --git a/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts index c40740ee763..ff7aed370b5 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts @@ -8,19 +8,15 @@ describe('e2e_token_contract burn', () => { let { asset, accounts, tokenSim, wallets } = t; beforeAll(async () => { - await t.pushBaseSnapshots(); - await t.pushMintSnapshot(); + await t.applyBaseSnapshots(); + await t.applyMintSnapshot(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, accounts, tokenSim, wallets } = t); }); afterAll(async () => { - await t.snapshotManager.pop(); // mint - await t.popBaseSnapshots(); - }); - - beforeEach(async () => { - await t.snapshotManager.setup(); - // Have to destructure again to ensure we have latest refs. - ({ asset, accounts, tokenSim, wallets } = t); + await t.teardown(); }); afterEach(async () => { diff --git a/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts index 9bff43deee2..2fa48998dcb 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts @@ -8,16 +8,13 @@ describe('e2e_token_contract minting', () => { let { asset, accounts, tokenSim, wallets } = t; beforeAll(async () => { - await t.pushBaseSnapshots(); + await t.applyBaseSnapshots(); + await t.setup(); + ({ asset, accounts, tokenSim, wallets } = t); }); afterAll(async () => { - await t.popBaseSnapshots(); - }); - - beforeEach(async () => { - await t.snapshotManager.setup(); - ({ asset, accounts, tokenSim, wallets } = t); + await t.teardown(); }); afterEach(async () => { diff --git a/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts index 9bb2d1084f3..43762d88e26 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts @@ -20,9 +20,9 @@ describe('e2e_token_contract reading constants', () => { let reader: ReaderContract; beforeAll(async () => { - await t.pushBaseSnapshots(); + await t.applyBaseSnapshots(); - await t.snapshotManager.snapshot( + await t.snapshot( 'reading_constants', async () => { logger('Deploying ReaderContract...'); @@ -35,16 +35,15 @@ describe('e2e_token_contract reading constants', () => { logger(`Reader contract restored to ${readerAddress}.`); }, ); + + await t.setup(); }); afterAll(async () => { - await t.snapshotManager.pop(); // reading_constants - await t.popBaseSnapshots(); + await t.teardown(); }); - beforeEach(async () => { - await t.snapshotManager.setup(); - }); + beforeEach(async () => {}); afterEach(async () => { await t.tokenSim.check(); 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/sheilding.test.ts index 14da530347a..99fcd3c1336 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/sheilding.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/sheilding.test.ts @@ -10,20 +10,16 @@ describe('e2e_token_contract shield + redeem shield', () => { let secretHash: Fr; beforeAll(async () => { - await t.pushBaseSnapshots(); - await t.pushMintSnapshot(); + await t.applyBaseSnapshots(); + await t.applyMintSnapshot(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, accounts, tokenSim, wallets } = t); secretHash = computeMessageSecretHash(secret); }); afterAll(async () => { - await t.snapshotManager.pop(); // mint - await t.popBaseSnapshots(); - }); - - beforeEach(async () => { - await t.snapshotManager.setup(); - // Have to destructure again to ensure we have latest refs. - ({ asset, accounts, tokenSim, wallets } = t); + await t.teardown(); }); afterEach(async () => { diff --git a/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts index 72896148d07..f32e3b4536f 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts @@ -12,17 +12,22 @@ import { } from '@aztec/aztec.js'; import { DocsExampleContract, TokenContract } from '@aztec/noir-contracts.js'; -import { SnapshotManager, addAccounts, publicDeployAccounts } from '../fixtures/snapshot_manager.js'; +import { + SnapshotManager, + type SubsystemsContext, + addAccounts, + publicDeployAccounts, +} from '../fixtures/snapshot_manager.js'; import { TokenSimulator } from '../simulators/token_simulator.js'; -const { E2E_DATA_PATH: dataPath = './data' } = process.env; +const { E2E_DATA_PATH: dataPath } = process.env; export class TokenContractTest { static TOKEN_NAME = 'Aztec Token'; static TOKEN_SYMBOL = 'AZT'; static TOKEN_DECIMALS = 18n; + private snapshotManager: SnapshotManager; logger: DebugLogger; - snapshotManager: SnapshotManager; wallets: AccountWallet[] = []; accounts: CompleteAddress[] = []; asset!: TokenContract; @@ -39,7 +44,7 @@ export class TokenContractTest { * 1. Add 3 accounts. * 2. Publicly deploy accounts, deploy token contract and a "bad account". */ - async pushBaseSnapshots() { + async applyBaseSnapshots() { await this.snapshotManager.snapshot('3_accounts', addAccounts(3, 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())); @@ -97,28 +102,34 @@ export class TokenContractTest { // }); } - async popBaseSnapshots() { - await this.snapshotManager.pop(); // e2e_token_contract - await this.snapshotManager.pop(); // 3_accounts + async setup() { + await this.snapshotManager.setup(); } - async addPendingShieldNoteToPXE(accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) { - const storageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote + snapshot = ( + name: string, + apply: (context: SubsystemsContext) => Promise, + restore: (snapshotData: T, context: SubsystemsContext) => Promise = () => Promise.resolve(), + ): Promise => this.snapshotManager.snapshot(name, apply, restore); + async teardown() { + await this.snapshotManager.teardown(); + } + + async addPendingShieldNoteToPXE(accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) { const note = new Note([new Fr(amount), secretHash]); const extendedNote = new ExtendedNote( note, this.accounts[accountIndex].address, this.asset.address, - storageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, txHash, ); await this.wallets[accountIndex].addNote(extendedNote); } - async pushMintSnapshot() { + async applyMintSnapshot() { await this.snapshotManager.snapshot( 'mint', async () => { diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts index 4df010374a9..3251c7422a9 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts @@ -7,18 +7,14 @@ describe('e2e_token_contract transfer private', () => { let { asset, accounts, tokenSim, wallets, badAccount } = t; beforeAll(async () => { - await t.pushBaseSnapshots(); - await t.pushMintSnapshot(); + await t.applyBaseSnapshots(); + await t.applyMintSnapshot(); + await t.setup(); + ({ asset, accounts, tokenSim, wallets, badAccount } = t); }); afterAll(async () => { - await t.snapshotManager.pop(); // mint - await t.popBaseSnapshots(); - }); - - beforeEach(async () => { - await t.snapshotManager.setup(); - ({ asset, accounts, tokenSim, wallets, badAccount } = t); + await t.teardown(); }); afterEach(async () => { diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts index f29b1c91afd..13430c1916a 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts @@ -8,19 +8,15 @@ describe('e2e_token_contract transfer public', () => { let { asset, accounts, tokenSim, wallets, badAccount } = t; beforeAll(async () => { - await t.pushBaseSnapshots(); - await t.pushMintSnapshot(); + await t.applyBaseSnapshots(); + await t.applyMintSnapshot(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, accounts, tokenSim, wallets, badAccount } = t); }); afterAll(async () => { - await t.snapshotManager.pop(); // mint - await t.popBaseSnapshots(); - }); - - beforeEach(async () => { - await t.snapshotManager.setup(); - // Have to destructure again to ensure we have latest refs. - ({ asset, accounts, tokenSim, wallets, badAccount } = t); + await t.teardown(); }); afterEach(async () => { 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/unsheilding.test.ts index 38d7a627deb..998b978e081 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/unsheilding.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/unsheilding.test.ts @@ -7,19 +7,15 @@ describe('e2e_token_contract unshielding', () => { let { asset, accounts, tokenSim, wallets } = t; beforeAll(async () => { - await t.pushBaseSnapshots(); - await t.pushMintSnapshot(); + await t.applyBaseSnapshots(); + await t.applyMintSnapshot(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, accounts, tokenSim, wallets } = t); }); afterAll(async () => { - await t.snapshotManager.pop(); // mint - await t.popBaseSnapshots(); - }); - - beforeEach(async () => { - await t.snapshotManager.setup(); - // Have to destructure again to ensure we have latest refs. - ({ asset, accounts, tokenSim, wallets } = t); + await t.teardown(); }); afterEach(async () => { diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index f08725fa497..d28e0396667 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -11,8 +11,8 @@ import { } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; import { asyncMap } from '@aztec/foundation/async-map'; -import { resolver, reviver } from '@aztec/foundation/serialize'; import { createDebugLogger } from '@aztec/foundation/log'; +import { resolver, reviver } from '@aztec/foundation/serialize'; import { type PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; import { type Anvil, createAnvil } from '@viem/anvil'; @@ -26,7 +26,7 @@ import { MNEMONIC } from './fixtures.js'; import { getACVMConfig } from './get_acvm_config.js'; import { setupL1Contracts } from './setup_l1_contracts.js'; -type SubsystemsContext = { +export type SubsystemsContext = { anvil: Anvil; acvmConfig: any; aztecNode: AztecNodeService; @@ -47,17 +47,28 @@ export class SnapshotManager { private livePath: string; private logger: DebugLogger; - constructor(testName: string, private dataPath: string) { - this.livePath = join(this.dataPath, 'live', testName); + constructor(testName: string, private dataPath?: string) { + this.livePath = this.dataPath ? join(this.dataPath, 'live', testName) : ''; this.logger = createDebugLogger(`aztec:snapshot_manager:${testName}`); } public async snapshot( name: string, apply: (context: SubsystemsContext) => Promise, - restore: (snapshotData: T, context: SubsystemsContext) => Promise = () => - Promise.resolve(), + restore: (snapshotData: T, context: SubsystemsContext) => Promise = () => Promise.resolve(), ) { + if (!this.dataPath) { + // We are running in disabled mode. Just apply the state. + this.logger(`No data path given, will not persist any snapshots.`); + this.context = await this.setupFromFresh(); + this.logger(`Applying state transition for ${name}...`); + const snapshotData = await apply(this.context); + this.logger(`State transition for ${name} complete.`); + // Execute the restoration function. + await restore(snapshotData, this.context); + return; + } + const snapshotPath = join(this.dataPath, 'snapshots', ...this.snapshotStack.map(e => e.name), name, 'snapshot'); if (existsSync(snapshotPath)) { @@ -134,14 +145,6 @@ export class SnapshotManager { return this.context; } - /** - * Destroy the current subsystem context and pop off the top of the snapshot stack. - */ - public async pop() { - this.snapshotStack.pop(); - await this.teardown(); - } - /** * Destroys the current subsystem context. */ @@ -201,7 +204,9 @@ export class SnapshotManager { pxeConfig.dataDirectory = statePath; const pxe = await createPXEService(aztecNode, pxeConfig); - writeFileSync(`${statePath}/aztec_node_config.json`, JSON.stringify(aztecNodeConfig)); + if (statePath) { + writeFileSync(`${statePath}/aztec_node_config.json`, JSON.stringify(aztecNodeConfig)); + } return { aztecNodeConfig, From 9a804dd321320ac0bfa5e27ba05251fc944bad20 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Mon, 8 Apr 2024 09:11:26 +0000 Subject: [PATCH 11/12] We can run 35 of our e2e tests just using jest. --- yarn-project/end-to-end/package.json | 2 +- ..._js_browser.test.ts => _e2e_aztec_js_browser.test.ts} | 0 ...{e2e_persistence.test.ts => _e2e_persistence.test.ts} | 0 ...dbox_example.test.ts => _e2e_sandbox_example.test.ts} | 0 .../end-to-end/src/e2e_dapp_subscription.test.ts | 6 +----- yarn-project/end-to-end/src/e2e_fees.test.ts | 2 +- yarn-project/end-to-end/src/fixtures/utils.ts | 9 +++++---- 7 files changed, 8 insertions(+), 11 deletions(-) rename yarn-project/end-to-end/src/{e2e_aztec_js_browser.test.ts => _e2e_aztec_js_browser.test.ts} (100%) rename yarn-project/end-to-end/src/{e2e_persistence.test.ts => _e2e_persistence.test.ts} (100%) rename yarn-project/end-to-end/src/{e2e_sandbox_example.test.ts => _e2e_sandbox_example.test.ts} (100%) diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index b5df23580ee..8087a1be253 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -102,7 +102,7 @@ "node": ">=18" }, "jest": { - "slowTestThreshold": 180, + "slowTestThreshold": 300, "extensionsToTreatAsEsm": [ ".ts" ], diff --git a/yarn-project/end-to-end/src/e2e_aztec_js_browser.test.ts b/yarn-project/end-to-end/src/_e2e_aztec_js_browser.test.ts similarity index 100% rename from yarn-project/end-to-end/src/e2e_aztec_js_browser.test.ts rename to yarn-project/end-to-end/src/_e2e_aztec_js_browser.test.ts diff --git a/yarn-project/end-to-end/src/e2e_persistence.test.ts b/yarn-project/end-to-end/src/_e2e_persistence.test.ts similarity index 100% rename from yarn-project/end-to-end/src/e2e_persistence.test.ts rename to yarn-project/end-to-end/src/_e2e_persistence.test.ts diff --git a/yarn-project/end-to-end/src/e2e_sandbox_example.test.ts b/yarn-project/end-to-end/src/_e2e_sandbox_example.test.ts similarity index 100% rename from yarn-project/end-to-end/src/e2e_sandbox_example.test.ts rename to yarn-project/end-to-end/src/_e2e_sandbox_example.test.ts diff --git a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts index 90348fa8d28..a00584b76ad 100644 --- a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts +++ b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts @@ -22,12 +22,8 @@ import { } from '@aztec/noir-contracts.js'; import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; -import { jest } from '@jest/globals'; - import { type BalancesFn, expectMapping, getBalancesFn, publicDeployAccounts, setup } from './fixtures/utils.js'; -jest.setTimeout(100_000); - const TOKEN_NAME = 'BananaCoin'; const TOKEN_SYMBOL = 'BC'; const TOKEN_DECIMALS = 18n; @@ -66,7 +62,7 @@ describe('e2e_dapp_subscription', () => { let wallets: AccountWalletWithPrivateKey[]; let aztecNode: AztecNode; let deployL1ContractsValues: DeployL1Contracts; - ({ wallets, aztecNode, deployL1ContractsValues, logger, pxe } = await setup(3)); + ({ wallets, aztecNode, deployL1ContractsValues, logger, pxe } = await setup(3, {}, {}, true)); await publicDeployAccounts(wallets[0], wallets); diff --git a/yarn-project/end-to-end/src/e2e_fees.test.ts b/yarn-project/end-to-end/src/e2e_fees.test.ts index 7aed8e282eb..ec85175bd1a 100644 --- a/yarn-project/end-to-end/src/e2e_fees.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees.test.ts @@ -54,7 +54,7 @@ describe('e2e_fees', () => { let bananaPrivateBalances: BalancesFn; beforeAll(async () => { - const { wallets: _wallets, aztecNode, deployL1ContractsValues, logger, pxe } = await setup(3); + const { wallets: _wallets, aztecNode, deployL1ContractsValues, logger, pxe } = await setup(3, {}, {}, true); wallets = _wallets; await aztecNode.setConfig({ diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index b4f492919a2..1faa9ac4951 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -78,7 +78,6 @@ const { TEMP_DIR = '/tmp', ACVM_BINARY_PATH = '', ACVM_WORKING_DIRECTORY = '', - ENABLE_GAS = '', } = process.env; const getAztecUrl = () => { @@ -222,6 +221,7 @@ async function setupWithRemoteEnvironment( config: AztecNodeConfig, logger: DebugLogger, numberOfAccounts: number, + enableGas: boolean, ) { // we are setting up against a remote environment, l1 contracts are already deployed const aztecNodeUrl = getAztecUrl(); @@ -259,7 +259,7 @@ async function setupWithRemoteEnvironment( const cheatCodes = CheatCodes.create(config.rpcUrl, pxeClient!); const teardown = () => Promise.resolve(); - if (['1', 'true'].includes(ENABLE_GAS)) { + if (enableGas) { // this contract might already have been deployed // the following function is idempotent await deployCanonicalGasToken(new SignerlessWallet(pxeClient, new DefaultMultiCallEntrypoint())); @@ -322,6 +322,7 @@ export async function setup( numberOfAccounts = 1, opts: SetupOptions = {}, pxeOpts: Partial = {}, + enableGas = false, ): Promise { const config = { ...getConfigEnvVars(), ...opts }; @@ -354,7 +355,7 @@ export async function setup( if (PXE_URL) { // we are setting up against a remote environment, l1 contracts are assumed to already be deployed - return await setupWithRemoteEnvironment(hdAccount, config, logger, numberOfAccounts); + return await setupWithRemoteEnvironment(hdAccount, config, logger, numberOfAccounts, enableGas); } const deployL1ContractsValues = @@ -377,7 +378,7 @@ export async function setup( logger('Creating a pxe...'); const { pxe, wallets } = await setupPXEService(numberOfAccounts, aztecNode!, pxeOpts, logger); - if (['1', 'true'].includes(ENABLE_GAS)) { + if (enableGas) { logger(`Deploying canonical gas token...`); await deployCanonicalGasToken(new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint())); logger(`Done.`); From 49e5257c5c07298f696ab135f429ef2b1dd8680a Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Sun, 31 Mar 2024 10:18:11 +0000 Subject: [PATCH 12/12] feat: Tell user they need to pip install clang. --- barretenberg/scripts/bindgen.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/barretenberg/scripts/bindgen.sh b/barretenberg/scripts/bindgen.sh index e3d5b9b5a70..b1c43e319b3 100755 --- a/barretenberg/scripts/bindgen.sh +++ b/barretenberg/scripts/bindgen.sh @@ -1,6 +1,15 @@ #!/usr/bin/env bash set -eu +if ! pip list | grep -E 'clang\s+16.0.6' > /dev/null; then + echo "You need to install python clang: pip install clang==16.0.6" + exit 1 +fi + #find ./cpp/src -type f -name "c_bind*.hpp" | ./scripts/decls_json.py > exports.json cat ./scripts/c_bind_files.txt | ./scripts/decls_json.py > exports.json -(cd ./ts && yarn node --loader ts-node/esm ./src/bindgen/index.ts ../exports.json > ./src/barretenberg_api/index.ts) \ No newline at end of file +( + cd ./ts && \ + yarn node --loader ts-node/esm ./src/bindgen/index.ts ../exports.json > ./src/barretenberg_api/index.ts && \ + yarn prettier -w ./src/barretenberg_api/index.ts +) \ No newline at end of file