Skip to content

Commit

Permalink
feat: e2e token contract can run in 2m with snapshots and test separa…
Browse files Browse the repository at this point in the history
…tion. (#5526)

Trying to run e2e tests makes one want to live a different life. They're
slow (for root reasons that will eventually be addressed). e2e tests
*ultimately* should be able to run in the "seconds" not "minutes" range.
One day we may have a foundry like solution that could give sub-second
state transitions, but that's a whole different ball game and won't
happen soon.

Developers should not be spending their time watching a minute of setup
time in an e2e test before it even hits their code. Our CI is also
horribly slow, and this PR could eventually help alleviate that. There
are two main things of note in this PR.

* It introduces a little snapshot engine for our e2e tests, that builds
upon our ability to have our own anvil per test.
* It splits our e2e_token_contract.test into 8 separate test files, and
a `TokenContractTest` class to capture common behaviour/state.

The second, is something we simply should be doing anyway. It's
independent of the snapshot engine. People have got into the habit of
rightward-drifting describe blocks into single e2e test files, but it's
a bad pattern because jest cannot parallelise within a single test file.
Better, for some of these larger e2e tests, would be to create a folder
named after the test and to split them up, getting rid of the
right-drifting describes, and having a nice flat test file. That's what
this PR does with the token contract.

Ignoring snapshotting, this get's running the e2e token contract tests
time down from 10m to about 3m. That's with every test file still doing
it's own ~1m of setup.

# Snapshot Engine

If you're a developer, and you're just wanting to run a part of your
tests, the turnaround time of waiting 1 minute before you even hit your
code is unacceptable. This PR introduces a small engine for reusing
state snapshots within, and across test runs. To enable the snapshot
engine, set e.g. `E2E_DATA_PATH=./data` before running tests. On the
first run you'll notice the `data` folder is created, this contains
snapshotted state. Subsequent runs leverage this to avoid having to do
setup again. Within the `data` folder you'll see two folders:
* `live`
* `snapshots`

The `live` folder contains subfolders scoped by test name, and reflects
the currently executing state of that test. We generate snapshots from
this live state, or restore the live state from snapshots.

The `snapshots` folder contains a tree of folders that reflects the
state snapshot tree. An example path here might be:
```
./data/snapshots/3_accounts/e2e_token_contract/mint/snapshot
```
This shows 3 snapshots building on each other.
* `3_accounts` added 3 accounts.
* `e2e_token_contract` did some specifics to that test (deployed a
token, bad account, etc).
* `mint` minted some funds, specific to tests that needed minted funds.

You can erase any level of the tree, and the next run will rebuild from
that point.

The `snapshot` function has the following signature.
```typescript
  public async snapshot<T>(
    name: string,
    apply: (context: SubsystemsContext) => Promise<T>,
    restore: (snapshotData: T, context: SubsystemsContext) => Promise<void> = () => Promise.resolve(),
  );
```
* `name`: name of the snapshot, eg `3_accounts`.
* `apply`: A function that actually does the state shift, e.g. deploying
the accounts. It should return an object that is JSON serialisable that
contains the necessary information to restore the state, e.g. the
account addresses.
* `restore`: A function that takes first argument the data that was
returned by `apply`. You can use this to restore live objects within the
test.

Originally I had the concept of "pushing" and "popping" snapshots. But
the popping aspect was a footgun, and would force use of the snapshot
engine. Now a single test file can build a stack of snapshots, but can't
pop. It's a simpler design and encourages creation of a new test file if
that test file wants to start from a different state. i.e. you can
imagine that a single test file sits at one of the nodes of the snapshot
tree.

If you have snapshot state, this gets running the contract tests down
from 3m to 2m. However the real win is the turnaround time when you're
iterating on a single test, which you can now run in sometimes as little
as 7s.
  • Loading branch information
charlielye authored Apr 10, 2024
1 parent e4d2df6 commit b0037dd
Show file tree
Hide file tree
Showing 35 changed files with 2,035 additions and 1,293 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/developers/tutorials/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 1 addition & 2 deletions docs/docs/developers/tutorials/writing_token_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions yarn-project/Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec.js/src/account_manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec.js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export {
Body,
CompleteAddress,
ExtendedNote,
FunctionCall,
type FunctionCall,
GrumpkinPrivateKey,
L1ToL2Message,
L1Actor,
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/circuit-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
`;

Expand Down Expand Up @@ -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",
}
`;
3 changes: 2 additions & 1 deletion yarn-project/circuits.js/src/types/grumpkin_private_key.ts
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 5 additions & 2 deletions yarn-project/end-to-end/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 && \
Expand Down
15 changes: 9 additions & 6 deletions yarn-project/end-to-end/Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion yarn-project/end-to-end/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -101,6 +102,7 @@
"node": ">=18"
},
"jest": {
"slowTestThreshold": 180,
"extensionsToTreatAsEsm": [
".ts"
],
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/package.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Loading

0 comments on commit b0037dd

Please sign in to comment.