From a770b80607ce5f83010525f412058700d0690ceb Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Mon, 1 Apr 2024 17:10:42 +0000 Subject: [PATCH 01/20] 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 ++ .../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 | 1 + .../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 +++ 13 files changed, 459 insertions(+), 2 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 e7cc7c0ba205..d25d591a047d 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 43ecbefb9e61..f507bdcee44f 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 cb96ce1cd8c1..e147ea53d146 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 000000000000..6e667a6f88f0 --- /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/src/fixtures/get_acvm_config.ts b/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts new file mode 100644 index 000000000000..f2ee106043a5 --- /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 000000000000..66f29944150a --- /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 000000000000..1a3d3b351283 --- /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 c4938e4376e3..ade1be7c9568 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -377,6 +377,7 @@ 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)) { diff --git a/yarn-project/foundation/src/aztec-address/index.ts b/yarn-project/foundation/src/aztec-address/index.ts index c932c10cdaa4..e97fb1f17946 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 f61c1f75b4fd..5f28e59f54a1 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 a28018638f42..9b2a67b55b0c 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 875d37f44109..33cb9a5bb4d8 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 000000000000..bc48c60f0fda --- /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 f85aef2f515d6672001b65de59f3a9a1ca056ccc Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Mon, 1 Apr 2024 19:29:16 +0000 Subject: [PATCH 02/20] 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 e147ea53d146..fa2e53e3b9d0 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 66f29944150a..d04f3495bbec 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 000000000000..ac301433fbc9 --- /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 5bae45e38f82d258418b03444aaee61e835503f5 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Mon, 1 Apr 2024 23:37:03 +0000 Subject: [PATCH 03/20] 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 a8c96b6887ff..43fb28a5b23d 100644 --- a/yarn-project/aztec.js/src/account_manager/index.ts +++ b/yarn-project/aztec.js/src/account_manager/index.ts @@ -40,7 +40,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 6e667a6f88f0..000000000000 --- 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 f2ee106043a5..0565f3f0d5c5 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 d04f3495bbec..254862464a81 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 ac301433fbc9..f03f874de651 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 5529c3fed8ea6d12771ab0144e7e0d66e7587602 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Tue, 2 Apr 2024 23:35:10 +0000 Subject: [PATCH 04/20] 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 26726a633dda..f80c480f3773 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 0565f3f0d5c5..e89585341ec3 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 254862464a81..4d60026d2126 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 f03f874de651..000000000000 --- 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 9b2a67b55b0c..bc180fa9ecb6 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 722f749f9e61..b251c25c6b7c 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 2894dc9d08c831c3e9bf9be606470ab6d9b3545d Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Wed, 3 Apr 2024 15:35:04 +0000 Subject: [PATCH 05/20] 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 f80c480f3773..20264ff1960d 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 000000000000..8563a2c19009 --- /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 000000000000..96fdf7d99446 --- /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 000000000000..23fa7f1916e0 --- /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 000000000000..73603a099c59 --- /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 000000000000..0cd200708942 --- /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 000000000000..76b8958847bc --- /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 000000000000..822eb98b7e5b --- /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 000000000000..a1e7ca6a6a25 --- /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 000000000000..433150340c8d --- /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 4d60026d2126..36b82000d86f 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 bc48c60f0fda..85146710ed8c 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 621d8ba819a7c5b2ea59e347352de6d177ba284b Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Thu, 4 Apr 2024 22:01:24 +0000 Subject: [PATCH 06/20] 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 20264ff1960d..0aa48dd09a21 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 8563a2c19009..1cccd8813a7c 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 96fdf7d99446..c40740ee763c 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 23fa7f1916e0..9bff43deee2c 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 73603a099c59..9bb2d1084f3d 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 0cd200708942..14da530347a0 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 76b8958847bc..9402a70b63f1 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 822eb98b7e5b..4df010374a90 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 a1e7ca6a6a25..f29b1c91afd9 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 433150340c8d..38d7a627deb6 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 36b82000d86f..de1203541611 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 56e496763069ef30e6b1dc627a6873d75db350e3 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Thu, 4 Apr 2024 22:03:18 +0000 Subject: [PATCH 07/20] 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 9402a70b63f1..1ff7131f2745 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 2641870cc58f68135e0bd7212cf09868b6298267 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Fri, 5 Apr 2024 11:06:30 +0000 Subject: [PATCH 08/20] 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 0aa48dd09a21..b5df23580ee7 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 de1203541611..88d78d4a9cdb 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 223699ef4d62b717368339c963566b8761d47fb0 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Fri, 5 Apr 2024 11:52:59 +0000 Subject: [PATCH 09/20] 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 1ff7131f2745..72896148d07a 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 88d78d4a9cdb..f08725fa4971 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 96093066d68625e770ab2939d59ca1a65824bffe Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Tue, 9 Apr 2024 17:32:02 +0000 Subject: [PATCH 10/20] 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 1cccd8813a7c..8efc7d88277e 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 c40740ee763c..ff7aed370b55 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 9bff43deee2c..2fa48998dcb3 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 9bb2d1084f3d..43762d88e263 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 14da530347a0..99fcd3c1336d 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 72896148d07a..f32e3b4536fb 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 4df010374a90..3251c7422a97 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 f29b1c91afd9..13430c1916aa 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 38d7a627deb6..998b978e0810 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 f08725fa4971..d28e03966675 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 e4f18396035e1b2f1337d779adc7a53f94d91d0f Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Tue, 9 Apr 2024 21:43:29 +0000 Subject: [PATCH 11/20] fix --- .../reading_constants.test.ts | 6 +-- .../e2e_token_contract/token_contract_test.ts | 30 ++++++------- .../src/fixtures/get_acvm_config.ts | 4 +- .../src/fixtures/snapshot_manager.ts | 44 +++++++++---------- yarn-project/end-to-end/src/fixtures/utils.ts | 2 +- 5 files changed, 43 insertions(+), 43 deletions(-) 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 43762d88e263..5327ca15188f 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 @@ -25,14 +25,14 @@ describe('e2e_token_contract reading constants', () => { await t.snapshot( 'reading_constants', async () => { - logger('Deploying ReaderContract...'); + logger.verbose('Deploying ReaderContract...'); const reader = await ReaderContract.deploy(t.wallets[0]).send().deployed(); - logger(`Deployed ReaderContract to ${reader.address}.`); + logger.verbose(`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}.`); + logger.verbose(`Reader contract restored to ${readerAddress}.`); }, ); 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 f32e3b4536fb..9e6fb3f110c6 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 @@ -49,7 +49,7 @@ export class TokenContractTest { 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.wallets.forEach((w, i) => this.logger(`Wallet ${i} address: ${w.getAddress()}`)); + this.wallets.forEach((w, i) => this.logger.verbose(`Wallet ${i} address: ${w.getAddress()}`)); }); await this.snapshotManager.snapshot( @@ -57,10 +57,10 @@ export class TokenContractTest { async () => { // Create the token contract state. // Move this account thing to addAccounts above? - this.logger(`Public deploy accounts...`); + this.logger.verbose(`Public deploy accounts...`); await publicDeployAccounts(this.wallets[0], this.accounts.slice(0, 2)); - this.logger(`Deploying TokenContract...`); + this.logger.verbose(`Deploying TokenContract...`); const asset = await TokenContract.deploy( this.wallets[0], this.accounts[0], @@ -70,18 +70,18 @@ export class TokenContractTest { ) .send() .deployed(); - this.logger(`Token deployed to ${asset.address}`); + this.logger.verbose(`Token deployed to ${asset.address}`); - this.logger(`Deploying bad account...`); + this.logger.verbose(`Deploying bad account...`); this.badAccount = await DocsExampleContract.deploy(this.wallets[0]).send().deployed(); - this.logger(`Deployed to ${this.badAccount.address}.`); + this.logger.verbose(`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 address: ${this.asset.address}`); + this.logger.verbose(`Token contract address: ${this.asset.address}`); this.tokenSim = new TokenSimulator( this.asset, @@ -90,7 +90,7 @@ export class TokenContractTest { ); this.badAccount = await DocsExampleContract.at(badAccountAddress, this.wallets[0]); - this.logger(`Bad account address: ${this.badAccount.address}`); + this.logger.verbose(`Bad account address: ${this.badAccount.address}`); expect(await this.asset.methods.admin().simulate()).toBe(this.accounts[0].address.toBigInt()); }, @@ -98,7 +98,7 @@ export class TokenContractTest { // TokenContract.artifact.functions.forEach(fn => { // const sig = decodeFunctionSignature(fn.name, fn.parameters); - // logger(`Function ${sig} and the selector: ${FunctionSelector.fromNameAndParameters(fn.name, fn.parameters)}`); + // logger.verbose(`Function ${sig} and the selector: ${FunctionSelector.fromNameAndParameters(fn.name, fn.parameters)}`); // }); } @@ -136,10 +136,10 @@ export class TokenContractTest { const { asset, accounts } = this; const amount = 10000n; - this.logger(`Minting ${amount} publicly...`); + this.logger.verbose(`Minting ${amount} publicly...`); await asset.methods.mint_public(accounts[0].address, amount).send().wait(); - this.logger(`Minting ${amount} privately...`); + this.logger.verbose(`Minting ${amount} privately...`); const secret = Fr.random(); const secretHash = computeMessageSecretHash(secret); const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); @@ -147,7 +147,7 @@ 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.`); + this.logger.verbose(`Minting complete.`); return { amount }; }, @@ -160,17 +160,17 @@ export class TokenContractTest { tokenSim.mintPublic(address, amount); const publicBalance = await asset.methods.balance_of_public(address).simulate(); - this.logger(`Public balance of wallet 0: ${publicBalance}`); + this.logger.verbose(`Public balance of wallet 0: ${publicBalance}`); expect(publicBalance).toEqual(this.tokenSim.balanceOfPublic(address)); tokenSim.mintPrivate(amount); tokenSim.redeemShield(address, amount); const privateBalance = await asset.methods.balance_of_private(address).simulate(); - this.logger(`Private balance of wallet 0: ${privateBalance}`); + this.logger.verbose(`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}`); + this.logger.verbose(`Total supply: ${totalSupply}`); expect(totalSupply).toEqual(tokenSim.totalSupply); return Promise.resolve(); 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 e89585341ec3..35d4bb2290f6 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 @@ -23,7 +23,7 @@ export async function getACVMConfig(logger: DebugLogger) { 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}`); + logger.verbose(`Using native ACVM binary at ${expectedAcvmPath} with working directory ${acvmWorkingDirectory}`); const directoryToCleanup = ACVM_WORKING_DIRECTORY ? undefined : tempWorkingDirectory; @@ -40,7 +40,7 @@ export async function getACVMConfig(logger: DebugLogger) { cleanup, }; } catch (err) { - logger(`Native ACVM not available, error: ${err}`); + logger.verbose(`Native ACVM not available, error: ${err}`); return undefined; } } 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 d28e03966675..f5536ea1cdd3 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -59,11 +59,11 @@ export class SnapshotManager { ) { 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.logger.verbose(`No data path given, will not persist any snapshots.`); this.context = await this.setupFromFresh(); - this.logger(`Applying state transition for ${name}...`); + this.logger.verbose(`Applying state transition for ${name}...`); const snapshotData = await apply(this.context); - this.logger(`State transition for ${name} complete.`); + this.logger.verbose(`State transition for ${name} complete.`); // Execute the restoration function. await restore(snapshotData, this.context); return; @@ -74,7 +74,7 @@ export class SnapshotManager { if (existsSync(snapshotPath)) { // 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.logger.verbose(`Snapshot exists at ${snapshotPath}. Continuing...`); this.snapshotStack.push({ name, apply, restore, snapshotPath }); return; } @@ -89,9 +89,9 @@ export class SnapshotManager { this.snapshotStack.push({ name, apply, restore, snapshotPath }); // Apply current state transition. - this.logger(`Applying state transition for ${name}...`); + this.logger.verbose(`Applying state transition for ${name}...`); const snapshotData = await apply(this.context); - this.logger(`State transition for ${name} complete.`); + this.logger.verbose(`State transition for ${name} complete.`); // Execute the restoration function. await restore(snapshotData, this.context); @@ -104,12 +104,12 @@ export class SnapshotManager { // 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}...`); + this.logger.verbose(`Saving snapshot to ${snapshotPath}...`); if (mkdirSync(snapshotPath, { recursive: true })) { copySync(this.livePath, snapshotPath); - this.logger(`Snapshot copied to ${snapshotPath}.`); + this.logger.verbose(`Snapshot copied to ${snapshotPath}.`); } else { - this.logger(`Snapshot already exists at ${snapshotPath}. Discarding our version.`); + this.logger.verbose(`Snapshot already exists at ${snapshotPath}. Discarding our version.`); await this.teardown(); } } @@ -128,15 +128,15 @@ export class SnapshotManager { 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}...`); + this.logger.verbose(`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}...`); + this.logger.verbose(`Executing restoration function for ${e.name}...`); await e.restore(snapshotData, this.context!); - this.logger(`Restoration of ${e.name} complete.`); + this.logger.verbose(`Restoration of ${e.name} complete.`); }); } else { this.context = await this.setupFromFresh(this.livePath); @@ -166,7 +166,7 @@ export class SnapshotManager { * If there is no statePath, in-memory and temporary state locations will be used. */ private async setupFromFresh(statePath?: string): Promise { - this.logger(`Initializing state...`); + this.logger.verbose(`Initializing state...`); // Fetch the AztecNode config. // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. @@ -174,14 +174,14 @@ export class SnapshotManager { aztecNodeConfig.dataDirectory = statePath; // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. - this.logger('Starting anvil...'); + this.logger.verbose('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(); // Deploy our L1 contracts. - this.logger('Deploying L1 contracts...'); + this.logger.verbose('Deploying L1 contracts...'); const hdAccount = mnemonicToAccount(MNEMONIC); const privKeyRaw = hdAccount.getHdKey().privateKey; const publisherPrivKey = privKeyRaw === null ? null : Buffer.from(privKeyRaw); @@ -196,10 +196,10 @@ export class SnapshotManager { aztecNodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; } - this.logger('Creating and synching an aztec node...'); + this.logger.verbose('Creating and synching an aztec node...'); const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); - this.logger('Creating pxe...'); + this.logger.verbose('Creating pxe...'); const pxeConfig = getPXEServiceConfig(); pxeConfig.dataDirectory = statePath; const pxe = await createPXEService(aztecNode, pxeConfig); @@ -221,7 +221,7 @@ export class SnapshotManager { * Given a statePath, setup the system starting from that state. */ private async setupFromState(statePath: string): Promise { - this.logger(`Initializing with saved state at ${statePath}...`); + this.logger.verbose(`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. @@ -248,10 +248,10 @@ export class SnapshotManager { aztecNodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; } - this.logger('Creating aztec node...'); + this.logger.verbose('Creating aztec node...'); const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); - this.logger('Creating pxe...'); + this.logger.verbose('Creating pxe...'); const pxeConfig = getPXEServiceConfig(); pxeConfig.dataDirectory = statePath; const pxe = await createPXEService(aztecNode, pxeConfig); @@ -279,7 +279,7 @@ export const addAccounts = GrumpkinPrivateKey.random(), ]); - logger('Simulating account deployment...'); + logger.verbose('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 @@ -296,7 +296,7 @@ export const addAccounts = return account; }); - logger('Deploying accounts...'); + logger.verbose('Deploying accounts...'); const txs = await Promise.all(accountManagers.map(account => account.deploy())); await Promise.all(txs.map(tx => tx.wait({ interval: 0.1 }))); diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index ade1be7c9568..e9fcce89357b 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -377,7 +377,7 @@ export async function setup( const aztecNode = await AztecNodeService.createAndSync(config); const sequencer = aztecNode.getSequencer(); - logger('Creating a pxe...'); + logger.verbose('Creating a pxe...'); const { pxe, wallets } = await setupPXEService(numberOfAccounts, aztecNode!, pxeOpts, logger); if (['1', 'true'].includes(ENABLE_GAS)) { From 8a8286aea59180aba7795997cccd725cc846dcfa Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Tue, 9 Apr 2024 21:46:42 +0000 Subject: [PATCH 12/20] formatting --- yarn-project/end-to-end/src/fixtures/get_acvm_config.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 35d4bb2290f6..556726411137 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 @@ -1,18 +1,15 @@ -import { type DebugLogger, fileURLToPath } from '@aztec/aztec.js'; +import { type DebugLogger } 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 From 3688ccc94075e4438caf4846b9b292e5e6d0456c Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Wed, 10 Apr 2024 09:19:59 +0000 Subject: [PATCH 13/20] fix --- yarn-project/end-to-end/package.local.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/end-to-end/package.local.json b/yarn-project/end-to-end/package.local.json index 8f1316f33284..d7161c036596 100644 --- a/yarn-project/end-to-end/package.local.json +++ b/yarn-project/end-to-end/package.local.json @@ -2,6 +2,6 @@ "scripts": { "build": "yarn clean && tsc -b && webpack", "formatting": "run -T prettier --check ./src \"!src/web/main.js\" && run -T eslint ./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=120000 --forceExit" } } From e679ebd27f73573cd316699d1d771a5b901744dc Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Wed, 10 Apr 2024 10:13:16 +0000 Subject: [PATCH 14/20] update snapshots --- .../__snapshots__/revert_code.test.ts.snap | 84 ++----------------- 1 file changed, 6 insertions(+), 78 deletions(-) diff --git a/yarn-project/circuits.js/src/structs/__snapshots__/revert_code.test.ts.snap b/yarn-project/circuits.js/src/structs/__snapshots__/revert_code.test.ts.snap index 88a46bd073be..fff7e680ed07 100644 --- a/yarn-project/circuits.js/src/structs/__snapshots__/revert_code.test.ts.snap +++ b/yarn-project/circuits.js/src/structs/__snapshots__/revert_code.test.ts.snap @@ -50,45 +50,9 @@ exports[`revert_code should serialize properly 2`] = ` `; exports[`revert_code should serialize properly 3`] = ` -Fr { - "asBigInt": 0n, - "asBuffer": { - "data": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], - "type": "Buffer", - }, +{ + "type": "Fr", + "value": "0x0000000000000000000000000000000000000000000000000000000000000000", } `; @@ -142,44 +106,8 @@ exports[`revert_code should serialize properly 5`] = ` `; exports[`revert_code should serialize properly 6`] = ` -Fr { - "asBigInt": 1n, - "asBuffer": { - "data": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - ], - "type": "Buffer", - }, +{ + "type": "Fr", + "value": "0x0000000000000000000000000000000000000000000000000000000000000001", } `; From c106682d084c5dabc9acc4bb0bb59255cbeefb1e Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Wed, 10 Apr 2024 11:58:44 +0000 Subject: [PATCH 15/20] run new contract tests. --- .circleci/config.yml | 2 +- yarn-project/Earthfile | 1 + yarn-project/end-to-end/Earthfile | 15 +- .../end-to-end/src/e2e_token_contract.test.ts | 1191 ----------------- 4 files changed, 11 insertions(+), 1198 deletions(-) delete mode 100644 yarn-project/end-to-end/src/e2e_token_contract.test.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 7d46584e12d8..04ff8c78cf2b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -705,7 +705,7 @@ jobs: - *setup_env - run: name: "Test" - command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_token_contract.test.ts + command: cond_spot_run_container end-to-end 4 ./src/e2e_token_contract/ aztec_manifest_key: end-to-end <<: *defaults_e2e_test diff --git a/yarn-project/Earthfile b/yarn-project/Earthfile index 75316122d4e4..604bdeef8ed6 100644 --- a/yarn-project/Earthfile +++ b/yarn-project/Earthfile @@ -70,6 +70,7 @@ end-to-end: FROM node:18.19.1-slim RUN apt-get update && apt-get install jq chromium netcat-openbsd -y ENV CHROME_BIN="/usr/bin/chromium" + COPY ../foundry/+build/usr/src/foundry/bin/anvil /usr/src/foundry/bin/anvil COPY +end-to-end-prod/usr/src /usr/src WORKDIR /usr/src/yarn-project/end-to-end ENTRYPOINT ["yarn", "test"] diff --git a/yarn-project/end-to-end/Earthfile b/yarn-project/end-to-end/Earthfile index d9b8aaef2436..322f2caa5296 100644 --- a/yarn-project/end-to-end/Earthfile +++ b/yarn-project/end-to-end/Earthfile @@ -2,7 +2,7 @@ VERSION 0.8 # requires first saving the images locally with ../+export-end-to-end -# run locally and build +# run locally and build E2E_TEST_LOCAL: FUNCTION ARG test @@ -115,8 +115,10 @@ e2e-lending-contract: DO +E2E_TEST --test=e2e_lending_contract.test.ts --e2e_mode=$e2e_mode e2e-token-contract: - ARG e2e_mode=local - DO +E2E_TEST --test=e2e_token_contract.test.ts --e2e_mode=$e2e_mode + LOCALLY + WITH DOCKER --load end-to-end=../+end-to-end + RUN docker run --rm -e LOG_LEVEL=silent -e DEBUG=aztec:e2e_token_contract* end-to-end ./src/e2e_token_contract/ + END e2e-authwit-test: ARG e2e_mode=local @@ -265,12 +267,13 @@ guides-sample-dapp: bench-publish-rollup: ARG e2e_mode=local - DO +E2E_TEST --test=benchmarks/bench_publish_rollup.test.ts --debug="aztec:benchmarks:*,aztec:sequencer,aztec:sequencer:*,aztec:world_state,aztec:merkle_trees" --e2e_mode=$e2e_mode --compose_file=./scripts/docker-compose-no-sandbox.yml + DO +E2E_TEST --test=benchmarks/bench_publish_rollup.test.ts --debug="aztec:benchmarks:*,aztec:sequencer,aztec:sequencer:*,aztec:world_state,aztec:merkle_trees" --e2e_mode=$e2e_mode --compose_file=./scripts/docker-compose-no-sandbox.yml bench-process-history: ARG e2e_mode=local - DO +E2E_TEST --test=benchmarks/bench_process_history.test.ts --debug="aztec:benchmarks:*,aztec:sequencer,aztec:sequencer:*,aztec:world_state,aztec:merkle_trees" --e2e_mode=$e2e_mode --compose_file=./scripts/docker-compose-no-sandbox.yml + DO +E2E_TEST --test=benchmarks/bench_process_history.test.ts --debug="aztec:benchmarks:*,aztec:sequencer,aztec:sequencer:*,aztec:world_state,aztec:merkle_trees" --e2e_mode=$e2e_mode --compose_file=./scripts/docker-compose-no-sandbox.yml bench-tx-size: ARG e2e_mode=local - DO +E2E_TEST --test=benchmarks/bench_tx_size_fees.test.ts --debug="aztec:benchmarks:*,aztec:sequencer,aztec:sequencer:*,aztec:world_state,aztec:merkle_trees" --e2e_mode=$e2e_mode --enable_gas=1 --compose_file=./scripts/docker-compose-no-sandbox.yml + DO +E2E_TEST --test=benchmarks/bench_tx_size_fees.test.ts --debug="aztec:benchmarks:*,aztec:sequencer,aztec:sequencer:*,aztec:world_state,aztec:merkle_trees" --e2e_mode= +$e2e_mode --enable_gas=1 --compose_file=./scripts/docker-compose-no-sandbox.yml \ No newline at end of file diff --git a/yarn-project/end-to-end/src/e2e_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_token_contract.test.ts deleted file mode 100644 index 01e3b683488b..000000000000 --- a/yarn-project/end-to-end/src/e2e_token_contract.test.ts +++ /dev/null @@ -1,1191 +0,0 @@ -import { - type AccountWallet, - type DebugLogger, - ExtendedNote, - Fr, - FunctionSelector, - Note, - type TxHash, - computeAuthWitMessageHash, - computeMessageSecretHash, -} from '@aztec/aztec.js'; -import { decodeFunctionSignature } from '@aztec/foundation/abi'; -import { DocsExampleContract, ReaderContract, TokenContract } from '@aztec/noir-contracts.js'; - -import { jest } from '@jest/globals'; - -import { BITSIZE_TOO_BIG_ERROR, U128_OVERFLOW_ERROR, U128_UNDERFLOW_ERROR } from './fixtures/fixtures.js'; -import { publicDeployAccounts, setup } from './fixtures/utils.js'; -import { TokenSimulator } from './simulators/token_simulator.js'; - -const TIMEOUT = 100_000; - -describe('e2e_token_contract', () => { - jest.setTimeout(TIMEOUT); - - const TOKEN_NAME = 'Aztec Token'; - const TOKEN_SYMBOL = 'AZT'; - const TOKEN_DECIMALS = 18n; - let teardown: () => Promise; - let wallets: AccountWallet[]; - - let logger: DebugLogger; - - let asset: TokenContract; - let badAccount: DocsExampleContract; - - let tokenSim: TokenSimulator; - - const addPendingShieldNoteToPXE = async (accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) => { - const note = new Note([new Fr(amount), secretHash]); - const extendedNote = new ExtendedNote( - note, - wallets[accountIndex].getAddress(), - asset.address, - TokenContract.storage.pending_shields.slot, - TokenContract.notes.TransparentNote.id, - txHash, - ); - await wallets[accountIndex].addNote(extendedNote); - }; - - 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; - }; - - beforeAll(async () => { - ({ teardown, logger, wallets } = await setup(3)); - await publicDeployAccounts(wallets[0], wallets.slice(0, 2)); - - TokenContract.artifact.functions.forEach(fn => { - const sig = decodeFunctionSignature(fn.name, fn.parameters); - logger.verbose( - `Function ${sig} and the selector: ${FunctionSelector.fromNameAndParameters(fn.name, fn.parameters)}`, - ); - }); - - asset = await TokenContract.deploy(wallets[0], wallets[0].getAddress(), TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS) - .send() - .deployed(); - logger.info(`Token deployed to ${asset.address}`); - tokenSim = new TokenSimulator( - asset, - logger, - wallets.map(w => w.getAddress()), - ); - - expect(await asset.methods.admin().simulate()).toBe(wallets[0].getAddress().toBigInt()); - - badAccount = await DocsExampleContract.deploy(wallets[0]).send().deployed(); - }, 100_000); - - afterAll(() => teardown()); - - afterEach(async () => { - await tokenSim.check(); - }, TIMEOUT); - - describe('Reading constants', () => { - let reader: ReaderContract; - beforeAll(async () => { - reader = await ReaderContract.deploy(wallets[0]).send().deployed(); - }); - - describe('name', () => { - it.each([ - ['private', 'check_name_private' as const, "Cannot satisfy constraint 'name.is_eq(_what)'"], - [ - 'public', - 'check_name_public' as const, - "Failed to solve brillig function, reason: explicit trap hit in brillig 'name.is_eq(_what)'", - ], - ])('name - %s', async (_type, method, errorMessage) => { - const t = toString(await asset.methods.un_get_name().simulate()); - expect(t).toBe(TOKEN_NAME); - - await reader.methods[method](asset.address, TOKEN_NAME).send().wait(); - await expect(reader.methods[method](asset.address, 'WRONG_NAME').prove()).rejects.toThrow(errorMessage); - }); - }); - - describe('symbol', () => { - it('private', async () => { - const t = toString(await asset.methods.un_get_symbol().simulate()); - expect(t).toBe(TOKEN_SYMBOL); - - await reader.methods.check_symbol_private(asset.address, TOKEN_SYMBOL).send().wait(); - - await expect(reader.methods.check_symbol_private(asset.address, 'WRONG_SYMBOL').prove()).rejects.toThrow( - "Cannot satisfy constraint 'symbol.is_eq(_what)'", - ); - }); - it('public', async () => { - const t = toString(await asset.methods.un_get_symbol().simulate()); - expect(t).toBe(TOKEN_SYMBOL); - - await reader.methods.check_symbol_public(asset.address, TOKEN_SYMBOL).send().wait(); - - await expect(reader.methods.check_symbol_public(asset.address, 'WRONG_SYMBOL').prove()).rejects.toThrow( - "Failed to solve brillig function, reason: explicit trap hit in brillig 'symbol.is_eq(_what)'", - ); - }); - }); - - describe('decimals', () => { - it('private', async () => { - const t = await asset.methods.un_get_decimals().simulate(); - expect(t).toBe(TOKEN_DECIMALS); - - await reader.methods.check_decimals_private(asset.address, TOKEN_DECIMALS).send().wait(); - - await expect(reader.methods.check_decimals_private(asset.address, 99).prove()).rejects.toThrow( - "Cannot satisfy constraint 'ret[0] as u8 == what'", - ); - }); - - it('public', async () => { - const t = await asset.methods.un_get_decimals().simulate(); - expect(t).toBe(TOKEN_DECIMALS); - - await reader.methods.check_decimals_public(asset.address, TOKEN_DECIMALS).send().wait(); - - await expect(reader.methods.check_decimals_public(asset.address, 99).prove()).rejects.toThrow( - "Failed to solve brillig function, reason: explicit trap hit in brillig 'ret[0] as u8 == what'", - ); - }); - }); - }); - - describe('Access controlled functions', () => { - it('Set admin', async () => { - await asset.methods.set_admin(wallets[1].getAddress()).send().wait(); - expect(await asset.methods.admin().simulate()).toBe(wallets[1].getAddress().toBigInt()); - }); - - it('Add minter as admin', async () => { - await asset.withWallet(wallets[1]).methods.set_minter(wallets[1].getAddress(), true).send().wait(); - expect(await asset.methods.is_minter(wallets[1].getAddress()).simulate()).toBe(true); - }); - - it('Revoke minter as admin', async () => { - await asset.withWallet(wallets[1]).methods.set_minter(wallets[1].getAddress(), false).send().wait(); - expect(await asset.methods.is_minter(wallets[1].getAddress()).simulate()).toBe(false); - }); - - describe('failure cases', () => { - it('Set admin (not admin)', async () => { - await expect(asset.methods.set_admin(wallets[0].getAddress()).prove()).rejects.toThrow( - 'Assertion failed: caller is not admin', - ); - }); - it('Revoke minter not as admin', async () => { - await expect(asset.methods.set_minter(wallets[0].getAddress(), false).prove()).rejects.toThrow( - 'Assertion failed: caller is not admin', - ); - }); - }); - }); - - describe('Minting', () => { - describe('Public', () => { - it('as minter', async () => { - const amount = 10000n; - await asset.methods.mint_public(wallets[0].getAddress(), amount).send().wait(); - - tokenSim.mintPublic(wallets[0].getAddress(), amount); - expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual( - tokenSim.balanceOfPublic(wallets[0].getAddress()), - ); - expect(await asset.methods.total_supply().simulate()).toEqual(tokenSim.totalSupply); - }); - - describe('failure cases', () => { - it('as non-minter', async () => { - const amount = 10000n; - await expect( - asset.withWallet(wallets[1]).methods.mint_public(wallets[0].getAddress(), amount).prove(), - ).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(wallets[0].getAddress(), amount).prove()).rejects.toThrow( - BITSIZE_TOO_BIG_ERROR, - ); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.balanceOfPublic(wallets[0].getAddress()); - await expect(asset.methods.mint_public(wallets[0].getAddress(), amount).prove()).rejects.toThrow( - U128_OVERFLOW_ERROR, - ); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.balanceOfPublic(wallets[0].getAddress()); - await expect(asset.methods.mint_public(wallets[1].getAddress(), amount).prove()).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 addPendingShieldNoteToPXE(0, amount, secretHash, txHash); - const txClaim = asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).send(); - // docs:start:debug - const receiptClaim = await txClaim.wait({ debug: true }); - // docs:end:debug - tokenSim.redeemShield(wallets[0].getAddress(), 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(addPendingShieldNoteToPXE(0, amount, secretHash, txHash)).rejects.toThrow( - 'The note has been destroyed.', - ); - await expect(asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).prove()).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).prove()).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).prove()).rejects.toThrow(BITSIZE_TOO_BIG_ERROR); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.balanceOfPrivate(wallets[0].getAddress()); - expect(amount).toBeLessThan(2n ** 128n); - await expect(asset.methods.mint_private(amount, secretHash).prove()).rejects.toThrow(U128_OVERFLOW_ERROR); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.totalSupply; - await expect(asset.methods.mint_private(amount, secretHash).prove()).rejects.toThrow(U128_OVERFLOW_ERROR); - }); - }); - }); - }); - - describe('Transfer', () => { - describe('public', () => { - it('transfer less than balance', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, 0).send().wait(); - - tokenSim.transferPublic(wallets[0].getAddress(), wallets[1].getAddress(), amount); - }); - - it('transfer to self', async () => { - const balance = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer_public(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).send().wait(); - - tokenSim.transferPublic(wallets[0].getAddress(), wallets[0].getAddress(), amount); - }); - - it('transfer on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - 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(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - // docs:end:authwit_public_transfer_example - - // Perform the transfer - await action.send().wait(); - - tokenSim.transferPublic(wallets[0].getAddress(), wallets[1].getAddress(), 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(wallets[0].getAddress(), wallets[1].getAddress(), 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(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = 0; - await expect( - asset.methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce).prove(), - ).rejects.toThrow(U128_UNDERFLOW_ERROR); - }); - - it('transfer on behalf of self with non-zero nonce', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 - 1n; - const nonce = 1; - await expect( - asset.methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce).prove(), - ).rejects.toThrow('Assertion failed: invalid nonce'); - }); - - it('transfer on behalf of other without "approval"', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - await expect( - asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce) - .prove(), - ).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(wallets[0].getAddress()).simulate(); - const balance1 = await asset.methods.balance_of_public(wallets[1].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - expect( - await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: wallets[1].getAddress(), 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: wallets[1].getAddress(), action }, true).send().wait(); - - expect( - await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: wallets[1].getAddress(), action }), - ).toEqual({ - isValidInPrivate: false, - isValidInPublic: true, - }); - - // Perform the transfer - await expect(action.prove()).rejects.toThrow(U128_UNDERFLOW_ERROR); - - expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual(balance0); - expect(await asset.methods.balance_of_public(wallets[1].getAddress()).simulate()).toEqual(balance1); - }); - - it('transfer on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const balance1 = await asset.methods.balance_of_public(wallets[1].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - await wallets[0].setPublicAuthWit({ caller: wallets[0].getAddress(), action }, true).send().wait(); - - // Perform the transfer - await expect(action.prove()).rejects.toThrow('Assertion failed: Message not authorized by account'); - - expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual(balance0); - expect(await asset.methods.balance_of_public(wallets[1].getAddress()).simulate()).toEqual(balance1); - }); - - it('transfer on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const balance1 = await asset.methods.balance_of_public(wallets[1].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[0].getAddress(), action }, true).send().wait(); - - // Perform the transfer - await expect(action.prove()).rejects.toThrow('Assertion failed: Message not authorized by account'); - - expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual(balance0); - expect(await asset.methods.balance_of_public(wallets[1].getAddress()).simulate()).toEqual(balance1); - }); - - it('transfer on behalf of other, cancelled authwit', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - const nonce = Fr.random(); - - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - await wallets[0].cancelAuthWit({ caller: wallets[1].getAddress(), 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(wallets[0].getAddress(), wallets[1].getAddress(), 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(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - const nonce = Fr.random(); - - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), 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(wallets[0].getAddress(), wallets[1].getAddress(), 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(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - const nonce = Fr.random(); - - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - const messageHash = computeAuthWitMessageHash( - wallets[1].getAddress(), - 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(wallets[0].getAddress(), wallets[1].getAddress(), 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, wallets[1].getAddress(), 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 - }); - }); - }); - - describe('private', () => { - it('transfer less than balance', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, 0).send().wait(); - tokenSim.transferPrivate(wallets[0].getAddress(), wallets[1].getAddress(), amount); - }); - - it('transfer to self', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).send().wait(); - tokenSim.transferPrivate(wallets[0].getAddress(), wallets[0].getAddress(), amount); - }); - - it('transfer on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - expect( - await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: wallets[1].getAddress(), action }), - ).toEqual({ - isValidInPrivate: true, - isValidInPublic: false, - }); - // docs:end:authwit_transfer_example - - // Perform the transfer - await action.send().wait(); - tokenSim.transferPrivate(wallets[0].getAddress(), wallets[1].getAddress(), amount); - - // Perform the transfer again, should fail - const txReplay = asset - .withWallet(wallets[1]) - .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), 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(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - expect(amount).toBeGreaterThan(0n); - await expect( - asset.methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, 0).prove(), - ).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(wallets[0].getAddress()).simulate(); - const amount = balance0 - 1n; - expect(amount).toBeGreaterThan(0n); - await expect( - asset.methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, 1).prove(), - ).rejects.toThrow('Assertion failed: invalid nonce'); - }); - - it('transfer more than balance on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const balance1 = await asset.methods.balance_of_private(wallets[1].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), 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: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - // Perform the transfer - await expect(action.prove()).rejects.toThrow('Assertion failed: Balance too low'); - expect(await asset.methods.balance_of_private(wallets[0].getAddress()).simulate()).toEqual(balance0); - expect(await asset.methods.balance_of_private(wallets[1].getAddress()).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(wallets[0].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - const messageHash = computeAuthWitMessageHash( - wallets[1].getAddress(), - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - await expect(action.prove()).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(wallets[0].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - const expectedMessageHash = computeAuthWitMessageHash( - wallets[2].getAddress(), - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[2].addAuthWitness(witness); - - await expect(action.prove()).rejects.toThrow( - `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, - ); - expect(await asset.methods.balance_of_private(wallets[0].getAddress()).simulate()).toEqual(balance0); - }); - - it('transfer on behalf of other, cancelled authwit', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), 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(wallets[0].getAddress(), wallets[1].getAddress(), 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(wallets[0].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - await wallets[0].cancelAuthWit({ caller: wallets[1].getAddress(), action }).send().wait(); - - // Perform the transfer, should fail because nullifier already emitted - const txCancelledAuthwit = asset - .withWallet(wallets[1]) - .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), 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, wallets[1].getAddress(), 0, nonce) - .send(); - await expect(txCancelledAuthwit.wait()).rejects.toThrow( - "Assertion failed: Message not authorized by account 'result == IS_VALID_SELECTOR'", - ); - }); - }); - }); - }); - - describe('Shielding (shield + redeem_shield)', () => { - const secret = Fr.random(); - let secretHash: Fr; - - beforeAll(() => { - secretHash = computeMessageSecretHash(secret); - }); - - it('on behalf of self', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balancePub / 2n; - expect(amount).toBeGreaterThan(0n); - - const receipt = await asset.methods.shield(wallets[0].getAddress(), amount, secretHash, 0).send().wait(); - - tokenSim.shield(wallets[0].getAddress(), amount); - await tokenSim.check(); - - // Redeem it - await addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); - await asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).send().wait(); - - tokenSim.redeemShield(wallets[0].getAddress(), amount); - }); - - it('on behalf of other', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - 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(wallets[0].getAddress(), amount, secretHash, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - const receipt = await action.send().wait(); - - tokenSim.shield(wallets[0].getAddress(), amount); - await tokenSim.check(); - - // Check that replaying the shield should fail! - const txReplay = asset - .withWallet(wallets[1]) - .methods.shield(wallets[0].getAddress(), amount, secretHash, nonce) - .send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); - - // Redeem it - await addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); - await asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).send().wait(); - - tokenSim.redeemShield(wallets[0].getAddress(), amount); - }); - - describe('failure cases', () => { - it('on behalf of self (more than balance)', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balancePub + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect(asset.methods.shield(wallets[0].getAddress(), amount, secretHash, 0).prove()).rejects.toThrow( - U128_UNDERFLOW_ERROR, - ); - }); - - it('on behalf of self (invalid nonce)', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balancePub + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect(asset.methods.shield(wallets[0].getAddress(), amount, secretHash, 1).prove()).rejects.toThrow( - 'Assertion failed: invalid nonce', - ); - }); - - it('on behalf of other (more than balance)', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - 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(wallets[0].getAddress(), amount, secretHash, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - await expect(action.prove()).rejects.toThrow(U128_UNDERFLOW_ERROR); - }); - - it('on behalf of other (wrong designated caller)', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - 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(wallets[0].getAddress(), amount, secretHash, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - await expect(action.prove()).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(wallets[0].getAddress()).simulate(); - const amount = balance / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.withWallet(wallets[1]).methods.shield(wallets[0].getAddress(), amount, secretHash, nonce).prove(), - ).rejects.toThrow(`Assertion failed: Message not authorized by account`); - }); - }); - }); - - describe('Unshielding', () => { - it('on behalf of self', async () => { - const balancePriv = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balancePriv / 2n; - expect(amount).toBeGreaterThan(0n); - - await asset.methods.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).send().wait(); - - tokenSim.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount); - }); - - it('on behalf of other', async () => { - const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), 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: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - await action.send().wait(); - tokenSim.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount); - - // Perform the transfer again, should fail - const txReplay = asset - .withWallet(wallets[1]) - .methods.unshield(wallets[0].getAddress(), wallets[1].getAddress(), 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(wallets[0].getAddress()).simulate(); - const amount = balancePriv + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.methods.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).prove(), - ).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('on behalf of self (invalid nonce)', async () => { - const balancePriv = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balancePriv + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.methods.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount, 1).prove(), - ).rejects.toThrow('Assertion failed: invalid nonce'); - }); - - it('on behalf of other (more than balance)', async () => { - const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), 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: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - await expect(action.prove()).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('on behalf of other (invalid designated caller)', async () => { - const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).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(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - const expectedMessageHash = computeAuthWitMessageHash( - wallets[2].getAddress(), - 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: wallets[1].getAddress(), action }); - await wallets[2].addAuthWitness(witness); - - await expect(action.prove()).rejects.toThrow( - `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, - ); - }); - }); - }); - - describe('Burn', () => { - describe('public', () => { - it('burn less than balance', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.burn_public(wallets[0].getAddress(), amount, 0).send().wait(); - - tokenSim.burnPublic(wallets[0].getAddress(), amount); - }); - - it('burn on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).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(wallets[0].getAddress(), amount, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - await action.send().wait(); - - tokenSim.burnPublic(wallets[0].getAddress(), 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(wallets[0].getAddress(), 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(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = 0; - await expect(asset.methods.burn_public(wallets[0].getAddress(), amount, nonce).prove()).rejects.toThrow( - U128_UNDERFLOW_ERROR, - ); - }); - - it('burn on behalf of self with non-zero nonce', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 - 1n; - expect(amount).toBeGreaterThan(0n); - const nonce = 1; - await expect(asset.methods.burn_public(wallets[0].getAddress(), amount, nonce).prove()).rejects.toThrow( - 'Assertion failed: invalid nonce', - ); - }); - - it('burn on behalf of other without "approval"', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - await expect( - asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).prove(), - ).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(wallets[0].getAddress()).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(wallets[0].getAddress(), amount, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - await expect(action.prove()).rejects.toThrow(U128_UNDERFLOW_ERROR); - }); - - it('burn on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).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_public(wallets[0].getAddress(), amount, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[0].getAddress(), action }, true).send().wait(); - - await expect( - asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).prove(), - ).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(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.burn(wallets[0].getAddress(), amount, 0).send().wait(); - tokenSim.burnPrivate(wallets[0].getAddress(), amount); - }); - - it('burn on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).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(wallets[0].getAddress(), 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: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - await asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce).send().wait(); - tokenSim.burnPrivate(wallets[0].getAddress(), amount); - - // Perform the transfer again, should fail - const txReplay = asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), 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(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - expect(amount).toBeGreaterThan(0n); - await expect(asset.methods.burn(wallets[0].getAddress(), amount, 0).prove()).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(wallets[0].getAddress()).simulate(); - const amount = balance0 - 1n; - expect(amount).toBeGreaterThan(0n); - await expect(asset.methods.burn(wallets[0].getAddress(), amount, 1).prove()).rejects.toThrow( - 'Assertion failed: invalid nonce', - ); - }); - - it('burn more than balance on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).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(wallets[0].getAddress(), 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: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - await expect(action.prove()).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('burn on behalf of other without approval', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).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(wallets[0].getAddress(), amount, nonce); - const messageHash = computeAuthWitMessageHash( - wallets[1].getAddress(), - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - await expect(action.prove()).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(wallets[0].getAddress()).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(wallets[0].getAddress(), amount, nonce); - const expectedMessageHash = computeAuthWitMessageHash( - wallets[2].getAddress(), - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[2].addAuthWitness(witness); - - await expect(action.prove()).rejects.toThrow( - `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, - ); - }); - }); - }); - }); -}); From 6a06ebcbe05fd3a7a1cae46a2e730062289a091f Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Wed, 10 Apr 2024 12:36:14 +0000 Subject: [PATCH 16/20] fix --- yarn-project/end-to-end/Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/yarn-project/end-to-end/Dockerfile b/yarn-project/end-to-end/Dockerfile index 52bd00f142ef..882a890e642f 100644 --- a/yarn-project/end-to-end/Dockerfile +++ b/yarn-project/end-to-end/Dockerfile @@ -12,7 +12,6 @@ COPY --from=bb.js /usr/src/barretenberg/ts /usr/src/barretenberg/ts COPY --from=noir-packages /usr/src/noir/packages /usr/src/noir/packages COPY --from=contracts /usr/src/l1-contracts /usr/src/l1-contracts COPY --from=noir-projects /usr/src/noir-projects /usr/src/noir-projects -# We want the native ACVM binary COPY --from=noir /usr/src/noir/noir-repo/target/release/acvm /usr/src/noir/noir-repo/target/release/acvm WORKDIR /usr/src/yarn-project @@ -40,13 +39,16 @@ RUN rm -rf /usr/src/noir-projects /usr/src/l1-contracts # Create minimal image. FROM node:18.19.1-slim -RUN apt-get update && apt-get install jq gnupg wget netcat-openbsd -y && \ +RUN apt-get update && apt-get install jq gnupg wget netcat-openbsd git -y && \ wget --quiet --output-document=- https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor > /etc/apt/trusted.gpg.d/google-archive.gpg && \ sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' && \ apt-get update && \ apt-get install google-chrome-stable -y --no-install-recommends && \ rm -rf /var/lib/apt/lists/* ENV CHROME_BIN="/usr/bin/google-chrome-stable" +# Anvil. Hacky, but we're moving to earthly. +RUN curl -L https://foundry.paradigm.xyz | bash +RUN /root/.foundry/bin/foundryup --version nightly-de33b6af53005037b463318d2628b5cfcaf39916 && mkdir -p /usr/src/foundry/bin && ln -s /root/.foundry/bin/anvil /usr/src/foundry/bin/anvil COPY --from=builder /usr/src /usr/src WORKDIR /usr/src/yarn-project/end-to-end ENTRYPOINT ["yarn", "test"] \ No newline at end of file From d74bd8b9b13f406af23440f07f1a8a5ac88d2bb6 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Wed, 10 Apr 2024 12:48:08 +0000 Subject: [PATCH 17/20] fix --- yarn-project/end-to-end/Dockerfile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/yarn-project/end-to-end/Dockerfile b/yarn-project/end-to-end/Dockerfile index 882a890e642f..e008a983b254 100644 --- a/yarn-project/end-to-end/Dockerfile +++ b/yarn-project/end-to-end/Dockerfile @@ -5,7 +5,7 @@ FROM --platform=linux/amd64 aztecprotocol/noir-projects as noir-projects FROM aztecprotocol/noir as noir FROM node:18.19.0 as builder -RUN apt update && apt install -y jq curl perl && rm -rf /var/lib/apt/lists/* && apt-get clean +RUN apt update && apt install -y jq curl perl git && rm -rf /var/lib/apt/lists/* && apt-get clean # Copy in portalled packages. COPY --from=bb.js /usr/src/barretenberg/ts /usr/src/barretenberg/ts @@ -37,18 +37,19 @@ RUN yarn workspaces focus @aztec/end-to-end --production && yarn cache clean # We no longer need these RUN rm -rf /usr/src/noir-projects /usr/src/l1-contracts +# Anvil. Hacky, but can't be bothered handling foundry image as we're moving to earthly. +RUN curl -L https://foundry.paradigm.xyz | bash +RUN /root/.foundry/bin/foundryup --version nightly-de33b6af53005037b463318d2628b5cfcaf39916 && mkdir -p /usr/src/foundry/bin && ln -s /root/.foundry/bin/anvil /usr/src/foundry/bin/anvil + # Create minimal image. FROM node:18.19.1-slim -RUN apt-get update && apt-get install jq gnupg wget netcat-openbsd git -y && \ +RUN apt-get update && apt-get install jq gnupg wget netcat-openbsd -y && \ wget --quiet --output-document=- https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor > /etc/apt/trusted.gpg.d/google-archive.gpg && \ sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' && \ apt-get update && \ apt-get install google-chrome-stable -y --no-install-recommends && \ rm -rf /var/lib/apt/lists/* ENV CHROME_BIN="/usr/bin/google-chrome-stable" -# Anvil. Hacky, but we're moving to earthly. -RUN curl -L https://foundry.paradigm.xyz | bash -RUN /root/.foundry/bin/foundryup --version nightly-de33b6af53005037b463318d2628b5cfcaf39916 && mkdir -p /usr/src/foundry/bin && ln -s /root/.foundry/bin/anvil /usr/src/foundry/bin/anvil COPY --from=builder /usr/src /usr/src WORKDIR /usr/src/yarn-project/end-to-end ENTRYPOINT ["yarn", "test"] \ No newline at end of file From b22985d1d103be85dbe238bcaa7ec18e03391812 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Wed, 10 Apr 2024 12:59:22 +0000 Subject: [PATCH 18/20] fix --- docs/docs/developers/tutorials/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/developers/tutorials/testing.md b/docs/docs/developers/tutorials/testing.md index 9e80aba1636f..d3dd5c5fcfc2 100644 --- a/docs/docs/developers/tutorials/testing.md +++ b/docs/docs/developers/tutorials/testing.md @@ -88,7 +88,7 @@ This debug information will be populated in the transaction receipt. You can log If a note doesn't appear when you expect it to, check the visible notes returned by the debug options. See the following example for reference on how it's done in the token contract tests. -#include_code debug /yarn-project/end-to-end/src/e2e_token_contract.test.ts typescript +#include_code debug /yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts typescript If the note appears in the visible notes and it contains the expected values there is probably an issue with how you fetch the notes. Check that the note getter (or note viewer) parameters are set correctly. If the note doesn't appear, ensure that you have emitted the corresponding encrypted log (usually by passing in a `broadcast = true` param to the `create_note` function). You can also check the Sandbox logs to see if the `emitEncryptedLog` was emitted. Run `export DEBUG="aztec:\*" before spinning up sandbox to see all the logs. From f7c2cdbde59e847a73b10bed0f7e36b418891093 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Wed, 10 Apr 2024 13:17:42 +0000 Subject: [PATCH 19/20] fix --- yarn-project/end-to-end/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/end-to-end/Dockerfile b/yarn-project/end-to-end/Dockerfile index e008a983b254..215a0fb1f977 100644 --- a/yarn-project/end-to-end/Dockerfile +++ b/yarn-project/end-to-end/Dockerfile @@ -39,7 +39,7 @@ RUN rm -rf /usr/src/noir-projects /usr/src/l1-contracts # Anvil. Hacky, but can't be bothered handling foundry image as we're moving to earthly. RUN curl -L https://foundry.paradigm.xyz | bash -RUN /root/.foundry/bin/foundryup --version nightly-de33b6af53005037b463318d2628b5cfcaf39916 && mkdir -p /usr/src/foundry/bin && ln -s /root/.foundry/bin/anvil /usr/src/foundry/bin/anvil +RUN /root/.foundry/bin/foundryup --version nightly-de33b6af53005037b463318d2628b5cfcaf39916 && mkdir -p /usr/src/foundry/bin && cp /root/.foundry/bin/anvil /usr/src/foundry/bin/anvil # Create minimal image. FROM node:18.19.1-slim From ffd887c95b80aa4d1be438fad1a7c2a5e59b7484 Mon Sep 17 00:00:00 2001 From: Charlie Lye Date: Wed, 10 Apr 2024 13:25:36 +0000 Subject: [PATCH 20/20] fix --- .../contracts/resources/common_patterns/authwit.md | 8 ++++---- .../contracts/resources/common_patterns/main.md | 2 +- docs/docs/developers/tutorials/writing_token_contract.md | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/docs/developers/contracts/resources/common_patterns/authwit.md b/docs/docs/developers/contracts/resources/common_patterns/authwit.md index 10480a7de40e..334eba3e2406 100644 --- a/docs/docs/developers/contracts/resources/common_patterns/authwit.md +++ b/docs/docs/developers/contracts/resources/common_patterns/authwit.md @@ -98,9 +98,9 @@ To make it convenient to compute the message hashes in TypeScript, the `aztec.js For private calls where we allow execution on behalf of others, we generally want to check if the current call is authenticated by `on_behalf_of`. To easily do so, we can use the `assert_current_call_valid_authwit` which fetches information from the current context without us needing to provide much beyond the `on_behalf_of`. -This function will then make a to `on_behalf_of` to execute the `spend_private_authwit` function which validates that the call is authenticated. +This function will then make a to `on_behalf_of` to execute the `spend_private_authwit` function which validates that the call is authenticated. The `on_behalf_of` should assert that we are indeed authenticated and then emit a nullifier when we are spending the authwit to prevent replay attacks. -If the return value is not as expected, we throw an error. +If the return value is not as expected, we throw an error. This is to cover the case where the `on_behalf_of` might implemented some function with the same selector as the `spend_private_authwit` that could be used to authenticate unintentionally. #### Example @@ -149,7 +149,7 @@ In the snippet we are constraining the `else` case such that only `nonce = 0` is Cool, so we have a function that checks if the current call is authenticated, but how do we actually authenticate it? Well, assuming that we use a wallet that is following the spec, we import `computeAuthWitMessageHash` from `aztec.js` to help us compute the hash, and then we simply `addAuthWitness` to the wallet. Behind the scenes this will make the witness available to the oracle. -#include_code authwit_transfer_example /yarn-project/end-to-end/src/e2e_token_contract.test.ts typescript +#include_code authwit_transfer_example /yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts typescript ### Public Functions @@ -165,7 +165,7 @@ Authenticating an action in the public domain is quite similar to the private do In the snippet below, this is done as a separate contract call, but can also be done as part of a batch as mentioned in the [Accounts concepts](./../../../../learn/concepts/accounts/authwit.md#what-about-public). -#include_code authwit_public_transfer_example /yarn-project/end-to-end/src/e2e_token_contract.test.ts typescript +#include_code authwit_public_transfer_example /yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts typescript #### Updating approval state in Noir diff --git a/docs/docs/developers/contracts/resources/common_patterns/main.md b/docs/docs/developers/contracts/resources/common_patterns/main.md index 45b4c8f623ec..39763df867d9 100644 --- a/docs/docs/developers/contracts/resources/common_patterns/main.md +++ b/docs/docs/developers/contracts/resources/common_patterns/main.md @@ -20,7 +20,7 @@ We call this the "authentication witness" pattern or authwit for short. Here you approve a contract to burn funds on your behalf. - Approve in public domain: - #include_code authwit_public_transfer_example /yarn-project/end-to-end/src/e2e_token_contract.test.ts typescript + #include_code authwit_public_transfer_example /yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts typescript Here you approve someone to transfer funds publicly on your behalf diff --git a/docs/docs/developers/tutorials/writing_token_contract.md b/docs/docs/developers/tutorials/writing_token_contract.md index 04d8b0008235..a4f5926c5b50 100644 --- a/docs/docs/developers/tutorials/writing_token_contract.md +++ b/docs/docs/developers/tutorials/writing_token_contract.md @@ -206,7 +206,6 @@ Just below the contract definition, add the following imports: We are importing the Option type, items from the `value_note` library to help manage private value storage, note utilities, context (for managing private and public execution contexts), `state_vars` for helping manage state, `types` for data manipulation and `oracle` for help passing data from the private to public execution context. We also import the `auth` [library](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/auth.nr) to handle token authorizations from [Account Contracts](../../learn/concepts/accounts/main). Check out the Account Contract with AuthWitness [here](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/schnorr_single_key_account_contract/src/main.nr). - For more detail on execution contexts, see [Contract Communication](../../learn/concepts/communication/main). ### Types files @@ -441,7 +440,7 @@ aztec-cli codegen target -o src/artifacts --ts Review the end to end tests for reference: -https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/yarn-project/end-to-end/src/e2e_token_contract.test.ts +https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/yarn-project/end-to-end/src/e2e_token_contract/*.test.ts ### Token Bridge Contract