diff --git a/barretenberg/scripts/bindgen.sh b/barretenberg/scripts/bindgen.sh index e3d5b9b5a70c..b1c43e319b3c 100755 --- a/barretenberg/scripts/bindgen.sh +++ b/barretenberg/scripts/bindgen.sh @@ -1,6 +1,15 @@ #!/usr/bin/env bash set -eu +if ! pip list | grep -E 'clang\s+16.0.6' > /dev/null; then + echo "You need to install python clang: pip install clang==16.0.6" + exit 1 +fi + #find ./cpp/src -type f -name "c_bind*.hpp" | ./scripts/decls_json.py > exports.json cat ./scripts/c_bind_files.txt | ./scripts/decls_json.py > exports.json -(cd ./ts && yarn node --loader ts-node/esm ./src/bindgen/index.ts ../exports.json > ./src/barretenberg_api/index.ts) \ No newline at end of file +( + cd ./ts && \ + yarn node --loader ts-node/esm ./src/bindgen/index.ts ../exports.json > ./src/barretenberg_api/index.ts && \ + yarn prettier -w ./src/barretenberg_api/index.ts +) \ No newline at end of file diff --git a/yarn-project/aztec.js/src/account_manager/index.ts b/yarn-project/aztec.js/src/account_manager/index.ts index d18bc8b38d24..55dbcb772baa 100644 --- a/yarn-project/aztec.js/src/account_manager/index.ts +++ b/yarn-project/aztec.js/src/account_manager/index.ts @@ -34,7 +34,7 @@ export class AccountManager { private accountContract: AccountContract, salt?: Salt, ) { - this.salt = salt ? new Fr(salt) : Fr.random(); + this.salt = salt !== undefined ? new Fr(salt) : Fr.random(); } protected getEncryptionPublicKey() { diff --git a/yarn-project/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..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,5 @@ -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; +export const GrumpkinPrivateKey = GrumpkinScalar; diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index f0e61fa68a19..8087a1be253b 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -15,7 +15,7 @@ "clean": "rm -rf ./dest .tsbuildinfo", "formatting": "run -T prettier --check ./src \"!src/web/main.js\" && run -T eslint ./src", "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src", - "test": "LOG_LEVEL=verbose NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --runInBand --testTimeout=60000 --forceExit", + "test": "LOG_LEVEL=${LOG_LEVEL:-verbose} NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --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" }, @@ -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", @@ -101,6 +102,7 @@ "node": ">=18" }, "jest": { + "slowTestThreshold": 300, "extensionsToTreatAsEsm": [ ".ts" ], diff --git a/yarn-project/end-to-end/src/e2e_aztec_js_browser.test.ts b/yarn-project/end-to-end/src/_e2e_aztec_js_browser.test.ts similarity index 100% rename from yarn-project/end-to-end/src/e2e_aztec_js_browser.test.ts rename to yarn-project/end-to-end/src/_e2e_aztec_js_browser.test.ts diff --git a/yarn-project/end-to-end/src/e2e_persistence.test.ts b/yarn-project/end-to-end/src/_e2e_persistence.test.ts similarity index 100% rename from yarn-project/end-to-end/src/e2e_persistence.test.ts rename to yarn-project/end-to-end/src/_e2e_persistence.test.ts diff --git a/yarn-project/end-to-end/src/e2e_sandbox_example.test.ts b/yarn-project/end-to-end/src/_e2e_sandbox_example.test.ts similarity index 100% rename from yarn-project/end-to-end/src/e2e_sandbox_example.test.ts rename to yarn-project/end-to-end/src/_e2e_sandbox_example.test.ts diff --git a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts index 90348fa8d28b..a00584b76adf 100644 --- a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts +++ b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts @@ -22,12 +22,8 @@ import { } from '@aztec/noir-contracts.js'; import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; -import { jest } from '@jest/globals'; - import { type BalancesFn, expectMapping, getBalancesFn, publicDeployAccounts, setup } from './fixtures/utils.js'; -jest.setTimeout(100_000); - const TOKEN_NAME = 'BananaCoin'; const TOKEN_SYMBOL = 'BC'; const TOKEN_DECIMALS = 18n; @@ -66,7 +62,7 @@ describe('e2e_dapp_subscription', () => { let wallets: AccountWalletWithPrivateKey[]; let aztecNode: AztecNode; let deployL1ContractsValues: DeployL1Contracts; - ({ wallets, aztecNode, deployL1ContractsValues, logger, pxe } = await setup(3)); + ({ wallets, aztecNode, deployL1ContractsValues, logger, pxe } = await setup(3, {}, {}, true)); await publicDeployAccounts(wallets[0], wallets); diff --git a/yarn-project/end-to-end/src/e2e_fees.test.ts b/yarn-project/end-to-end/src/e2e_fees.test.ts index 7aed8e282ebf..ec85175bd1ab 100644 --- a/yarn-project/end-to-end/src/e2e_fees.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees.test.ts @@ -54,7 +54,7 @@ describe('e2e_fees', () => { let bananaPrivateBalances: BalancesFn; beforeAll(async () => { - const { wallets: _wallets, aztecNode, deployL1ContractsValues, logger, pxe } = await setup(3); + const { wallets: _wallets, aztecNode, deployL1ContractsValues, logger, pxe } = await setup(3, {}, {}, true); wallets = _wallets; await aztecNode.setConfig({ diff --git a/yarn-project/end-to-end/src/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..8efc7d88277e --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts @@ -0,0 +1,46 @@ +import { TokenContractTest } from './token_contract_test.js'; + +describe('e2e_token_contract access control', () => { + const t = new TokenContractTest('access_control'); + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.setup(); + }); + + afterAll(async () => { + await t.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + 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()); + }); + + 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('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); + }); + + 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..ff7aed370b55 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts @@ -0,0 +1,225 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { U128_UNDERFLOW_ERROR } from '../fixtures/index.js'; +import { TokenContractTest } from './token_contract_test.js'; + +describe('e2e_token_contract burn', () => { + const t = new TokenContractTest('burn'); + let { asset, accounts, tokenSim, wallets } = t; + + beforeAll(async () => { + 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.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + 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(); + + 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 '); + }); + + 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 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 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).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(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).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 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).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); + + // 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).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()}`, + ); + }); + + 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 new file mode 100644 index 000000000000..2fa48998dcb3 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts @@ -0,0 +1,131 @@ +import { Fr, type TxHash, computeMessageSecretHash } from '@aztec/aztec.js'; + +import { BITSIZE_TOO_BIG_ERROR, U128_OVERFLOW_ERROR } from '../fixtures/fixtures.js'; +import { TokenContractTest } from './token_contract_test.js'; + +describe('e2e_token_contract minting', () => { + const t = new TokenContractTest('minting'); + let { asset, accounts, tokenSim, wallets } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.setup(); + ({ asset, accounts, tokenSim, wallets } = t); + }); + + afterAll(async () => { + await t.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + 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).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 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..43762d88e263 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts @@ -0,0 +1,115 @@ +import { ReaderContract } from '@aztec/noir-contracts.js'; + +import { TokenContractTest } from './token_contract_test.js'; + +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 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.applyBaseSnapshots(); + + await t.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}.`); + }, + ); + + await t.setup(); + }); + + afterAll(async () => { + await t.teardown(); + }); + + beforeEach(async () => {}); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + 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)', + ); + }); + + 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 new file mode 100644 index 000000000000..99fcd3c1336d --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/sheilding.test.ts @@ -0,0 +1,130 @@ +import { Fr, computeMessageSecretHash } from '@aztec/aztec.js'; + +import { U128_UNDERFLOW_ERROR } from '../fixtures/fixtures.js'; +import { TokenContractTest } from './token_contract_test.js'; + +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.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.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + 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); + }); + + 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); + + // 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).simulate(); + 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).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 (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); + + // 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).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(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).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/token_contract_test.ts b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts new file mode 100644 index 000000000000..f32e3b4536fb --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts @@ -0,0 +1,180 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { + type AccountWallet, + type CompleteAddress, + type DebugLogger, + ExtendedNote, + Fr, + Note, + type TxHash, + computeMessageSecretHash, + createDebugLogger, +} from '@aztec/aztec.js'; +import { DocsExampleContract, TokenContract } from '@aztec/noir-contracts.js'; + +import { + SnapshotManager, + type SubsystemsContext, + addAccounts, + publicDeployAccounts, +} from '../fixtures/snapshot_manager.js'; +import { TokenSimulator } from '../simulators/token_simulator.js'; + +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; + wallets: AccountWallet[] = []; + accounts: CompleteAddress[] = []; + asset!: TokenContract; + tokenSim!: TokenSimulator; + badAccount!: DocsExampleContract; + + constructor(testName: string) { + this.logger = createDebugLogger(`aztec:e2e_token_contract:${testName}`); + this.snapshotManager = new SnapshotManager(`e2e_token_contract/${testName}`, dataPath); + } + + /** + * Adds two state shifts to snapshot manager. + * 1. Add 3 accounts. + * 2. Publicly deploy accounts, deploy token contract and a "bad account". + */ + 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())); + this.accounts = await pxe.getRegisteredAccounts(); + this.wallets.forEach((w, i) => this.logger(`Wallet ${i} address: ${w.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], + TokenContractTest.TOKEN_NAME, + TokenContractTest.TOKEN_SYMBOL, + TokenContractTest.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 address: ${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 address: ${this.badAccount.address}`); + + expect(await this.asset.methods.admin().simulate()).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 setup() { + await this.snapshotManager.setup(); + } + + 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, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, + txHash, + ); + await this.wallets[accountIndex].addNote(extendedNote); + } + + async applyMintSnapshot() { + await this.snapshotManager.snapshot( + 'mint', + 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(); + + 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: [{ 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(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/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..3251c7422a97 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_private.test.ts @@ -0,0 +1,231 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { TokenContractTest } from './token_contract_test.js'; + +describe('e2e_token_contract transfer private', () => { + const t = new TokenContractTest('transfer_private'); + let { asset, accounts, tokenSim, wallets, badAccount } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.applyMintSnapshot(); + await t.setup(); + ({ asset, accounts, tokenSim, wallets, badAccount } = t); + }); + + afterAll(async () => { + await t.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + 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); + }); + + 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 new file mode 100644 index 000000000000..13430c1916aa --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_public.test.ts @@ -0,0 +1,270 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { U128_UNDERFLOW_ERROR } from '../fixtures/fixtures.js'; +import { TokenContractTest } from './token_contract_test.js'; + +describe('e2e_token_contract transfer public', () => { + const t = new TokenContractTest('transfer_public'); + let { asset, accounts, tokenSim, wallets, badAccount } = t; + + beforeAll(async () => { + 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.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + 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); + }); + + 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(); + + 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).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(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).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); + }); + + 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'); + }); + + 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) + .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 new file mode 100644 index 000000000000..998b978e0810 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_token_contract/unsheilding.test.ts @@ -0,0 +1,129 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { TokenContractTest } from './token_contract_test.js'; + +describe('e2e_token_contract unshielding', () => { + const t = new TokenContractTest('unshielding'); + let { asset, accounts, tokenSim, wallets } = t; + + beforeAll(async () => { + 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.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + 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); + }); + + 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 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); + + 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).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 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).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/get_acvm_config.ts b/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts new file mode 100644 index 000000000000..e89585341ec3 --- /dev/null +++ b/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts @@ -0,0 +1,46 @@ +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 : `../../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_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/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts new file mode 100644 index 000000000000..d28e03966675 --- /dev/null +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -0,0 +1,320 @@ +import { SchnorrAccountContractArtifact, getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; +import { + type AztecAddress, + BatchCall, + type CompleteAddress, + type DebugLogger, + EthCheatCodes, + GrumpkinPrivateKey, + type Wallet, +} from '@aztec/aztec.js'; +import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; +import { asyncMap } from '@aztec/foundation/async-map'; +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'; +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 type SubsystemsContext = { + anvil: Anvil; + acvmConfig: any; + aztecNode: AztecNodeService; + aztecNodeConfig: AztecNodeConfig; + pxe: PXEService; +}; + +type SnapshotEntry = { + name: string; + apply: (context: SubsystemsContext) => Promise; + restore: (snapshotData: any, context: SubsystemsContext) => Promise; + snapshotPath: string; +}; + +export class SnapshotManager { + private snapshotStack: SnapshotEntry[] = []; + private context?: SubsystemsContext; + private livePath: string; + private logger: DebugLogger; + + 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(), + ) { + 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)) { + // 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 didn't exist at snapshotPath, and by definition none of the child snapshots can exist. + + if (!this.context) { + // We have no subsystem context yet, create it from the top of the snapshot stack (if it exists). + this.context = await this.setup(); + } + + this.snapshotStack.push({ name, apply, restore, snapshotPath }); + + // Apply current state transition. + 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); + + // 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 || {}, 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}...`); + 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(); + } + } + + /** + * 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 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 }); + 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(`Restoration of ${e.name} complete.`); + }); + } else { + this.context = await this.setupFromFresh(this.livePath); + } + } + return this.context; + } + + /** + * Destroys the current subsystem context. + */ + 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; + removeSync(this.livePath); + } + + /** + * 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 = statePath; + + // 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(); + + // 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 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 = statePath; + const pxe = await createPXEService(aztecNode, pxeConfig); + + if (statePath) { + writeFileSync(`${statePath}/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`; + 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); + + this.logger('Creating pxe...'); + const pxeConfig = getPXEServiceConfig(); + pxeConfig.dataDirectory = statePath; + const pxe = await createPXEService(aztecNode, pxeConfig); + + return { + aztecNodeConfig, + anvil, + aztecNode, + pxe, + acvmConfig, + }; + } +} + +/** + * 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, logger: DebugLogger) => + async ({ pxe }: SubsystemsContext) => { + // Generate account keys. + const accountKeys: [GrumpkinPrivateKey, GrumpkinPrivateKey][] = Array.from({ length: numberOfAccounts }).map(_ => [ + GrumpkinPrivateKey.random(), + GrumpkinPrivateKey.random(), + ]); + + 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.prove({ + contractAddressSalt: account.salt, + skipClassRegistration: true, + skipPublicDeployment: true, + universalDeploy: true, + }), + ); + return account; + }); + + 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 }; + }; + +/** + * 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/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 39b4fba0df9d..1faa9ac49512 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -78,7 +78,6 @@ const { TEMP_DIR = '/tmp', ACVM_BINARY_PATH = '', ACVM_WORKING_DIRECTORY = '', - ENABLE_GAS = '', } = process.env; const getAztecUrl = () => { @@ -222,6 +221,7 @@ async function setupWithRemoteEnvironment( config: AztecNodeConfig, logger: DebugLogger, numberOfAccounts: number, + enableGas: boolean, ) { // we are setting up against a remote environment, l1 contracts are already deployed const aztecNodeUrl = getAztecUrl(); @@ -259,7 +259,7 @@ async function setupWithRemoteEnvironment( const cheatCodes = CheatCodes.create(config.rpcUrl, pxeClient!); const teardown = () => Promise.resolve(); - if (['1', 'true'].includes(ENABLE_GAS)) { + if (enableGas) { // this contract might already have been deployed // the following function is idempotent await deployCanonicalGasToken(new SignerlessWallet(pxeClient, new DefaultMultiCallEntrypoint())); @@ -322,6 +322,7 @@ export async function setup( numberOfAccounts = 1, opts: SetupOptions = {}, pxeOpts: Partial = {}, + enableGas = false, ): Promise { const config = { ...getConfigEnvVars(), ...opts }; @@ -354,7 +355,7 @@ export async function setup( if (PXE_URL) { // we are setting up against a remote environment, l1 contracts are assumed to already be deployed - return await setupWithRemoteEnvironment(hdAccount, config, logger, numberOfAccounts); + return await setupWithRemoteEnvironment(hdAccount, config, logger, numberOfAccounts, enableGas); } const deployL1ContractsValues = @@ -374,10 +375,13 @@ export async function setup( const aztecNode = await AztecNodeService.createAndSync(config); const sequencer = aztecNode.getSequencer(); + logger('Creating a pxe...'); const { pxe, wallets } = await setupPXEService(numberOfAccounts, aztecNode!, pxeOpts, logger); - if (['1', 'true'].includes(ENABLE_GAS)) { + if (enableGas) { + logger(`Deploying canonical gas token...`); await deployCanonicalGasToken(new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint())); + logger(`Done.`); } const cheatCodes = CheatCodes.create(config.rpcUrl, pxe!); diff --git a/yarn-project/foundation/src/aztec-address/index.ts b/yarn-project/foundation/src/aztec-address/index.ts index 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..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); @@ -257,8 +258,18 @@ export class Fr extends BaseField { return new Fr(this.toBigInt() / rhs.toBigInt()); } + + toJSON() { + return { + type: 'Fr', + value: this.toString(), + }; + } } +// For deserializing JSON. +TypeRegistry.register('Fr', Fr); + /** * Branding to ensure fields are not interchangeable types. */ @@ -319,8 +330,18 @@ 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(), + }; + } } +// For deserializing JSON. +TypeRegistry.register('Fq', Fq); + // 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..85146710ed8c --- /dev/null +++ b/yarn-project/foundation/src/serialize/type_registry.ts @@ -0,0 +1,43 @@ +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); + } +} + +// 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) { + return Constructor.fromString(value.value); + } + } + return value; +} 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: