diff --git a/.circleci/config.yml b/.circleci/config.yml index 7d46584e12d..04ff8c78cf2 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/docs/docs/developers/contracts/resources/common_patterns/authwit.md b/docs/docs/developers/contracts/resources/common_patterns/authwit.md index 10480a7de40..334eba3e240 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 45b4c8f623e..39763df867d 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/testing.md b/docs/docs/developers/tutorials/testing.md index 9e80aba1636..d3dd5c5fcfc 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. diff --git a/docs/docs/developers/tutorials/writing_token_contract.md b/docs/docs/developers/tutorials/writing_token_contract.md index 04d8b000823..a4f5926c5b5 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 diff --git a/yarn-project/Earthfile b/yarn-project/Earthfile index 75316122d4e..604bdeef8ed 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/aztec.js/src/account_manager/index.ts b/yarn-project/aztec.js/src/account_manager/index.ts index a8c96b6887f..43fb28a5b23 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/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index e7cc7c0ba20..d25d591a047 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -85,7 +85,7 @@ export { Body, CompleteAddress, ExtendedNote, - FunctionCall, + type FunctionCall, GrumpkinPrivateKey, L1ToL2Message, L1Actor, diff --git a/yarn-project/circuit-types/src/index.ts b/yarn-project/circuit-types/src/index.ts index 43ecbefb9e6..f507bdcee44 100644 --- a/yarn-project/circuit-types/src/index.ts +++ b/yarn-project/circuit-types/src/index.ts @@ -19,4 +19,4 @@ export * from './packed_arguments.js'; export * from './interfaces/index.js'; export * from './auth_witness.js'; export * from './aztec_node/rpc/index.js'; -export { CompleteAddress, PublicKey, PartialAddress, GrumpkinPrivateKey } from '@aztec/circuits.js'; +export { CompleteAddress, type PublicKey, type PartialAddress, GrumpkinPrivateKey } from '@aztec/circuits.js'; diff --git a/yarn-project/circuits.js/src/structs/__snapshots__/revert_code.test.ts.snap b/yarn-project/circuits.js/src/structs/__snapshots__/revert_code.test.ts.snap index 88a46bd073b..fff7e680ed0 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", } `; diff --git a/yarn-project/circuits.js/src/types/grumpkin_private_key.ts b/yarn-project/circuits.js/src/types/grumpkin_private_key.ts index cb96ce1cd8c..fa2e53e3b9d 100644 --- a/yarn-project/circuits.js/src/types/grumpkin_private_key.ts +++ b/yarn-project/circuits.js/src/types/grumpkin_private_key.ts @@ -1,4 +1,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/Dockerfile b/yarn-project/end-to-end/Dockerfile index 52bd00f142e..215a0fb1f97 100644 --- a/yarn-project/end-to-end/Dockerfile +++ b/yarn-project/end-to-end/Dockerfile @@ -5,14 +5,13 @@ 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 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 @@ -38,6 +37,10 @@ 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 && cp /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 -y && \ diff --git a/yarn-project/end-to-end/Earthfile b/yarn-project/end-to-end/Earthfile index d9b8aaef243..322f2caa529 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/package.json b/yarn-project/end-to-end/package.json index 26726a633dd..b5df23580ee 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -15,7 +15,7 @@ "clean": "rm -rf ./dest .tsbuildinfo", "formatting": "run -T prettier --check ./src \"!src/web/main.js\" && run -T eslint ./src", "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src", - "test": "LOG_LEVEL=${LOG_LEVEL:-verbose} NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --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": 180, "extensionsToTreatAsEsm": [ ".ts" ], diff --git a/yarn-project/end-to-end/package.local.json b/yarn-project/end-to-end/package.local.json index 8f1316f3328..d7161c03659 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" } } 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 01e3b683488..00000000000 --- 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()}`, - ); - }); - }); - }); - }); -}); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts new file mode 100644 index 00000000000..8efc7d88277 --- /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 00000000000..ff7aed370b5 --- /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 00000000000..2fa48998dcb --- /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 00000000000..5327ca15188 --- /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.verbose('Deploying ReaderContract...'); + const reader = await ReaderContract.deploy(t.wallets[0]).send().deployed(); + logger.verbose(`Deployed ReaderContract to ${reader.address}.`); + return { readerAddress: reader.address }; + }, + async ({ readerAddress }) => { + reader = await ReaderContract.at(readerAddress, t.wallets[0]); + logger.verbose(`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 00000000000..99fcd3c1336 --- /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 00000000000..9e6fb3f110c --- /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.verbose(`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.verbose(`Public deploy accounts...`); + await publicDeployAccounts(this.wallets[0], this.accounts.slice(0, 2)); + + this.logger.verbose(`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.verbose(`Token deployed to ${asset.address}`); + + this.logger.verbose(`Deploying bad account...`); + this.badAccount = await DocsExampleContract.deploy(this.wallets[0]).send().deployed(); + 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.verbose(`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.verbose(`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.verbose(`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.verbose(`Minting ${amount} publicly...`); + await asset.methods.mint_public(accounts[0].address, amount).send().wait(); + + 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(); + + 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.verbose(`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.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.verbose(`Private balance of wallet 0: ${privateBalance}`); + expect(privateBalance).toEqual(tokenSim.balanceOfPrivate(address)); + + const totalSupply = await asset.methods.total_supply().simulate(); + this.logger.verbose(`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 00000000000..3251c7422a9 --- /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 00000000000..13430c1916a --- /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 00000000000..998b978e081 --- /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 00000000000..55672641113 --- /dev/null +++ b/yarn-project/end-to-end/src/fixtures/get_acvm_config.ts @@ -0,0 +1,43 @@ +import { type DebugLogger } from '@aztec/aztec.js'; +import { randomBytes } from '@aztec/foundation/crypto'; + +import * as fs from 'fs/promises'; + +export { deployAndInitializeTokenAndBridgeContracts } from '../shared/cross_chain_test_harness.js'; + +const { + NOIR_RELEASE_DIR = 'noir-repo/target/release', + TEMP_DIR = '/tmp', + ACVM_BINARY_PATH = '', + ACVM_WORKING_DIRECTORY = '', +} = 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.verbose(`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.verbose(`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 00000000000..1a3d3b35128 --- /dev/null +++ b/yarn-project/end-to-end/src/fixtures/setup_l1_contracts.ts @@ -0,0 +1,87 @@ +import { + type DebugLogger, + type DeployL1Contracts, + type L1ContractArtifactsForDeployment, + deployL1Contracts, +} from '@aztec/aztec.js'; +import { + AvailabilityOracleAbi, + AvailabilityOracleBytecode, + GasPortalAbi, + GasPortalBytecode, + InboxAbi, + InboxBytecode, + OutboxAbi, + OutboxBytecode, + PortalERC20Abi, + PortalERC20Bytecode, + RegistryAbi, + RegistryBytecode, + RollupAbi, + RollupBytecode, +} from '@aztec/l1-artifacts'; +import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; + +import { type HDAccount, type PrivateKeyAccount, getContract } from 'viem'; +import { foundry } from 'viem/chains'; + +export { deployAndInitializeTokenAndBridgeContracts } from '../shared/cross_chain_test_harness.js'; + +export const setupL1Contracts = async ( + l1RpcUrl: string, + account: HDAccount | PrivateKeyAccount, + logger: DebugLogger, +) => { + const l1Artifacts: L1ContractArtifactsForDeployment = { + registry: { + contractAbi: RegistryAbi, + contractBytecode: RegistryBytecode, + }, + inbox: { + contractAbi: InboxAbi, + contractBytecode: InboxBytecode, + }, + outbox: { + contractAbi: OutboxAbi, + contractBytecode: OutboxBytecode, + }, + availabilityOracle: { + contractAbi: AvailabilityOracleAbi, + contractBytecode: AvailabilityOracleBytecode, + }, + rollup: { + contractAbi: RollupAbi, + contractBytecode: RollupBytecode, + }, + gasToken: { + contractAbi: PortalERC20Abi, + contractBytecode: PortalERC20Bytecode, + }, + gasPortal: { + contractAbi: GasPortalAbi, + contractBytecode: GasPortalBytecode, + }, + }; + + const l1Data = await deployL1Contracts(l1RpcUrl, account, foundry, logger, l1Artifacts); + await initGasBridge(l1Data); + + return l1Data; +}; + +async function initGasBridge({ walletClient, l1ContractAddresses }: DeployL1Contracts) { + const gasPortal = getContract({ + address: l1ContractAddresses.gasPortalAddress.toString(), + abi: GasPortalAbi, + client: walletClient, + }); + + await gasPortal.write.initialize( + [ + l1ContractAddresses.registryAddress.toString(), + l1ContractAddresses.gasTokenAddress.toString(), + getCanonicalGasTokenAddress(l1ContractAddresses.gasPortalAddress).toString(), + ], + {} as any, + ); +} diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts new file mode 100644 index 00000000000..f5536ea1cdd --- /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.verbose(`No data path given, will not persist any snapshots.`); + this.context = await this.setupFromFresh(); + this.logger.verbose(`Applying state transition for ${name}...`); + const snapshotData = await apply(this.context); + this.logger.verbose(`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.verbose(`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.verbose(`Applying state transition for ${name}...`); + const snapshotData = await apply(this.context); + this.logger.verbose(`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.verbose(`Saving snapshot to ${snapshotPath}...`); + if (mkdirSync(snapshotPath, { recursive: true })) { + copySync(this.livePath, snapshotPath); + this.logger.verbose(`Snapshot copied to ${snapshotPath}.`); + } else { + this.logger.verbose(`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.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.verbose(`Executing restoration function for ${e.name}...`); + await e.restore(snapshotData, this.context!); + this.logger.verbose(`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.verbose(`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.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.verbose('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.verbose('Creating and synching an aztec node...'); + const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); + + this.logger.verbose('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.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. + 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.verbose('Creating aztec node...'); + const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); + + this.logger.verbose('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.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 + // 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.verbose('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 c4938e4376e..e9fcce89357 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.verbose('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 c932c10cdaa..e97fb1f1794 100644 --- a/yarn-project/foundation/src/aztec-address/index.ts +++ b/yarn-project/foundation/src/aztec-address/index.ts @@ -2,6 +2,7 @@ import { inspect } from 'util'; import { Fr, fromBuffer } from '../fields/index.js'; import { type BufferReader, FieldReader } from '../serialize/index.js'; +import { TypeRegistry } from '../serialize/type_registry.js'; /** * AztecAddress represents a 32-byte address in the Aztec Protocol. @@ -53,4 +54,14 @@ export class AztecAddress extends Fr { static random() { return new AztecAddress(super.random().toBuffer()); } + + toJSON() { + return { + type: 'AztecAddress', + value: this.toString(), + }; + } } + +// For deserializing JSON. +TypeRegistry.register('AztecAddress', AztecAddress); diff --git a/yarn-project/foundation/src/eth-address/index.ts b/yarn-project/foundation/src/eth-address/index.ts index f61c1f75b4f..5f28e59f54a 100644 --- a/yarn-project/foundation/src/eth-address/index.ts +++ b/yarn-project/foundation/src/eth-address/index.ts @@ -4,6 +4,7 @@ import { keccak256String } from '../crypto/keccak/index.js'; import { randomBytes } from '../crypto/random/index.js'; import { Fr } from '../fields/index.js'; import { BufferReader, FieldReader } from '../serialize/index.js'; +import { TypeRegistry } from '../serialize/type_registry.js'; /** * Represents an Ethereum address as a 20-byte buffer and provides various utility methods @@ -236,4 +237,14 @@ export class EthAddress { toFriendlyJSON() { return this.toString(); } + + toJSON() { + return { + type: 'EthAddress', + value: this.toString(), + }; + } } + +// For deserializing JSON. +TypeRegistry.register('EthAddress', EthAddress); diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index a28018638f4..bc180fa9ecb 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -3,6 +3,7 @@ import { inspect } from 'util'; import { toBigIntBE, toBufferBE } from '../bigint-buffer/index.js'; import { randomBytes } from '../crypto/random/index.js'; import { BufferReader } from '../serialize/buffer_reader.js'; +import { TypeRegistry } from '../serialize/type_registry.js'; const ZERO_BUFFER = Buffer.alloc(32); @@ -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 875d37f4410..33cb9a5bb4d 100644 --- a/yarn-project/foundation/src/serialize/index.ts +++ b/yarn-project/foundation/src/serialize/index.ts @@ -3,3 +3,4 @@ export * from './buffer_reader.js'; export * from './field_reader.js'; export * from './types.js'; export * from './serialize.js'; +export * from './type_registry.js'; diff --git a/yarn-project/foundation/src/serialize/type_registry.ts b/yarn-project/foundation/src/serialize/type_registry.ts new file mode 100644 index 00000000000..85146710ed8 --- /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 722f749f9e6..b251c25c6b7 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -404,6 +404,7 @@ __metadata: buffer: ^6.0.3 concurrently: ^7.6.0 crypto-browserify: ^3.12.0 + fs-extra: ^11.2.0 get-port: ^7.1.0 glob: ^10.3.10 jest: ^29.5.0 @@ -7254,7 +7255,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^11.1.1": +"fs-extra@npm:^11.1.1, fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" dependencies: