From 4ca19fb901761e0cc4dada885c69d657b48998f6 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Mon, 2 Nov 2026 17:46:08 +0000 Subject: [PATCH 01/32] fix(docs): default #include_aztec_version to the pinned aztec-node tag Without COMMIT_TAG (previews, local builds) the legacy macros resolved to next and latest. aztec-node has no next ref, so 38 source links and the aztec-nr Nargo tags pointed nowhere. They now take the ref docs/aztec-node.json pins. Release cuts set COMMIT_TAG and are unchanged. Three @aztec-foundation installs used the Labs macro and named the Labs version in every build; they now use #include_foundation_version_without_prefix. --- .../contract_tutorials/recursive_verification.md | 4 ++-- .../docs/tutorials/js_tutorials/token_bridge.md | 2 +- docs/src/preprocess/__tests__/include_version.test.js | 11 +++++++++++ docs/src/preprocess/include_version.js | 9 +++++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md b/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md index d4006262f4e..491fe768171 100644 --- a/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md +++ b/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md @@ -397,10 +397,10 @@ Create the following files in your project root directory. "dependencies": { "@aztec-labs/accounts": "#include_version_without_prefix", "@aztec-labs/aztec.js": "#include_version_without_prefix", - "@aztec-foundation/bb.js": "#include_version_without_prefix", + "@aztec-foundation/bb.js": "#include_foundation_version_without_prefix", "@aztec-labs/kv-store": "#include_version_without_prefix", "@aztec-labs/noir-contracts.js": "#include_version_without_prefix", - "@aztec-foundation/noir-noir_js": "#include_version_without_prefix", + "@aztec-foundation/noir-noir_js": "#include_foundation_version_without_prefix", "@aztec-labs/pxe": "#include_version_without_prefix", "@aztec-labs/wallets": "#include_version_without_prefix", "tsx": "^4.20.6" diff --git a/docs/docs-developers/docs/tutorials/js_tutorials/token_bridge.md b/docs/docs-developers/docs/tutorials/js_tutorials/token_bridge.md index 3c3ef674460..6d5225859db 100644 --- a/docs/docs-developers/docs/tutorials/js_tutorials/token_bridge.md +++ b/docs/docs-developers/docs/tutorials/js_tutorials/token_bridge.md @@ -47,7 +47,7 @@ Second, replace the starter's `@aztec/l1-contracts` git dependency, whose reposi ```bash npm pkg delete dependencies.@aztec/l1-contracts devDependencies.@aztec/l1-contracts -npm pkg set "dependencies.@aztec-foundation/l1-artifacts=#include_version_without_prefix" +npm pkg set "dependencies.@aztec-foundation/l1-artifacts=#include_foundation_version_without_prefix" npm pkg set scripts.postinstall="node -e \"require('fs').writeFileSync('node_modules/@aztec-foundation/l1-artifacts/remappings.txt', '@oz/=l1-contracts/lib/openzeppelin-contracts/contracts/\\n@aztec/=l1-contracts/src/\\n@aztec-blob-lib/=l1-contracts/src/mock/libraries/\\n@zkpassport/=l1-contracts/lib/circuits/src/solidity/src/\\n')\"" ``` diff --git a/docs/src/preprocess/__tests__/include_version.test.js b/docs/src/preprocess/__tests__/include_version.test.js index 124baf745d0..67122ab1aa9 100644 --- a/docs/src/preprocess/__tests__/include_version.test.js +++ b/docs/src/preprocess/__tests__/include_version.test.js @@ -109,6 +109,17 @@ describe("preprocessIncludeVersion", () => { assert.strictEqual(content, "1.2.3"); }); + it("without COMMIT_TAG, names the pinned aztec-node tag, independent of NIGHTLY_TAG", async () => { + const { ref } = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../aztec-node.json"), "utf8")); + setEnv({ RELEASE_TYPE: "nightly", NIGHTLY_TAG: "v9.9.9-nightly.1" }); + delete process.env.COMMIT_TAG; + const { content } = await preprocessIncludeVersion( + "#include_aztec_version #include_version_without_prefix #release_version", + "test.md", + ); + assert.strictEqual(content, `${ref} ${ref.slice(1)} 9.9.9-nightly.1`); + }); + const versionMacros = [ ["#include_devnet_version", "DEVNET_TAG", "3.0.0-devnet.5"], ["#include_testnet_version", "TESTNET_TAG", "2.1.11"], diff --git a/docs/src/preprocess/include_version.js b/docs/src/preprocess/include_version.js index f4866333de3..79d584aa99d 100644 --- a/docs/src/preprocess/include_version.js +++ b/docs/src/preprocess/include_version.js @@ -76,6 +76,10 @@ function foundationVersion() { return `v${JSON.parse(fs.readFileSync(pkg, "utf8")).version}`; } +function pinnedAztecNodeRef() { + return JSON.parse(fs.readFileSync(path.join(__dirname, "../../aztec-node.json"), "utf8")).ref; +} + /** * Preprocesses markdown content to replace version macros and conditional blocks. * @param {string} markdownContent - The markdown content to process @@ -94,8 +98,9 @@ async function preprocessIncludeVersion(markdownContent, filePath = "unknown") { const testnetTag = process.env.TESTNET_TAG || stripV(developerVersionConfig.testnet) || "0.0.0"; const mainnetTag = process.env.MAINNET_TAG || stripV(developerVersionConfig.mainnet) || "0.0.0"; const releaseType = process.env.RELEASE_TYPE || "mainnet"; - // COMMIT_TAG kept for backwards compatibility - const commitTag = process.env.COMMIT_TAG || "next"; + // COMMIT_TAG kept for backwards compatibility. Release cuts set it to the Labs tag; any other build + // names the pinned aztec-node tag, the code the pages quote. + const commitTag = process.env.COMMIT_TAG || pinnedAztecNodeRef(); // Validate release type if (!VALID_RELEASE_TYPES.includes(releaseType)) { From 93d3e32ae06e27787c0fd207822f702a71a5a9e1 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Mon, 2 Nov 2026 17:46:13 +0000 Subject: [PATCH 02/32] docs: follow aztec-node main for dev docs, v6 tags for v6.x cuts From 2026-11-04 aztec-node carries v7 on main and v6 on a v6 branch, with hand backports. The dev pin moves to the newest nightly of main's major and is checked to be on main; release cuts pin their tag. The release skills now find hand backports in migration-note triage (TBD and Unreleased) and expect adapted backports on both sides of the content check. update-doc-references reports, without editing, v6 PRs the pin lacks; updating-changelog takes a PR's major from its base. networks.md and support.md no longer name a development branch or aztec-node's nonexistent next. --- .claude/skills/release-docs/SKILL.md | 34 +++++++++++++------ .claude/skills/release-network-docs/SKILL.md | 4 ++- .claude/skills/update-doc-references/SKILL.md | 15 ++++++++ .claude/skills/updating-changelog/SKILL.md | 8 +++-- docs/CLAUDE.md | 7 ++-- docs/README.md | 5 +-- docs/docs-developers/support.md | 2 +- docs/docs/networks.md | 2 +- 8 files changed, 56 insertions(+), 21 deletions(-) diff --git a/.claude/skills/release-docs/SKILL.md b/.claude/skills/release-docs/SKILL.md index c8b0fa70a8b..2b186661ccd 100644 --- a/.claude/skills/release-docs/SKILL.md +++ b/.claude/skills/release-docs/SKILL.md @@ -48,7 +48,9 @@ paste the key into the conversation or onto a command line. `BB` once provisioned). In the cut that is the release tag's. - **The cut runs in its own worktree** (`$CUT`), created from the destination commit; it moves the cut's pin to the Labs tag. The destination checkout is not touched until Step 17, - and its pin never moves: the destination's dev docs keep following their own pin. + and its pin never moves: the destination's dev docs keep following their own pin, on + aztec-node `main`. A v6.x cut pins its tag on aztec-node's `v6` branch, so the cut and + the destination can sit on different lines (Step 12). - **Release inputs**, recorded in the commit that lands the cut (Step 17): | Input | Source | @@ -493,13 +495,14 @@ aztec-node has neither ref: replace both fetches with one dated by that tag here git -C "$AZTEC_NODE_DIR" fetch --shallow-since="$(git -C "$DEST_ROOT" log -1 --format=%cI v)" origin tag "$LABS_TAG" ``` -1. **Triage existing TBD items.** Not every item under `## TBD` belongs to this release. - For each entry decide whether it: +1. **Triage existing TBD and Unreleased items.** Not every item under `## TBD` belongs to + this release, and an item under `## Unreleased (...)` can ship early in a release line + by hand backport. For each entry decide whether it: - shipped in this release → move it under the new `## ` heading; - targets a future major → move it under `## Unreleased (v)` (create it between `## TBD` and the new version heading); - - is still TBD → leave it. + - is still TBD, or still unreleased → leave it. Present the proposed triage to the user for confirmation before rearranging. @@ -512,7 +515,12 @@ git -C "$AZTEC_NODE_DIR" fetch --shallow-since="$(git -C "$DEST_ROOT" log -1 --f ``` An item whose commit is not an ancestor of the tag has not shipped, even if it is on - aztec-node `main`: leave it under `## TBD`. In one rehearsal 4 of 21 items failed this + aztec-node `main`: leave it where it is. The exception is a hand backport. aztec-node + backports between `main` and a release line (`v6`) by hand, so the commit that shipped + is not the one the entry describes. A merged backport PR is only a lead + (`gh pr list --repo aztec-labs-eng/aztec-node --base v6 --state merged --search ''`): + test its merge commit (`gh pr view <n> --repo aztec-labs-eng/aztec-node --json mergeCommit -q .mergeCommit.oid`) + the same way, or use the syntax test below. In one rehearsal 4 of 21 items failed this test. A commit outside the fetched history prints `fatal: Not a valid commit name` before `NOT-in-tag`: that is unknown, not absent. Ask GitHub instead, which needs no history: `gh api "repos/aztec-labs-eng/aztec-node/compare/<commit>...$LABS_TAG" --jq .status` @@ -825,11 +833,11 @@ grep -rhoE 'github\.com/AztecProtocol/aztec-packages(/(blob|tree)/[^/)"]+(/[^/)" sort | uniq -c ``` -- The first grep must print nothing. Without `COMMIT_TAG`, `#include_aztec_version` - resolves to `next` (a ref `aztec-labs-eng/aztec-nr` lacks, and not an npm dist-tag) and - `#include_version_without_prefix` to `latest` (an older nightly at the v6 rehearsal, - wire-incompatible with the rc). The build validates neither. Both spacings of - `tag = "next"` occur, hence the pattern. +- The first grep must print nothing. A hit is a hand-written moving version: `next` (a ref + `aztec-labs-eng/aztec-nr` lacks, and not an npm dist-tag) or `latest` (an older nightly + at the v6 rehearsal, wire-incompatible with the rc). The macros no longer produce them: + without `COMMIT_TAG` they take the pin's `ref`, the tag in the cut. The build validates + neither. Both spacings of `tag = "next"` occur, hence the pattern. - The second lists every link, clone and Nargo `git` dependency into this repository with its ref and the start of its path (a clone's path is in the page's next `cd`). Hits under `processed-docs/docs-participate/` and `processed-docs/docs/` are unversioned and never @@ -962,7 +970,11 @@ git -C "$AZTEC_NODE_HISTORY" log --oneline --left-right --cherry-pick --name-onl ``` Skip this step when the two refs name the same commit. `--cherry-pick` drops commits that -reached both sides as ports. The history goes into a checkout of its own, fetched afresh +reached both sides as patch-equivalent ports. A hand backport adapted to its line is not +patch-equivalent and shows on both sides: judge it against `$AZTEC_NODE_DIR`, the tag. A +v6.x cut against a destination pinned to a v7 nightly lists every v7 commit since `v6` +left `main` as `>`. When `docs/examples/` already use v7-only APIs, Step 15 fails for the +cut and stops: no snapshot edit can fix the examples. The history goes into a checkout of its own, fetched afresh down to the refs' merge base, which GitHub names without local history; fetching it into `$AZTEC_NODE_DIR` instead keeps that checkout's earlier shallow boundaries (the merge base goes missing) and shortens the history Step 8 fetched. The check compares the merge base diff --git a/.claude/skills/release-network-docs/SKILL.md b/.claude/skills/release-network-docs/SKILL.md index ce07f55bd08..eadd2dd5a5d 100644 --- a/.claude/skills/release-network-docs/SKILL.md +++ b/.claude/skills/release-network-docs/SKILL.md @@ -471,7 +471,9 @@ Never fetch with `--shallow-exclude=<previous release>`: the merge base is usual from it, so it is cut off and the range lists both sides back to the boundary (v6.0.0 against `v6.0.0-rc.3`: 166/166 commits instead of 57/2). This fetch needs no previous release, so it also works when that is not a Labs tag. The separate history checkout and -a failed count check work as in `/release-docs` Step 12. +a failed count check work as in `/release-docs` Step 12. So do hand backports: one adapted +to its line (`v6`) is not patch-equivalent, so `--cherry-pick` keeps it on both sides; +judge it against `$AZTEC_NODE_DIR`, the tag. `>` commits are in the destination's pin but not in the release. Start with the operator pages the second log names (aztec-node held the docs until they moved here, so a code diff --git a/.claude/skills/update-doc-references/SKILL.md b/.claude/skills/update-doc-references/SKILL.md index 903c8b6becd..566e171a6e0 100644 --- a/.claude/skills/update-doc-references/SKILL.md +++ b/.claude/skills/update-doc-references/SKILL.md @@ -40,6 +40,21 @@ gh pr diff <PR_NUMBER> --repo <REPO> An aztec-node PR names paths from aztec-node's root: prefix them with `labs/` to compare them with `references`. +The source docs follow aztec-node `main`. An aztec-node PR into a release line +(`baseRefName` `v6`) changes code the pin may never get. Test its merge commit against the +pin (an unmerged PR is not in it): + +```bash +cd "$(git rev-parse --show-toplevel)" +merge=$(gh pr view <PR_NUMBER> --repo aztec-labs-eng/aztec-node --json mergeCommit -q .mergeCommit.oid) +pin=$(node -p 'require("./docs/aztec-node.json").commit') +gh api "repos/aztec-labs-eng/aztec-node/compare/$merge...$pin" --jq .status +``` + +`ahead` or `identical` means the pin has it: continue as for any PR. Otherwise edit no +source docs, since they describe `main`. Still run Steps 3 and 4, and report what the +docs of the line's next release would need; that release's cut (`/release-docs`) decides. + When the change is a move of the docs' pin rather than a PR (`node docs/scripts/aztec_node.mts pin <tag>`), take it from the two pin commits, fetched into the checkout. A two-commit diff needs no history between them: diff --git a/.claude/skills/updating-changelog/SKILL.md b/.claude/skills/updating-changelog/SKILL.md index adf92a94bf1..a25cf7d3676 100644 --- a/.claude/skills/updating-changelog/SKILL.md +++ b/.claude/skills/updating-changelog/SKILL.md @@ -25,7 +25,11 @@ jq -r '."."' "$AZTEC_NODE_DIR/.release-please-manifest.json" ``` `6.0.0` → edit `v6.md`. The release-please manifest at the repository root versions the -Foundation tree (`7.0.0` while Labs is on v6): never take the version from it. +Foundation tree, whose numbers are unrelated to Labs': never take the version from it. + +Documenting one aztec-node PR (source 3 below) takes the major from the PR's line instead, +since the pin can lag it: `baseRefName` `main` is the major `main` carries (v7 from +2026-11-04), `v6` is v6. **Target files:** @@ -96,7 +100,7 @@ lives: 3. **One aztec-node PR**, when asked to document a change that has not reached the pin yet: ```bash - gh pr view <PR_NUMBER> --repo aztec-labs-eng/aztec-node --json title,body,files,url + gh pr view <PR_NUMBER> --repo aztec-labs-eng/aztec-node --json title,body,baseRefName,files,url gh pr diff <PR_NUMBER> --repo aztec-labs-eng/aztec-node ``` diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 1008a2c3357..ab56e9aebc8 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -63,7 +63,8 @@ The aztec-node (Labs) code the docs quote is the commit `aztec-node.json` pins, - Move the pin with `node scripts/aztec_node.mts pin <tag>` (a release or nightly tag), never by hand; then set `@aztec-foundation/l1-artifacts` to the version `check` names, run `yarn install`, and commit `aztec-node.json` with `package.json` and `yarn.lock`. - `pin` needs `gh` logged in: it records the sha256 of each noir release archive in `noirArchives` once `gh attestation verify` proves noir's publish workflow built it, and `provision` refuses a nargo archive whose sha256 differs. Never edit `noirArchives` to get past a mismatch: it means the release archive changed after the pin. - Tools that need nargo, bb or the aztec CLI use the pinned published ones: `node scripts/aztec_node.mts provision nargo bb cli`, then `eval "$(node scripts/aztec_node.mts env)"` exports `NARGO`, `BB`, `AZTEC` and the versions. Never run nargo inside `docs/` or the checkout: it takes `docs/Nargo.toml` as its workspace. -- A release cut moves the pin to the release tag in its own worktree only (`/release-docs`, `/release-network-docs`); the dev docs keep their pin. +- The dev docs follow aztec-node `main`: move their pin to the newest nightly of the major `main` carries (v7 from 2026-11-04), never to a tag of a release line such as `v6`. Older nightlies on `main` are still v6, so check the major, then check that the recorded commit is on `main`: `gh api "repos/aztec-labs-eng/aztec-node/compare/main...$(node -p 'require("./aztec-node.json").commit')" --jq .status` prints `behind` or `identical`. +- A release cut moves the pin to the release tag in its own worktree only (`/release-docs`, `/release-network-docs`); v6.x releases are cut from their tags on aztec-node's `v6` branch. The dev docs keep their pin. ### Preprocessing Environment Variables @@ -77,7 +78,7 @@ The preprocessing system uses these environment variables: | `DEVNET_TAG` | Version for devnet builds | from `developer_version_config.json` | | `TESTNET_TAG` | Version for testnet builds | from `developer_version_config.json` | | `MAINNET_TAG` | Version for mainnet builds | from `developer_version_config.json` | -| `COMMIT_TAG` | Legacy variable, used as fallback for `NIGHTLY_TAG` | `next` | +| `COMMIT_TAG` | Legacy variable, used as fallback for `NIGHTLY_TAG` | `ref` in `aztec-node.json` | ### Preprocessing Macros @@ -94,7 +95,7 @@ The preprocessing system uses these environment variables: **Legacy macros:** -- `#include_aztec_version` - Uses `COMMIT_TAG` (a Labs version, never a ref into aztec-packages) +- `#include_aztec_version` - Uses `COMMIT_TAG`, else the pin's `ref` (a Labs version, never a ref into aztec-packages; `@aztec-foundation/*` versions use `#include_foundation_version_without_prefix`) - `#include_devnet_version`, `#include_testnet_version`, `#include_mainnet_version` - Version-specific macros **`#include_code` paths**: Snippet paths may point into `docs/node_modules` (e.g. `docs/node_modules/@aztec-foundation/l1-artifacts/...`). For those snippets the generated source link resolves to the installed package's own version tag, NOT `COMMIT_TAG` - only the package's tag is guaranteed to match the extracted snippet line-for-line (see `src/preprocess/include_code.js`). diff --git a/docs/README.md b/docs/README.md index 4c5707cceba..e222edbcd90 100644 --- a/docs/README.md +++ b/docs/README.md @@ -227,7 +227,8 @@ eval "$(node scripts/aztec_node.mts env)" # export AZTEC_NODE_DIR, the versio - `#include_code` and `references:` paths keep their `labs/` prefix, which names the checkout: `labs/yarn-project/...` reads `.aztec-node/tree/yarn-project/...`, and source links name aztec-node at the pinned commit (a tag can move; a commit cannot). - **Moving the pin:** run `pin <tag>`, then do what `check` asks (usually: set `@aztec-foundation/l1-artifacts` in `package.json` to the pinned `BB_VERSION` and run `yarn install`), and commit `aztec-node.json`, `package.json` and `yarn.lock` together. Snippets whose markers moved fail the build until the pages follow. - **Verified nargo:** `pin` needs the GitHub CLI, logged in. It downloads noir's release archive for each platform, has `gh attestation verify` check that noir's `publish-nargo.yml` workflow built it, and records its sha256 in `noirArchives`. `provision nargo` needs no `gh`: it refuses an archive whose sha256 differs from the pin, and a platform the pin has no sha256 for. Never edit `noirArchives` by hand; a mismatch means the release archive changed after the pin. -- **Release cuts** move the pin to the release tag inside their own worktree, and the snapshot is generated from that; the dev docs keep their pin. See the `release-docs` and `release-network-docs` skills under `.claude/skills/`. +- **Which tag:** the dev docs follow aztec-node `main`. Move their pin to the newest nightly of the major `main` carries (v7 from 2026-11-04), never to a tag of a release line such as `v6`. Older nightlies on `main` are still v6, so check the major; then check that the recorded commit is on `main` (from `docs/`): `gh api "repos/aztec-labs-eng/aztec-node/compare/main...$(node -p 'require("./aztec-node.json").commit')" --jq .status` prints `behind` or `identical`. +- **Release cuts** move the pin to the release tag inside their own worktree, and the snapshot is generated from that; the dev docs keep their pin. v6.x releases are cut from their tags on aztec-node's `v6` branch. See the `release-docs` and `release-network-docs` skills under `.claude/skills/`. - **Examples** build with the pinned published toolchain against the default checkout, which their Nargo.toml path dependencies name (`../../../.aztec-node/tree/...`), so `AZTEC_NODE_DIR` does not apply to them. The TS examples install `@aztec-labs/*` at the pinned `AZTEC_VERSION` and `@aztec-foundation/*` at `BB_VERSION`. - Never run nargo inside `docs/` or the checkout: nargo takes `docs/Nargo.toml` as its workspace. @@ -444,7 +445,7 @@ You can embed code snippets into a `.md`/`.mdx` file from code which lives elsew ### `#include_aztec_version` -This macro will be replaced inline with `COMMIT_TAG`: the Labs release tag in a release cut, `next` otherwise. It is an aztec-node (Labs) version, never a ref into this repository; use `#include_foundation_version` for that. +This macro will be replaced inline with `COMMIT_TAG`: the Labs release tag in a release cut, the pin's `ref` from `aztec-node.json` otherwise. It is an aztec-node (Labs) version, never a ref into this repository and never an `@aztec-foundation/*` version; use `#include_foundation_version` (or `#include_foundation_version_without_prefix`) for those. Alternatively, you can also use the `AztecPackagesVersion()` js function, which you need to import explicitly: diff --git a/docs/docs-developers/support.md b/docs/docs-developers/support.md index 1aff215f8cf..f6c255e3a54 100644 --- a/docs/docs-developers/support.md +++ b/docs/docs-developers/support.md @@ -80,7 +80,7 @@ If something in the docs is wrong, outdated, or missing, please **[open an issue This includes typos and small text issues: please file them as issues rather than opening single-line PRs. A maintainer can fix several at once, which is faster to review than a stream of one-line pull requests. -For larger restructures or new pages, open an issue first so a maintainer can confirm the direction before you write the change. Every docs page in this site has an "Edit this page" link at the bottom that takes you to the right file in [`docs-developers/`](https://github.com/aztec-labs-eng/aztec-node/tree/next/docs/docs-developers) once the direction is agreed. +For larger restructures or new pages, open an issue first so a maintainer can confirm the direction before you write the change. Every docs page in this site has an "Edit this page" link at the bottom that takes you to the right file in [`docs-developers/`](https://github.com/AztecProtocol/aztec-packages/tree/next/docs/docs-developers) once the direction is agreed. ## Operator and node issues diff --git a/docs/docs/networks.md b/docs/docs/networks.md index a361b194494..7d0c2ad280e 100644 --- a/docs/docs/networks.md +++ b/docs/docs/networks.md @@ -39,7 +39,7 @@ Aztec publishes three kinds of builds, each with a different stability promise. | -------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | **Stable** | `6.0.0`, `5.2.0` | The validated, final version for a release cycle. | Builders shipping to users. Operators running Alpha or Testnet. | | **Release candidate (RC)** | `5.0.0-rc.1`, `5.0.0-rc.2` | Pre-release of an upcoming stable, used for internal validation and Testnet rehearsals. Additional RCs ship if issues are found. | Operators participating in pre-release rehearsals. Builders verifying compatibility ahead of a stable cut. | -| **Nightly** | `5.0.0-nightly.<date>` | Latest in-progress work from the development branch. Experimental, less tested. | Builders previewing upcoming features. Not recommended for production. | +| **Nightly** | `7.0.0-nightly.<date>` | Latest in-progress work from aztec-node's `main` branch. Experimental, less tested. | Builders previewing upcoming features. Not recommended for production. | An RC is not newer than its matching stable: `5.0.0-rc.1` is a checkpoint on the way to `5.0.0`, and `5.0.0` supersedes every `5.0.0-rc.*`. From 877943a554df4c8690761e8f37019de11b90f0f6 Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Mon, 2 Nov 2026 17:50:24 +0000 Subject: [PATCH 03/32] chore(docs): drop a repeated comment in include_version --- docs/src/preprocess/include_version.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/preprocess/include_version.js b/docs/src/preprocess/include_version.js index 79d584aa99d..07b8e8a9bf6 100644 --- a/docs/src/preprocess/include_version.js +++ b/docs/src/preprocess/include_version.js @@ -98,8 +98,8 @@ async function preprocessIncludeVersion(markdownContent, filePath = "unknown") { const testnetTag = process.env.TESTNET_TAG || stripV(developerVersionConfig.testnet) || "0.0.0"; const mainnetTag = process.env.MAINNET_TAG || stripV(developerVersionConfig.mainnet) || "0.0.0"; const releaseType = process.env.RELEASE_TYPE || "mainnet"; - // COMMIT_TAG kept for backwards compatibility. Release cuts set it to the Labs tag; any other build - // names the pinned aztec-node tag, the code the pages quote. + // Release cuts set COMMIT_TAG to the Labs tag; any other build names the pinned aztec-node tag, + // the code the pages quote. const commitTag = process.env.COMMIT_TAG || pinnedAztecNodeRef(); // Validate release type From df4cd322094a7b4ac544c0545cd25ea4b1c3083f Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Thu, 5 Nov 2026 14:56:36 +0000 Subject: [PATCH 04/32] docs: keep the dev pin on aztec-node's v6 nightlies until the docs carry a v7 line A v6.x release is cut from the dev docs' pages and examples, so it works only while they are v6 content. The pin check now tests that the recorded commit is on v6, and update-doc-references gates PRs into main (v7) instead of PRs into v6. --- .claude/skills/release-docs/SKILL.md | 14 ++++++++------ .claude/skills/update-doc-references/SKILL.md | 11 ++++++----- .claude/skills/updating-changelog/SKILL.md | 2 +- docs/CLAUDE.md | 2 +- docs/README.md | 2 +- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.claude/skills/release-docs/SKILL.md b/.claude/skills/release-docs/SKILL.md index bc0faeed78a..315e7a2c200 100644 --- a/.claude/skills/release-docs/SKILL.md +++ b/.claude/skills/release-docs/SKILL.md @@ -48,9 +48,9 @@ paste the key into the conversation or onto a command line. `BB` once provisioned). In the cut that is the release tag's. - **The cut runs in its own worktree** (`$CUT`), created from the destination commit; it moves the cut's pin to the Labs tag. The destination checkout is not touched until Step 17, - and its pin never moves: the destination's dev docs keep following their own pin, on - aztec-node `main`. A v6.x cut pins its tag on aztec-node's `v6` branch, so the cut and - the destination can sit on different lines (Step 12). + and its pin never moves: the destination's dev docs keep following their own pin, a v6 + nightly until the docs carry a separate v7 line (`docs/CLAUDE.md`). A v6.x cut pins its + tag on aztec-node's `v6` branch (Step 12). - **Release inputs**, recorded in the commit that lands the cut (Step 17): | Input | Source | @@ -982,9 +982,11 @@ git -C "$AZTEC_NODE_HISTORY" log --oneline --left-right --cherry-pick --name-onl Skip this step when the two refs name the same commit. `--cherry-pick` drops commits that reached both sides as patch-equivalent ports. A hand backport adapted to its line is not patch-equivalent and shows on both sides: judge it against `$AZTEC_NODE_DIR`, the tag. A -v6.x cut against a destination pinned to a v7 nightly lists every v7 commit since `v6` -left `main` as `>`. When `docs/examples/` already use v7-only APIs, Step 15 fails for the -cut and stops: no snapshot edit can fix the examples. The history goes into a checkout of its own, fetched afresh +destination pinned to a v7 nightly would list every v7 commit since `v6` left `main` as +`>`, and once `docs/examples/` use v7-only APIs Step 15 fails for a v6.x cut and stops: no +snapshot edit can fix the examples. That is why the dev pin stays on a v6 nightly +(`docs/CLAUDE.md`); if the destination's pin is a v7 tag, stop and report before cutting. +The history goes into a checkout of its own, fetched afresh down to the refs' merge base, which GitHub names without local history; fetching it into `$AZTEC_NODE_DIR` instead keeps that checkout's earlier shallow boundaries (the merge base goes missing) and shortens the history Step 8 fetched. The check compares the merge base diff --git a/.claude/skills/update-doc-references/SKILL.md b/.claude/skills/update-doc-references/SKILL.md index 566e171a6e0..80dc08ea5ac 100644 --- a/.claude/skills/update-doc-references/SKILL.md +++ b/.claude/skills/update-doc-references/SKILL.md @@ -40,9 +40,10 @@ gh pr diff <PR_NUMBER> --repo <REPO> An aztec-node PR names paths from aztec-node's root: prefix them with `labs/` to compare them with `references`. -The source docs follow aztec-node `main`. An aztec-node PR into a release line -(`baseRefName` `v6`) changes code the pin may never get. Test its merge commit against the -pin (an unmerged PR is not in it): +The source docs follow the pin's line, aztec-node `v6` until the docs carry a separate v7 +line (`docs/CLAUDE.md`). An aztec-node PR into `main` (`baseRefName` `main`, v7) changes +code the pin may never get. Test its merge commit against the pin (an unmerged PR is not +in it): ```bash cd "$(git rev-parse --show-toplevel)" @@ -52,8 +53,8 @@ gh api "repos/aztec-labs-eng/aztec-node/compare/$merge...$pin" --jq .status ``` `ahead` or `identical` means the pin has it: continue as for any PR. Otherwise edit no -source docs, since they describe `main`. Still run Steps 3 and 4, and report what the -docs of the line's next release would need; that release's cut (`/release-docs`) decides. +source docs, since they describe v6. Still run Steps 3 and 4, and report what v7 docs +would need. When the change is a move of the docs' pin rather than a PR (`node docs/scripts/aztec_node.mts pin <tag>`), take it from the two pin commits, fetched into the diff --git a/.claude/skills/updating-changelog/SKILL.md b/.claude/skills/updating-changelog/SKILL.md index b7d9bcd4974..499e0dfcf2d 100644 --- a/.claude/skills/updating-changelog/SKILL.md +++ b/.claude/skills/updating-changelog/SKILL.md @@ -39,7 +39,7 @@ root versions the Foundation tree instead, whose numbers are unrelated to Labs': the version from it. Documenting one aztec-node PR (source 3 below) takes the major from the PR's line instead, -since the pin can lag it: `baseRefName` `main` is the major `main` carries (v7 from +since the pin can lag it or sit on the other line: `baseRefName` `main` is the major `main` carries (v7 from 2026-11-04), `v6` is v6. **Target files:** diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 267e0eb6f84..cd6fbac658a 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -63,7 +63,7 @@ The aztec-node (Labs) code the docs quote is the commit `aztec-node.json` pins, - Move the pin with `node scripts/aztec_node.mts pin <tag>` (a release or nightly tag), never by hand; then set `@aztec-foundation/l1-artifacts` to the version `check` names, run `yarn install`, and commit `aztec-node.json` with `package.json` and `yarn.lock`. - `pin` needs `gh` logged in: it records the sha256 of each noir release archive in `noirArchives` once `gh attestation verify` proves noir's publish workflow built it, and `provision` refuses a nargo archive whose sha256 differs. Never edit `noirArchives` to get past a mismatch: it means the release archive changed after the pin. - Tools that need nargo, bb or the aztec CLI use the pinned published ones: `node scripts/aztec_node.mts provision nargo bb cli`, then `eval "$(node scripts/aztec_node.mts env)"` exports `NARGO`, `BB`, `AZTEC` and the versions. Never run nargo inside `docs/` or the checkout: it takes `docs/Nargo.toml` as its workspace. -- The dev docs follow aztec-node `main`: move their pin to the newest nightly of the major `main` carries (v7 from 2026-11-04), never to a tag of a release line such as `v6`. Older nightlies on `main` are still v6, so check the major, then check that the recorded commit is on `main`: `gh api "repos/aztec-labs-eng/aztec-node/compare/main...$(node -p 'require("./aztec-node.json").commit')" --jq .status` prints `behind` or `identical`. +- The dev docs stay on aztec-node's v6 line until they carry a separate v7 line: move their pin to the newest `v6.*` nightly, never to a v7 tag (`main` carries v7 from 2026-11-04). A v6.x release is cut from the dev docs' pages and examples, so it works only while they are v6 content. Then check that the recorded commit is on `v6`: `gh api "repos/aztec-labs-eng/aztec-node/compare/v6...$(node -p 'require("./aztec-node.json").commit')" --jq .status` prints `behind` or `identical`. - A release cut moves the pin to the release tag in its own worktree only (`/release-docs`, `/release-network-docs`); v6.x releases are cut from their tags on aztec-node's `v6` branch. The dev docs keep their pin. ### Preprocessing Environment Variables diff --git a/docs/README.md b/docs/README.md index e222edbcd90..d4a25015371 100644 --- a/docs/README.md +++ b/docs/README.md @@ -227,7 +227,7 @@ eval "$(node scripts/aztec_node.mts env)" # export AZTEC_NODE_DIR, the versio - `#include_code` and `references:` paths keep their `labs/` prefix, which names the checkout: `labs/yarn-project/...` reads `.aztec-node/tree/yarn-project/...`, and source links name aztec-node at the pinned commit (a tag can move; a commit cannot). - **Moving the pin:** run `pin <tag>`, then do what `check` asks (usually: set `@aztec-foundation/l1-artifacts` in `package.json` to the pinned `BB_VERSION` and run `yarn install`), and commit `aztec-node.json`, `package.json` and `yarn.lock` together. Snippets whose markers moved fail the build until the pages follow. - **Verified nargo:** `pin` needs the GitHub CLI, logged in. It downloads noir's release archive for each platform, has `gh attestation verify` check that noir's `publish-nargo.yml` workflow built it, and records its sha256 in `noirArchives`. `provision nargo` needs no `gh`: it refuses an archive whose sha256 differs from the pin, and a platform the pin has no sha256 for. Never edit `noirArchives` by hand; a mismatch means the release archive changed after the pin. -- **Which tag:** the dev docs follow aztec-node `main`. Move their pin to the newest nightly of the major `main` carries (v7 from 2026-11-04), never to a tag of a release line such as `v6`. Older nightlies on `main` are still v6, so check the major; then check that the recorded commit is on `main` (from `docs/`): `gh api "repos/aztec-labs-eng/aztec-node/compare/main...$(node -p 'require("./aztec-node.json").commit')" --jq .status` prints `behind` or `identical`. +- **Which tag:** the dev docs stay on aztec-node's v6 line until they carry a separate v7 line. Move their pin to the newest `v6.*` nightly, never to a v7 tag (`main` carries v7 from 2026-11-04): a v6.x release is cut from the dev docs' pages and examples, so it works only while they are v6 content. Then check that the recorded commit is on `v6` (from `docs/`): `gh api "repos/aztec-labs-eng/aztec-node/compare/v6...$(node -p 'require("./aztec-node.json").commit')" --jq .status` prints `behind` or `identical`. - **Release cuts** move the pin to the release tag inside their own worktree, and the snapshot is generated from that; the dev docs keep their pin. v6.x releases are cut from their tags on aztec-node's `v6` branch. See the `release-docs` and `release-network-docs` skills under `.claude/skills/`. - **Examples** build with the pinned published toolchain against the default checkout, which their Nargo.toml path dependencies name (`../../../.aztec-node/tree/...`), so `AZTEC_NODE_DIR` does not apply to them. The TS examples install `@aztec-labs/*` at the pinned `AZTEC_VERSION` and `@aztec-foundation/*` at `BB_VERSION`. - Never run nargo inside `docs/` or the checkout: nargo takes `docs/Nargo.toml` as its workspace. From 1b6ad66268ec71feb519271641309205a34616d7 Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Thu, 5 Nov 2026 15:58:31 +0000 Subject: [PATCH 05/32] chore(docs): add a production build fingerprint Prints a sorted hash per page, markdown twin and llms file, the sitemap paths and the search index, so a production build can be diffed against another to show tooling changes leave what docs.aztec.network serves untouched. --- .../__tests__/production_fingerprint.test.mts | 38 +++++++++++ docs/scripts/production_fingerprint.mts | 63 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 docs/scripts/__tests__/production_fingerprint.test.mts create mode 100644 docs/scripts/production_fingerprint.mts diff --git a/docs/scripts/__tests__/production_fingerprint.test.mts b/docs/scripts/__tests__/production_fingerprint.test.mts new file mode 100644 index 00000000000..c0054d4feee --- /dev/null +++ b/docs/scripts/__tests__/production_fingerprint.test.mts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { test } from "node:test"; +import zlib from "node:zlib"; +import { fingerprint } from "../production_fingerprint.mts"; + +function build(bundle: string, href: string) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fingerprint-")); + const write = (rel: string, data: string | Uint8Array) => { + fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); + fs.writeFileSync(path.join(dir, rel), data); + }; + write( + "developers/page.html", + `<link rel=stylesheet href=/assets/css/styles.${bundle}.css /><script src=/assets/js/main.${bundle}.js defer></script>` + + `<script>window.chunk="runtime~main.${bundle}.js"</script><main><a href=${href}>Next</a></main>`, + ); + write("developers/page.md", "# Page\n"); + write("sitemap.xml", "<urlset><url><loc>https://docs.aztec.network/developers/page</loc></url></urlset>"); + const fragment = { url: "/developers/page", filters: { docusaurus_tag: ["docs-developer-v6.0.0"] } }; + write("pagefind/fragment/en_1.pf_fragment", zlib.gzipSync(Buffer.from(`pagefind_dcd${JSON.stringify(fragment)}`))); + return dir; +} + +test("the fingerprint ignores bundle names and changes with a rendered link", () => { + const dirs = [build("aaaaaaaa", "/developers/next"), build("bbbbbbbb", "/developers/next"), build("aaaaaaaa", "/operate/next")]; + try { + const [base, rebundled, relinked] = dirs.map(fingerprint); + assert.deepEqual(rebundled, base); + assert.notDeepEqual(relinked, base); + assert.ok(base.includes("sitemap /developers/page")); + assert.ok(base.includes("search-tag docs-developer-v6.0.0 1")); + } finally { + for (const dir of dirs) fs.rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/docs/scripts/production_fingerprint.mts b/docs/scripts/production_fingerprint.mts new file mode 100644 index 00000000000..0a272d232b1 --- /dev/null +++ b/docs/scripts/production_fingerprint.mts @@ -0,0 +1,63 @@ +// Prints what a production build serves as sorted lines, so two builds compare with diff: a hash of +// each page's HTML and markdown twin, the llms files, the sitemap paths, and the search index with +// its pages per tag. Any theme code change renames the bundles, so the HTML is hashed without them; +// its text, links and structured data stay. +// +// node scripts/production_fingerprint.mts <build-dir> +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import zlib from "node:zlib"; +import { sitemapPaths } from "./check_search_index.mts"; + +const sha256 = (data: string | Uint8Array) => createHash("sha256").update(data).digest("hex"); + +/** `html` without `<link>` tags, `<script src>` tags and the content hashes in bundle names. */ +export const normalizeHtml = (html: string) => + html + .replace(/<script\b[^>]*\bsrc=[^>]*>\s*<\/script>/g, "") + .replace(/<link\b[^>]*>/g, "") + .replace(/\.[0-9a-f]{8}\.(js|css)\b/g, ".$1"); + +function* files(root: string, rel = ""): Generator<string> { + for (const entry of fs.readdirSync(path.join(root, rel), { withFileTypes: true })) { + const file = rel ? `${rel}/${entry.name}` : entry.name; + if (entry.isDirectory()) yield* files(root, file); + else yield file; + } +} + +// Pagefind writes these differently on two builds of the same tree. They are derived from the +// fragments, which hold each page's content and filters. +const UNSTABLE = /^pagefind\/(filter\/|pagefind-entry\.json$|pagefind\.[^/]+\.pf_meta$)/; + +// A fragment is gzipped JSON behind a 12-byte `pagefind_dcd` signature. +const fragmentTags = (bytes: Uint8Array): string[] => + JSON.parse(zlib.gunzipSync(bytes).subarray(12).toString("utf8")).filters?.docusaurus_tag ?? []; + +export function fingerprint(buildDir: string): string[] { + const lines: string[] = []; + const tags = new Map<string, number>(); + for (const rel of files(buildDir)) { + const bytes = fs.readFileSync(path.join(buildDir, rel)); + if (rel.startsWith("pagefind/")) { + if (!UNSTABLE.test(rel)) lines.push(`pagefind ${rel} ${sha256(bytes)}`); + if (rel.endsWith(".pf_fragment")) + for (const tag of fragmentTags(bytes)) tags.set(tag, (tags.get(tag) ?? 0) + 1); + } else if (rel.endsWith(".html")) lines.push(`html ${rel} ${sha256(normalizeHtml(bytes.toString("utf8")))}`); + else if (rel.endsWith(".md") || rel.endsWith(".txt")) lines.push(`text ${rel} ${sha256(bytes)}`); + else if (rel === "sitemap.xml") for (const p of sitemapPaths(bytes.toString("utf8"))) lines.push(`sitemap ${p}`); + } + for (const [tag, count] of tags) lines.push(`search-tag ${tag} ${count}`); + return lines.sort(); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { + const [buildDir] = process.argv.slice(2); + if (!buildDir) { + console.error("usage: node scripts/production_fingerprint.mts <build-dir>"); + process.exit(2); + } + process.stdout.write(`${fingerprint(buildDir).join("\n")}\n`); +} From 34263dcd67b1079cec0528969ba0460e94e552d1 Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Thu, 5 Nov 2026 16:04:00 +0000 Subject: [PATCH 06/32] test(docs): keep the fingerprint fixture's sitemap off docs.aztec.network check_source_urls requires every docs.aztec.network URL in tracked files to resolve, and the fixture's page does not exist. --- docs/scripts/__tests__/production_fingerprint.test.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/scripts/__tests__/production_fingerprint.test.mts b/docs/scripts/__tests__/production_fingerprint.test.mts index c0054d4feee..78b9572705e 100644 --- a/docs/scripts/__tests__/production_fingerprint.test.mts +++ b/docs/scripts/__tests__/production_fingerprint.test.mts @@ -18,7 +18,7 @@ function build(bundle: string, href: string) { `<script>window.chunk="runtime~main.${bundle}.js"</script><main><a href=${href}>Next</a></main>`, ); write("developers/page.md", "# Page\n"); - write("sitemap.xml", "<urlset><url><loc>https://docs.aztec.network/developers/page</loc></url></urlset>"); + write("sitemap.xml", "<urlset><url><loc>https://example.com/developers/page</loc></url></urlset>"); const fragment = { url: "/developers/page", filters: { docusaurus_tag: ["docs-developer-v6.0.0"] } }; write("pagefind/fragment/en_1.pf_fragment", zlib.gzipSync(Buffer.from(`pagefind_dcd${JSON.stringify(fragment)}`))); return dir; From e6ebc0bc19792bbf7aa00f57c0da050784099b74 Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Thu, 5 Nov 2026 16:04:00 +0000 Subject: [PATCH 07/32] feat(docs): let the aztec-node pin serve dev lines A line root docs/lines/<line>/ gets its own pin, checkout and toolchain through --line <line>. It never reads AZTEC_NODE_DIR. It refuses another major's tag offline. It gets its own l1-artifacts from npm, checked against the registry integrity. fetch covers every line except in production, and pin installs a line's l1-artifacts before checking. With no docs/lines/ nothing changes. --- docs/scripts/__tests__/aztec_node.test.mts | 81 +++++++++- docs/scripts/aztec_node.mts | 171 +++++++++++++++++---- 2 files changed, 217 insertions(+), 35 deletions(-) diff --git a/docs/scripts/__tests__/aztec_node.test.mts b/docs/scripts/__tests__/aztec_node.test.mts index e60ebfd8066..4ea49ef716b 100644 --- a/docs/scripts/__tests__/aztec_node.test.mts +++ b/docs/scripts/__tests__/aztec_node.test.mts @@ -13,9 +13,12 @@ import { type Pin, PinError, envLines, + fetchPinned, fetchTree, integrityMatches, + lineOptions, noirArchiveUrl, + pinAndCheck, pinFile, pinTag, platform, @@ -147,13 +150,38 @@ function makeFixture(): Fixture { toolchain: { aztec: "1.1.0-nightly.20260101", bb: "1.1.0", noir: "0.9.1", l1Artifacts: "1.1.0" }, noirArchives: archivesOf("0.9.1"), }; - return { root, o: { docsDir, repository: remote, noirReleases: releases }, remote, a, b, pinA, pinB }; + return { root, o: { docsDir, repository: remote, noirReleases: releases, npmRegistry: releases }, remote, a, b, pinA, pinB }; } +// Serves l1-artifacts <version>'s registry entry and tarball; `tampered` serves other bytes under the same integrity. +function serveL1Artifacts(version: string, tampered = false) { + const dir = fs.mkdtempSync(path.join(f.root, "l1-artifacts-")); + write(path.join(dir, "package/package.json"), JSON.stringify({ name: "@aztec-foundation/l1-artifacts", version })); + write(path.join(dir, "package/l1-contracts/src/Rollup.sol"), `// ${version}\n`); + execFileSync("tar", ["-czf", path.join(dir, "l1.tgz"), "-C", dir, "package"]); + const bytes = fs.readFileSync(path.join(dir, "l1.tgz")); + const tarball = `/l1-artifacts-${version}.tgz`; + routes.set(tarball, tampered ? new TextEncoder().encode("tampered") : bytes); + const dist = { tarball: `${releases}${tarball}`, integrity: `sha512-${createHash("sha512").update(bytes).digest("base64")}` }; + routes.set(`/@aztec-foundation/l1-artifacts/${version}`, new TextEncoder().encode(JSON.stringify({ dist }))); +} + +// A v1 line pinned to the nightly beside main docs pinned to the release, so their l1-artifacts differ. +function withLine(): Options { + const lo = lineOptions(f.o, "v1"); + write(pinFile(f.o), JSON.stringify(f.pinA)); + write(pinFile(lo), JSON.stringify(f.pinB)); + return lo; +} + +const l1ArtifactsVersion = (o: Options) => + JSON.parse(fs.readFileSync(path.join(o.docsDir, ".aztec-node/l1-artifacts/package.json"), "utf8")).version; + let f: Fixture; let savedPath: string | undefined; beforeEach(() => { delete process.env.AZTEC_NODE_DIR; + delete process.env.CONTEXT; savedPath = process.env.PATH; f = makeFixture(); routes = new Map(archives); @@ -407,6 +435,57 @@ test("env quotes every value for the shell and omits unprovisioned binaries", as assert.ok(fs.existsSync(manifest("aztec"))); }); +test("fetch gives each line its own checkout and l1-artifacts, and skips the lines in production", async () => { + const lo = withLine(); + serveL1Artifacts("1.1.0"); + process.env.CONTEXT = "production"; + assert.deepEqual(await fetchPinned(f.o), []); + assert.ok(!fs.existsSync(path.join(lo.docsDir, ".aztec-node"))); + delete process.env.CONTEXT; + + // AZTEC_NODE_DIR moves main's checkout only. + process.env.AZTEC_NODE_DIR = path.join(f.root, "main-tree"); + assert.deepEqual(await fetchPinned(f.o), []); + assert.equal(head(f.o), f.a); + assert.equal(treeDir(lo), path.join(lo.docsDir, ".aztec-node/tree")); + assert.equal(head(lo), f.b); + assert.equal(l1ArtifactsVersion(lo), "1.1.0"); // while docs/package.json pins 1.0.0-rc.1 for main + assert.ok(envLines(lo, f.pinB).includes(`export AZTEC_NODE_DIR='${treeDir(lo)}'`)); +}); + +test("a line refuses another major's tag before any network access", async () => { + const lo = withLine(); + const v2 = { ...f.pinB, ref: "v2.0.0", toolchain: { ...f.pinB.toolchain, aztec: "2.0.0" } }; + const refused = (e: unknown) => e instanceof PinError && /ref must be a v1\.\* tag/.test(e.message); + assert.throws(() => validatePin(v2, lo), refused); + validatePin(v2, f.o); + await assert.rejects(pinTag(lo, "v2.0.0"), refused); // the remote has no v2.0.0, so the refusal came first + write(pinFile(lo), JSON.stringify(v2)); + await assert.rejects(fetchPinned(f.o), refused); +}); + +test("l1-artifacts installs for a line only, from a tarball matching its registry integrity", async () => { + const lo = withLine(); + serveL1Artifacts("1.1.0", true); + await assert.rejects(provision(lo, f.pinB, ["l1-artifacts"]), (e) => e instanceof PinError && /does not match its registry integrity/.test(e.message)); + assert.ok(!fs.existsSync(path.join(lo.docsDir, ".aztec-node/l1-artifacts"))); + fetchTree(lo, f.pinB); + assert.match(problems(lo, f.pinB).join(), /holds @aztec-foundation\/l1-artifacts nothing, not the pinned 1\.1\.0/); + await assert.rejects(provision(f.o, f.pinA, ["l1-artifacts"]), (e) => e instanceof PinError && /line only/.test(e.message)); +}); + +test("pin on a line installs the new l1-artifacts before it checks, so a bb change passes", async () => { + usingGh(); + const lo = withLine(); + serveL1Artifacts("1.0.0-rc.1"); + serveL1Artifacts("1.1.0"); + assert.deepEqual(await pinAndCheck(lo, "v1.0.0"), []); + assert.equal(l1ArtifactsVersion(lo), "1.0.0-rc.1"); + assert.deepEqual(await pinAndCheck(lo, "v1.1.0-nightly.20260101"), []); + assert.equal(l1ArtifactsVersion(lo), "1.1.0"); + assert.deepEqual(readPin(lo), f.pinB); +}); + test("registry integrity and platform names", () => { const bytes = new TextEncoder().encode("tarball"); const sri = `sha512-${createHash("sha512").update(bytes).digest("base64")}`; diff --git a/docs/scripts/aztec_node.mts b/docs/scripts/aztec_node.mts index 8d204454927..93930d1b904 100644 --- a/docs/scripts/aztec_node.mts +++ b/docs/scripts/aztec_node.mts @@ -1,12 +1,13 @@ // The aztec-node code the docs quote, pinned in docs/aztec-node.json: snippets, reference checks and // generators read a checkout of the pinned commit, never the labs/ submodule, so the foundation's -// labs gitlink can move without touching the docs. +// labs gitlink can move without touching the docs. A line (docs/lines/<line>/, following aztec-node's +// <line> branch) has its own pin and state, selected with --line <line>. // -// node scripts/aztec_node.mts fetch check out the pinned commit (no network when current) -// node scripts/aztec_node.mts check fail on any drift between the pin, the checkout and docs -// node scripts/aztec_node.mts pin <tag> move the pin to an aztec-node release or nightly tag (needs gh) -// node scripts/aztec_node.mts provision <nargo|bb|cli>... install the pinned published toolchain -// node scripts/aztec_node.mts env print the pin as shell exports, for eval +// node scripts/aztec_node.mts [--line <line>] fetch check out the pinned commit (no network when current); without --line, every line's too outside production +// node scripts/aztec_node.mts [--line <line>] check fail on any drift between the pin, the checkout and docs +// node scripts/aztec_node.mts [--line <line>] pin <tag> move the pin to an aztec-node release or nightly tag (needs gh) +// node scripts/aztec_node.mts [--line <line>] provision <nargo|bb|cli|l1-artifacts>... install the pinned published toolchain (l1-artifacts: a line only) +// node scripts/aztec_node.mts [--line <line>] env print the pin as shell exports, for eval // // .mts because docs/package.json declares no module type: a .ts file with ESM syntax makes every run // print MODULE_TYPELESS_PACKAGE_JSON. @@ -54,26 +55,50 @@ export type Pin = { }; export type Options = { + /** The directory holding the pin and its gitignored `.aztec-node/` state: docs/, or a line root. */ docsDir: string; - // Only tests point these elsewhere; the CLI always uses the canonical repository and noir's releases. + /** docs/, whose package.json carries the yarn settings; defaults to `docsDir`. */ + packageDir?: string; + /** Set for a line root: the aztec-node branch it follows (`v6`), also the only major it accepts. */ + line?: string; + // Only tests point these elsewhere; the CLI always uses the canonical repository, noir's releases and npm. repository?: string; noirReleases?: string; + npmRegistry?: string; }; export class PinError extends Error {} const docsDirOf = (o: Options) => path.resolve(o.docsDir); +const packageDirOf = (o: Options) => path.resolve(o.packageDir ?? o.docsDir); +const LINE = /^v\d+$/; + +/** The lines under docs/lines/: each directory named for an aztec-node branch that holds a pin. */ +export function lineNames(o: Options): string[] { + const dir = path.join(packageDirOf(o), "lines"); + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir).filter((l) => LINE.test(l) && fs.existsSync(path.join(dir, l, "aztec-node.json"))).sort(); +} + +export const lineOptions = (o: Options, line: string): Options => ({ + ...o, + docsDir: path.join(packageDirOf(o), "lines", line), + packageDir: packageDirOf(o), + line, +}); export const pinFile = (o: Options) => path.join(docsDirOf(o), "aztec-node.json"); export const stateDir = (o: Options) => path.join(docsDirOf(o), ".aztec-node"); -// Readers run from different directories, so a relative override would name different trees. +// Readers run from different directories, so a relative override would name different trees. The +// override names main's checkout: a line reading it would quote main's code. export function treeDir(o: Options): string { - const dir = process.env.AZTEC_NODE_DIR; + const dir = o.line ? undefined : process.env.AZTEC_NODE_DIR; if (!dir) return path.join(stateDir(o), "tree"); if (!path.isAbsolute(dir)) throw new PinError(`AZTEC_NODE_DIR must be an absolute path, not ${dir}`); return dir; } const binDir = (o: Options) => path.join(stateDir(o), "bin"); const cliDir = (o: Options) => path.join(stateDir(o), "cli"); +const l1ArtifactsDir = (o: Options) => path.join(stateDir(o), "l1-artifacts"); // The CLIs the docs run and document, published at the pin's aztec version. const CLI_PACKAGES = ["@aztec-labs/aztec", "@aztec-labs/cli-wallet"]; const cliManifest = (o: Options, pkg: string) => path.join(cliDir(o), "node_modules", pkg, "package.json"); @@ -93,12 +118,17 @@ function tryGit(cwd: string, args: string[]): string | undefined { } } +// Offline, so CI enforces it: another major's tag is on another branch, whose code the line must not quote. +const majorProblems = (o: Options, ref: string): string[] => + o.line && !ref.startsWith(`${o.line}.`) ? [`ref must be a ${o.line}.* tag: the ${o.line} line follows aztec-node's ${o.line} branch`] : []; + /** The malformed fields other than noirArchives, which a pin being written does not have yet. */ function fieldProblems(p: Partial<Pin> | undefined, o: Options): string[] { const problems: string[] = []; const repository = o.repository ?? REPOSITORY; if (p?.repository !== repository) problems.push(`repository must be ${repository}`); if (typeof p?.ref !== "string" || !TAG.test(p.ref)) problems.push("ref must be a tag like v6.0.0 or v6.1.0-nightly.20261029"); + else problems.push(...majorProblems(o, p.ref)); if (typeof p?.commit !== "string" || !COMMIT.test(p.commit)) problems.push("commit must be 40 lowercase hex characters"); for (const key of ["aztec", "bb", "noir", "l1Artifacts"] as const) { const v = p?.toolchain?.[key]; @@ -224,6 +254,12 @@ export function problems(o: Options, pin: Pin): string[] { if (noir !== pin.toolchain.noir) out.push(`toolchain.noir is ${pin.toolchain.noir}, but ${pin.ref} pins NOIR_VERSION=${noir}: set it to ${noir}`); if (pin.toolchain.aztec !== pin.ref.slice(1)) out.push(`toolchain.aztec must be ${pin.ref.slice(1)}, the version ${pin.ref} publishes`); if (pin.toolchain.l1Artifacts !== pin.toolchain.bb) out.push(`toolchain.l1Artifacts must equal toolchain.bb (${pin.toolchain.bb}): aztec-node pins both to BB_VERSION`); + if (o.line) { + const held = packageVersion(l1ArtifactsDir(o)); + if (held !== pin.toolchain.l1Artifacts) + out.push(`${l1ArtifactsDir(o)} holds ${L1_ARTIFACTS} ${held ?? "nothing"}, not the pinned ${pin.toolchain.l1Artifacts}: run \`node scripts/aztec_node.mts --line ${o.line} provision l1-artifacts\` in docs/`); + return out; + } const pkg = JSON.parse(fs.readFileSync(path.join(docsDirOf(o), "package.json"), "utf8")); const installed = pkg.devDependencies?.[L1_ARTIFACTS] ?? pkg.dependencies?.[L1_ARTIFACTS]; if (installed !== pin.toolchain.l1Artifacts) @@ -236,6 +272,8 @@ export function problems(o: Options, pin: Pin): string[] { * digest of each noir archive gh attests. Writes the pin only after every downloaded archive verifies. */ export async function pinTag(o: Options, tag: string, log: (s: string) => void = () => {}): Promise<Pin> { + const [wrongMajor] = majorProblems(o, tag); + if (wrongMajor) throw new PinError(wrongMajor); const repository = o.repository ?? REPOSITORY; const commit = remoteTagCommit(repository, tag); const draft = { repository, ref: tag, commit, toolchain: { aztec: tag.slice(1), bb: "0.0.0", noir: "0.0.0", l1Artifacts: "0.0.0" } }; @@ -338,6 +376,15 @@ async function download(url: string): Promise<Uint8Array> { return bytes; } +/** A package's tarball from the npm registry, refused unless it matches the registry's integrity. */ +async function npmTarball(o: Options, name: string, version: string, log: (s: string) => void): Promise<Uint8Array> { + const meta = JSON.parse(new TextDecoder().decode(await download(`${o.npmRegistry ?? NPM_REGISTRY}/${name}/${version}`))); + log(`installing ${name} ${version}`); + const bytes = await download(meta.dist.tarball); + if (!integrityMatches(bytes, meta.dist.integrity)) throw new PinError(`${meta.dist.tarball} does not match its registry integrity`); + return bytes; +} + const versionOf = (binary: string) => execFileSync(binary, ["--version"], { encoding: "utf8" }); const reportsVersion = { nargo: (out: string, v: string) => out.split("\n").includes(`nargo version = ${v}`), bb: (out: string, v: string) => out.trim() === v }; @@ -358,7 +405,7 @@ function provisionCli(o: Options, pin: Pin, log: (s: string) => void): void { const dir = cliDir(o); fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(dir, { recursive: true }); - const { packageManager } = JSON.parse(fs.readFileSync(path.join(docsDirOf(o), "package.json"), "utf8")); + const { packageManager } = JSON.parse(fs.readFileSync(path.join(packageDirOf(o), "package.json"), "utf8")); const dependencies = Object.fromEntries(CLI_PACKAGES.map((pkg) => [pkg, version])); const manifest = { name: "aztec-node-cli", private: true, packageManager, dependencies }; fs.writeFileSync(path.join(dir, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`); @@ -375,7 +422,37 @@ function provisionCli(o: Options, pin: Pin, log: (s: string) => void): void { if (!cliIsAt(o, version)) throw new PinError(`${dir} does not hold ${CLI_PACKAGES.join(" and ")} at ${version}`); } -/** Installs each tool at the pinned version into .aztec-node/bin (or cli/), skipping those already there. */ +function packageVersion(dir: string): string | undefined { + try { + return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).version; + } catch { + return undefined; + } +} + +/** Unpacks a line's l1-artifacts at its pin into .aztec-node/l1-artifacts; main's is docs/package.json's devDependency. */ +async function provisionL1Artifacts(o: Options, pin: Pin, log: (s: string) => void): Promise<void> { + if (!o.line) throw new PinError(`l1-artifacts is provisioned for a line only: the main docs install ${L1_ARTIFACTS} with yarn`); + const version = pin.toolchain.l1Artifacts; + const dir = l1ArtifactsDir(o); + if (packageVersion(dir) === version) return; + const bytes = await npmTarball(o, L1_ARTIFACTS, version, log); + fs.mkdirSync(stateDir(o), { recursive: true }); + // Unpacked beside its destination, so a failure leaves the previous package and the swap is a rename. + const tmp = fs.mkdtempSync(path.join(stateDir(o), "l1-artifacts-")); + try { + fs.writeFileSync(path.join(tmp, "package.tgz"), bytes); + execFileSync("tar", ["-xzf", path.join(tmp, "package.tgz"), "-C", tmp]); + const unpacked = path.join(tmp, "package"); + if (packageVersion(unpacked) !== version) throw new PinError(`the ${L1_ARTIFACTS} ${version} tarball holds ${packageVersion(unpacked)}`); + fs.rmSync(dir, { recursive: true, force: true }); + fs.renameSync(unpacked, dir); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } +} + +/** Installs each tool at the pinned version into .aztec-node/bin (or cli/, l1-artifacts/), skipping those already there. */ export async function provision(o: Options, pin: Pin, tools: string[], log: (s: string) => void = () => {}): Promise<void> { const bin = binDir(o); fs.mkdirSync(bin, { recursive: true }); @@ -384,7 +461,11 @@ export async function provision(o: Options, pin: Pin, tools: string[], log: (s: provisionCli(o, pin, log); continue; } - if (tool !== "nargo" && tool !== "bb") throw new PinError(`cannot provision ${tool}: only nargo, bb and cli`); + if (tool === "l1-artifacts") { + await provisionL1Artifacts(o, pin, log); + continue; + } + if (tool !== "nargo" && tool !== "bb") throw new PinError(`cannot provision ${tool}: only nargo, bb, cli and l1-artifacts`); const version = tool === "nargo" ? pin.toolchain.noir : pin.toolchain.bb; const target = platform().rust; // Checked before the installed-binary shortcut, so a platform without a digest fails the same everywhere. @@ -397,12 +478,7 @@ export async function provision(o: Options, pin: Pin, tools: string[], log: (s: try { const archive = path.join(tmp, "archive.tgz"); if (tool === "bb") { - const name = `@aztec-foundation/bb-${platform().npm}`; - const meta = JSON.parse(new TextDecoder().decode(await download(`${NPM_REGISTRY}/${name}/${version}`))); - log(`installing bb ${version} from ${name}`); - const bytes = await download(meta.dist.tarball); - if (!integrityMatches(bytes, meta.dist.integrity)) throw new PinError(`${meta.dist.tarball} does not match its registry integrity`); - fs.writeFileSync(archive, bytes); + fs.writeFileSync(archive, await npmTarball(o, `@aztec-foundation/bb-${platform().npm}`, version, log)); execFileSync("tar", ["-xzf", archive, "-C", tmp, "package/bin/bb"]); fs.copyFileSync(path.join(tmp, "package/bin/bb"), binary); } else { @@ -447,29 +523,55 @@ export function envLines(o: Options, pin: Pin): string[] { return vars.map(([k, v]) => `export ${k}=${shellQuote(v)}`); } +/** + * `fetch`: checks the pinned commit out, with a line's l1-artifacts so a cold build can render its + * snippets, and returns the problems `check` would report. For main it then fetches every line, + * except under CONTEXT=production: production serves no dev docs. + */ +export async function fetchPinned(o: Options, log: (s: string) => void = () => {}): Promise<string[]> { + const pin = readPin(o); + fetchTree(o, pin, log); + if (o.line) await provision(o, pin, ["l1-artifacts"], log); + const found = problems(o, pin); + if (!o.line && process.env.CONTEXT !== "production") + for (const line of lineNames(o)) found.push(...(await fetchPinned(lineOptions(o, line), log))); + return found; +} + +/** `pin <tag>`: moves the pin, then installs a line's l1-artifacts before checking, so a bb change can pass. */ +export async function pinAndCheck(o: Options, tag: string, log: (s: string) => void = () => {}): Promise<string[]> { + const pin = await pinTag(o, tag, log); + log(`pinned ${pin.ref} (${pin.commit}): aztec ${pin.toolchain.aztec}, bb ${pin.toolchain.bb}, noir ${pin.toolchain.noir}`); + if (o.line) await provision(o, pin, ["l1-artifacts"], log); + return problems(o, pin); +} + async function main(argv: string[]): Promise<number> { - const o: Options = { docsDir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..") }; + const docs: Options = { docsDir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..") }; + let o = docs; + if (argv[0] === "--line") { + const line = argv[1] ?? ""; + if (!lineNames(docs).includes(line)) { + console.error(`aztec-node pin: no line ${line}; the lines are the directories docs/lines/<line>/ with an aztec-node.json: ${lineNames(docs).join(", ") || "none"}`); + return 2; + } + o = lineOptions(docs, line); + argv = argv.slice(2); + } const log = (s: string) => console.error(s); const [cmd, ...args] = argv; - const report = (pin: Pin) => { - const found = problems(o, pin); + const report = (found: string[]) => { for (const p of found) console.error(`aztec-node pin: ${p}`); return found.length ? 1 : 0; }; switch (cmd) { - case "fetch": { - const pin = readPin(o); - fetchTree(o, pin, log); - return report(pin); - } + case "fetch": + return report(await fetchPinned(o, log)); case "check": - return report(readPin(o)); - case "pin": { + return report(problems(o, readPin(o))); + case "pin": if (args.length !== 1) break; - const pin = await pinTag(o, args[0], log); - log(`pinned ${pin.ref} (${pin.commit}): aztec ${pin.toolchain.aztec}, bb ${pin.toolchain.bb}, noir ${pin.toolchain.noir}`); - return report(pin); - } + return report(await pinAndCheck(o, args[0], log)); case "provision": { if (!args.length) break; await provision(o, readPin(o), args, log); @@ -477,12 +579,13 @@ async function main(argv: string[]): Promise<number> { } case "env": { const pin = readPin(o); - if (report(pin)) return 1; + if (report(problems(o, pin))) return 1; console.log(envLines(o, pin).join("\n")); return 0; } } - console.error(fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(4, 9).map((l) => l.slice(3)).join("\n")); + const usage = fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").filter((l) => l.startsWith("// node ")); + console.error(usage.map((l) => l.slice(3)).join("\n")); return 2; } From 5170906ab1fbd0c77bf9bcd42a0c4fcdb696d65a Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Thu, 5 Nov 2026 16:09:57 +0000 Subject: [PATCH 08/32] feat(docs): preprocess dev line pages against their own pin A page under docs/lines/<line>/ takes its labs/ snippets, source links, COMMIT_TAG and foundation version from its line's pin and checkout. Its #api_ref_version is the folder of the network on its major, and it fails when no network runs that major. Its links into /developers/ and /operate/ move onto the line's instances. A page that quotes another line's examples or l1-artifacts is refused. The lines are processed into processed-docs/lines/<line>/ outside production. --- docs/src/__tests__/network_docs.test.js | 5 ++ docs/src/network_docs.js | 15 ++++ .../preprocess/__tests__/include_code.test.js | 30 +++++++ .../__tests__/include_version.test.js | 31 +++++++ docs/src/preprocess/__tests__/line.test.js | 22 +++++ docs/src/preprocess/include_code.js | 88 ++++++++++--------- docs/src/preprocess/include_version.js | 28 ++++-- docs/src/preprocess/index.js | 26 +++--- docs/src/preprocess/line.js | 42 +++++++++ 9 files changed, 232 insertions(+), 55 deletions(-) create mode 100644 docs/src/preprocess/__tests__/line.test.js create mode 100644 docs/src/preprocess/line.js diff --git a/docs/src/__tests__/network_docs.test.js b/docs/src/__tests__/network_docs.test.js index 7b7ffefd4b2..7a39494c24b 100644 --- a/docs/src/__tests__/network_docs.test.js +++ b/docs/src/__tests__/network_docs.test.js @@ -7,6 +7,7 @@ const { aztecNrApiFolders, docsInstances, historicalVersions, + lineApiFolder, llmsRoutes, networkSection, startHere, @@ -33,6 +34,10 @@ describe("networks on different versions", () => { }); }); + it("gives a dev line the API folder of the network on its major, and none without one", () => { + assert.deepStrictEqual([5, 6, 7].map((major) => lineApiFolder(split, major)), ["mainnet", "testnet", undefined]); + }); + it("names the cut versions no network reads", () => { assert.deepStrictEqual(historicalVersions(developer, ["v5.2.0", "v6.0.0-rc.3", "v4.2.0"]), ["v4.2.0"]); }); diff --git a/docs/src/network_docs.js b/docs/src/network_docs.js index 656b7bd2c8c..37d673b92b7 100644 --- a/docs/src/network_docs.js +++ b/docs/src/network_docs.js @@ -78,6 +78,20 @@ const aztecNrApiFolders = (instance) => .map((v) => [v.version, v.networks[0]]) ); +/** + * The Aztec.nr API reference folder a dev line's pages link: the folder of the docs version a + * network reads with the line's major. + * + * @param {Record<string, string>} developerConfig release type → version + * @param {number} major the line's major + * @returns {string | undefined} undefined when no network runs that major + */ +function lineApiFolder(developerConfig, major) { + const folders = aztecNrApiFolders(docsInstances(developerConfig, {}, true)[0]); + const version = Object.keys(folders).find((v) => v.startsWith(`v${major}.`)); + return version && folders[version]; +} + /** * The API reference folders of each docs version a network reads, one per kind, in the folder * aztecNrApiFolders names (the TypeScript reference is published per network the same way). @@ -271,6 +285,7 @@ module.exports = { aztecNrApiFolders, docsInstances, historicalVersions, + lineApiFolder, llmsRoutes, networkSection, startHere, diff --git a/docs/src/preprocess/__tests__/include_code.test.js b/docs/src/preprocess/__tests__/include_code.test.js index c3edea2150f..68ee36a2dee 100644 --- a/docs/src/preprocess/__tests__/include_code.test.js +++ b/docs/src/preprocess/__tests__/include_code.test.js @@ -110,6 +110,36 @@ describe("include_code", () => { }); }); + it("renders a line page from its line's checkout, pin and l1-artifacts, and refuses main's files", async () => { + const line = path.join(root.rootDir, "docs/lines/v1"); + const tree = path.join(line, ".aztec-node/tree"); + write(path.join(tree, "labs-aztec-toolchain/bootstrap.sh"), "BB_VERSION=1.1.0\nNOIR_VERSION=0.1.0\n"); + write(path.join(tree, "noir-projects/a.nr"), "// docs:start:x\nfn line() {}\n// docs:end:x\n"); + git(tree, "init", "-q"); + git(tree, "add", "-A"); + git(tree, "commit", "-q", "-m", "line"); + const commit = git(tree, "rev-parse", "HEAD"); + const toolchain = { aztec: "1.1.0", bb: "1.1.0", noir: "0.1.0", l1Artifacts: "1.1.0" }; + const pin = JSON.parse(fs.readFileSync(path.join(root.rootDir, "docs/aztec-node.json"), "utf8")); + write(path.join(line, "aztec-node.json"), JSON.stringify({ ...pin, ref: "v1.1.0", commit, toolchain })); + write(path.join(line, ".aztec-node/l1-artifacts/package.json"), '{"version":"1.1.0"}'); + process.env.AZTEC_NODE_DIR = root.tree; // main's checkout: a line page never reads it + + const linePage = path.join(line, "docs-developers/page.md"); + const { content } = await preprocessIncludeCode(page, linePage, root.rootDir); + assert.match(content, /fn line\(\)/); + assert.ok(content.includes(`https://github.com/aztec-labs-eng/aztec-node/blob/${commit}/noir-projects/a.nr#L1-L3`)); + assert.match((await preprocessIncludeCode(page, "page.md", root.rootDir)).content, /fn pinned\(\)/); + assert.deepStrictEqual( + resolveSnippetSource("docs/lines/v1/.aztec-node/l1-artifacts/src/a.sol", root.rootDir), + { repoUrl: "https://github.com/AztecProtocol/aztec-packages", ref: "v1.1.0", displayPath: "src/a.sol" }, + ); + + const include = (file) => `#include_code x /${file} typescript\n`; + await assert.rejects(preprocessIncludeCode(include("docs/examples/a.ts"), linePage, root.rootDir), /docs\/examples\/a\.ts is main's, but this page is line v1's/); + await assert.rejects(preprocessIncludeCode(include("docs/lines/v1/examples/a.ts"), "page.md", root.rootDir), /is line v1's, but this page is main's/); + }); + it("links l1-artifacts snippets to aztec-packages at the installed version", () => { write(path.join(root.rootDir, "docs/node_modules/@aztec-foundation/l1-artifacts/package.json"), '{"version":"6.0.0-rc.6"}'); assert.deepStrictEqual( diff --git a/docs/src/preprocess/__tests__/include_version.test.js b/docs/src/preprocess/__tests__/include_version.test.js index 2ef1ec4c43d..f5c0d6a0e87 100644 --- a/docs/src/preprocess/__tests__/include_version.test.js +++ b/docs/src/preprocess/__tests__/include_version.test.js @@ -203,6 +203,37 @@ describe("preprocessIncludeVersion", () => { }); }); + describe("on a line page", () => { + const config = require("../../../developer_version_config.json"); + const major = Number(config.mainnet.match(/^v(\d+)\./)[1]); + let rootDir; + beforeEach(() => { + rootDir = fs.mkdtempSync(path.join(require("os").tmpdir(), "include-version-")); + for (const line of [`v${major}`, "v999"]) { + const pin = { ref: `${line}.1.0-nightly.20260101`, toolchain: { l1Artifacts: `${line.slice(1)}.1.0` } }; + fs.mkdirSync(path.join(rootDir, "docs/lines", line), { recursive: true }); + fs.writeFileSync(path.join(rootDir, "docs/lines", line, "aztec-node.json"), JSON.stringify(pin)); + } + }); + afterEach(() => fs.rmSync(rootDir, { recursive: true, force: true })); + const page = (line) => path.join(rootDir, "docs/lines", line, "docs-developers/page.md"); + + it("takes its versions from the line's pin and its API folder from the network on its major", async () => { + setEnv({ RELEASE_TYPE: "mainnet", COMMIT_TAG: "v9.9.9" }); + const { content } = await preprocessIncludeVersion( + "#include_aztec_version #include_foundation_version #api_ref_version", + page(`v${major}`), + rootDir, + ); + assert.strictEqual(content, `v${major}.1.0-nightly.20260101 v${major}.1.0 mainnet`); + }); + + it("fails closed on #api_ref_version when no network runs the line's major", async () => { + await assert.rejects(preprocessIncludeVersion("#api_ref_version", page("v999"), rootDir), /no network runs v999.*retire the line/); + assert.strictEqual((await preprocessIncludeVersion("#include_aztec_version", page("v999"), rootDir)).content, "v999.1.0-nightly.20260101"); + }); + }); + describe("integration", () => { it("processes conditionals before version macros", async () => { setEnv({ RELEASE_TYPE: "devnet", DEVNET_TAG: "3.0.0-devnet.5" }); diff --git a/docs/src/preprocess/__tests__/line.test.js b/docs/src/preprocess/__tests__/line.test.js new file mode 100644 index 00000000000..74ba7441476 --- /dev/null +++ b/docs/src/preprocess/__tests__/line.test.js @@ -0,0 +1,22 @@ +const { it } = require("node:test"); +const assert = require("node:assert"); +const path = require("path"); +const { lineLinks, lineOf } = require("../line"); + +it("finds a page's line from its path, and moves only its dev-docs links onto that line", () => { + const root = "/repo"; + const line = lineOf("/repo/docs/lines/v6/docs-operate/operators/index.mdx", root); + assert.deepStrictEqual(line, { name: "v6", major: 6, root: path.join(root, "docs/lines/v6") }); + assert.deepStrictEqual(lineOf("docs/lines/v6/examples/a.ts", root), line); + for (const main of ["/repo/docs/docs-operate/index.mdx", "docs/lines/README.md", "/elsewhere/docs/lines/v6/a.md"]) + assert.strictEqual(lineOf(main, root), null, main); + + const page = + "[a](/operate/operators/setup) [b](/developers/docs/x#y) <a href=\"/operate/operators\">c</a> " + + "[d](/participate/x) [e](https://docs.aztec.network/operate/x) [f](/developers-v6/x)"; + assert.strictEqual( + lineLinks(page, line), + "[a](/operate-v6/operators/setup) [b](/developers-v6/docs/x#y) <a href=\"/operate-v6/operators\">c</a> " + + "[d](/participate/x) [e](https://docs.aztec.network/operate/x) [f](/developers-v6/x)", + ); +}); diff --git a/docs/src/preprocess/include_code.js b/docs/src/preprocess/include_code.js index 45761ee7bc7..72a6371db65 100644 --- a/docs/src/preprocess/include_code.js +++ b/docs/src/preprocess/include_code.js @@ -1,6 +1,7 @@ const fs = require("fs"); const path = require("path"); const { execFileSync } = require("child_process"); +const { lineOf, readPin } = require("./line"); // Snippets included from an installed npm package must not link to the local // node_modules path: the source link instead points at the repo and tag the @@ -15,16 +16,19 @@ const { execFileSync } = require("child_process"); // release rather than the docs release. const PUBLISHED_SNIPPET_SOURCES = [ { - pathPrefix: "docs/node_modules/@aztec-foundation/l1-artifacts/", + // Main's installed devDependency, or a line's copy (`aztec_node.mts --line <line> provision l1-artifacts`). + pathPrefix: /^docs\/(?:node_modules\/@aztec-foundation|lines\/v\d+\/\.aztec-node)\/l1-artifacts\//, repoUrl: "https://github.com/AztecProtocol/aztec-packages", }, ]; -// labs/<path> snippets are read from the checkout of the commit docs/aztec-node.json pins +// labs/<path> snippets are read from the checkout of the commit the page's pin names // (docs/scripts/aztec_node.mts), never from the labs/ submodule, and link to that commit. const LABS_PREFIX = "labs/"; -function aztecNodeDir(rootDir) { +// AZTEC_NODE_DIR names main's checkout; a line always reads its own. +function aztecNodeDir(rootDir, line) { + if (line) return path.join(line.root, ".aztec-node", "tree"); return ( process.env.AZTEC_NODE_DIR || path.join(rootDir, "docs", ".aztec-node", "tree") @@ -34,12 +38,12 @@ function aztecNodeDir(rootDir) { // Checked once per process before the first labs/ snippet is read: an edited, untracked or stale // file would render code the linked commit does not have. const checkedCheckouts = new Set(); -function checkAztecNode(rootDir) { - const key = `${rootDir}\0${aztecNodeDir(rootDir)}`; +function checkAztecNode(rootDir, line) { + const key = `${rootDir}\0${aztecNodeDir(rootDir, line)}`; if (checkedCheckouts.has(key)) return; const tool = path.join(rootDir, "docs", "scripts", "aztec_node.mts"); try { - execFileSync(process.execPath, [tool, "check"], { + execFileSync(process.execPath, [tool, ...(line ? ["--line", line.name] : []), "check"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); @@ -51,23 +55,28 @@ function checkAztecNode(rootDir) { checkedCheckouts.add(key); } -function pinnedCommit(rootDir) { - return JSON.parse( - fs.readFileSync(path.join(rootDir, "docs", "aztec-node.json"), "utf8"), - ).commit; -} - -function snippetFilePath(relativeCodeFilePath, rootDir) { +function snippetFilePath(relativeCodeFilePath, rootDir, line) { if (!relativeCodeFilePath.startsWith(LABS_PREFIX)) { return path.join(rootDir, relativeCodeFilePath); } - checkAztecNode(rootDir); + checkAztecNode(rootDir, line); return path.join( - aztecNodeDir(rootDir), + aztecNodeDir(rootDir, line), relativeCodeFilePath.slice(LABS_PREFIX.length), ); } +// Main's examples and l1-artifacts, and each line's files, have a copy per line: a page quoting +// another line's copy would show code its readers do not run. +const MAIN_ONLY = /^docs\/(?:examples|node_modules\/@aztec-foundation\/l1-artifacts)\//; +function checkSnippetLine(relativeCodeFilePath, rootDir, line) { + const owner = lineOf(relativeCodeFilePath, rootDir); + if (!owner && !MAIN_ONLY.test(relativeCodeFilePath)) return; + if (owner?.name === line?.name) return; + const whose = (l) => (l ? `line ${l.name}'s` : "main's"); + throw new Error(`${relativeCodeFilePath} is ${whose(owner)}, but this page is ${whose(line)}: quote the page's own copy`); +} + // This repository's own files (docs/examples/...) link to the aztec-packages commit being built: // DOCS_COMMIT, else HEAD. A release build sets DOCS_COMMIT to the cut's base on origin/next, since a // local HEAD may exist nowhere upstream; RELEASE_TYPE is unset (CI, Netlify) or nightly otherwise. @@ -92,34 +101,30 @@ function docsCommit(rootDir) { return headCommits.get(rootDir); } -function resolveSnippetSource(relativeCodeFilePath, rootDir) { - const published = PUBLISHED_SNIPPET_SOURCES.find((source) => - relativeCodeFilePath.startsWith(source.pathPrefix), - ); - if (!published) { - if (relativeCodeFilePath.startsWith(LABS_PREFIX)) { - return { - repoUrl: "https://github.com/aztec-labs-eng/aztec-node", - ref: pinnedCommit(rootDir), - displayPath: relativeCodeFilePath.slice(LABS_PREFIX.length), - }; - } +function resolveSnippetSource(relativeCodeFilePath, rootDir, line = null) { + for (const { pathPrefix, repoUrl } of PUBLISHED_SNIPPET_SOURCES) { + const prefix = pathPrefix.exec(relativeCodeFilePath)?.[0]; + if (!prefix) continue; + const { version } = JSON.parse( + fs.readFileSync(path.join(rootDir, prefix, "package.json"), "utf8"), + ); return { - repoUrl: "https://github.com/AztecProtocol/aztec-packages", - ref: docsCommit(rootDir), - displayPath: relativeCodeFilePath, + repoUrl, + ref: `v${version}`, + displayPath: relativeCodeFilePath.slice(prefix.length), + }; + } + if (relativeCodeFilePath.startsWith(LABS_PREFIX)) { + return { + repoUrl: "https://github.com/aztec-labs-eng/aztec-node", + ref: readPin(rootDir, line).commit, + displayPath: relativeCodeFilePath.slice(LABS_PREFIX.length), }; } - const { version } = JSON.parse( - fs.readFileSync( - path.join(rootDir, published.pathPrefix, "package.json"), - "utf8", - ), - ); return { - repoUrl: published.repoUrl, - ref: `v${version}`, - displayPath: relativeCodeFilePath.slice(published.pathPrefix.length), + repoUrl: "https://github.com/AztecProtocol/aztec-packages", + ref: docsCommit(rootDir), + displayPath: relativeCodeFilePath, }; } @@ -342,6 +347,7 @@ const regex = /^(?!<!--.*)(?=.*#include_code\s+(\S+)\s+(\S+)\s+(\S+)(?:[ ]+(\S+))?).*$/gm; async function preprocessIncludeCode(markdownContent, filePath, rootDir) { + const line = lineOf(filePath, rootDir); // Process each include tag in the current markdown file let updatedContent = markdownContent; let matchesFound = false; @@ -371,9 +377,10 @@ async function preprocessIncludeCode(markdownContent, filePath, rootDir) { .resolve(rootDir, codeFilePath) .replace(/^\//, ""); + checkSnippetLine(relativeCodeFilePath, rootDir, line); // Extract the code snippet between the specified comments const [codeSnippet, startLine, endLine] = extractCodeSnippet( - snippetFilePath(relativeCodeFilePath, rootDir), + snippetFilePath(relativeCodeFilePath, rootDir, line), identifier, filePath, ); @@ -381,6 +388,7 @@ async function preprocessIncludeCode(markdownContent, filePath, rootDir) { const { repoUrl, ref, displayPath } = resolveSnippetSource( relativeCodeFilePath, rootDir, + line, ); const urlText = `${displayPath}#L${startLine}-L${endLine}`; const url = `${repoUrl}/blob/${ref}/${urlText}`; diff --git a/docs/src/preprocess/include_version.js b/docs/src/preprocess/include_version.js index 9ee1b171474..2a4680be7ef 100644 --- a/docs/src/preprocess/include_version.js +++ b/docs/src/preprocess/include_version.js @@ -2,6 +2,8 @@ const fs = require("fs"); const path = require("path"); const { processConditionalBlocks } = require("./conditional_content"); const { releaseNameFor, versionLabel } = require("../release_names"); +const { lineApiFolder } = require("../network_docs"); +const { lineOf, readPin } = require("./line"); // Load version defaults from config file (source of truth for version→type mapping) const developerVersionConfig = require(path.join(__dirname, "../../developer_version_config.json")); @@ -77,6 +79,17 @@ function foundationVersion() { return `v${JSON.parse(fs.readFileSync(pkg, "utf8")).version}`; } +// A line's API links read the reference a network on its major publishes; another major's would +// document a different API. +function lineApiRefVersion(line) { + const folder = lineApiFolder(developerVersionConfig, line.major); + if (!folder) + throw new Error( + `no network runs v${line.major}, so line ${line.name} has no published API reference: retire the line (git rm -r docs/lines/${line.name}) or keep a reference for it`, + ); + return folder; +} + function pinnedAztecNodeRef() { return JSON.parse(fs.readFileSync(path.join(__dirname, "../../aztec-node.json"), "utf8")).ref; } @@ -84,11 +97,15 @@ function pinnedAztecNodeRef() { /** * Preprocesses markdown content to replace version macros and conditional blocks. * @param {string} markdownContent - The markdown content to process - * @param {string} filePath - Path to the file (for error messages) + * @param {string} filePath - Path to the file (for error messages, and the line it belongs to) + * @param {string} [rootDir] - The repository root; without it every page is main's * @returns {Promise<{content: string, isUpdated: boolean}>} - Processed content and update flag */ -async function preprocessIncludeVersion(markdownContent, filePath = "unknown") { +async function preprocessIncludeVersion(markdownContent, filePath = "unknown", rootDir = undefined) { const originalContent = markdownContent; + // A line page names its own pin, whatever a release build sets for main's pages. + const line = lineOf(filePath, rootDir); + const linePin = line && readPin(rootDir, line); // Get environment variables, falling back to version config file defaults // COMMIT_TAG: kept for backwards compatibility with #include_aztec_version @@ -101,7 +118,7 @@ async function preprocessIncludeVersion(markdownContent, filePath = "unknown") { const releaseType = process.env.RELEASE_TYPE || "mainnet"; // Release cuts set COMMIT_TAG to the Labs tag; any other build names the pinned aztec-node tag, // the code the pages quote. - const commitTag = process.env.COMMIT_TAG || pinnedAztecNodeRef(); + const commitTag = linePin ? linePin.ref : process.env.COMMIT_TAG || pinnedAztecNodeRef(); // Validate release type if (!VALID_RELEASE_TYPES.includes(releaseType)) { @@ -146,9 +163,10 @@ async function preprocessIncludeVersion(markdownContent, filePath = "unknown") { `#release_name`, releaseNameFor(releaseVersion) || `v${releaseVersion}`, ); - markdownContent = markdownContent.replaceAll(`#api_ref_version`, apiRefVersion); + if (markdownContent.includes("#api_ref_version")) + markdownContent = markdownContent.replaceAll(`#api_ref_version`, line ? lineApiRefVersion(line) : apiRefVersion); if (markdownContent.includes("#include_foundation_version")) { - const foundation = foundationVersion(); + const foundation = linePin ? `v${linePin.toolchain.l1Artifacts}` : foundationVersion(); // The prefixless name first: the other macro is a prefix of it. markdownContent = markdownContent.replaceAll("#include_foundation_version_without_prefix", foundation.slice(1)); markdownContent = markdownContent.replaceAll("#include_foundation_version", foundation); diff --git a/docs/src/preprocess/index.js b/docs/src/preprocess/index.js index 720feb3a917..9d5cdb51439 100644 --- a/docs/src/preprocess/index.js +++ b/docs/src/preprocess/index.js @@ -5,6 +5,7 @@ const childProcess = require("child_process"); const { preprocessIncludeCode } = require("./include_code"); const { preprocessIncludeVersion } = require("./include_version"); const { syncVersionsFile } = require("../network_docs"); +const { lineLinks, lineNames, lineOf } = require("./line"); // const { generateInstructionSet } = require("./InstructionSet/genMarkdown"); // Removed with protocol-specs @@ -31,6 +32,12 @@ async function processMarkdownFilesInDir(rootDir, docsDir, regex) { updatedContent = result.content; isUpdated = isUpdated || result.isUpdated; } + const line = lineOf(filepath, rootDir); + if (line) { + const linked = lineLinks(updatedContent, line); + isUpdated = isUpdated || linked !== updatedContent; + updatedContent = linked; + } contentUpdates.push({ content: updatedContent, @@ -134,24 +141,23 @@ async function run() { const baseDestDir = path.join(rootDir, "docs", "processed-docs"); const baseCachedDestDir = path.join(rootDir, "docs", "processed-docs-cache"); - // Process all docs directories that may contain macros - const docsDirs = [ - path.join(rootDir, "docs", "docs"), - path.join(rootDir, "docs", "docs-developers"), - path.join(rootDir, "docs", "docs-operate"), - path.join(rootDir, "docs", "docs-participate"), - ]; + // Process all docs directories that may contain macros, each to its own path under + // processed-docs/: docs/, docs-developers/, ..., and lines/<line>/docs-developers/ for a line. + // Production serves no dev docs, and a line's checkout is not fetched there. + const lineDirs = process.env.CONTEXT === "production" ? [] : lineNames(rootDir).flatMap((line) => + ["docs-developers", "docs-operate"].map((dir) => path.join("lines", line, dir)), + ); + const docsDirs = ["docs", "docs-developers", "docs-operate", "docs-participate", ...lineDirs]; - for (const docsDir of docsDirs) { + for (const dirName of docsDirs) { + const docsDir = path.join(docsRoot, dirName); if (!fs.existsSync(docsDir)) { console.log(`Skipping ${docsDir} (does not exist)`); continue; } - const dirName = path.basename(docsDir); console.log(`Processing ${dirName}...`); - // Maintain directory separation: processed-docs/docs/, processed-docs/docs-developers/, etc. const destDir = path.join(baseDestDir, dirName); const cachedDestDir = path.join(baseCachedDestDir, dirName); diff --git a/docs/src/preprocess/line.js b/docs/src/preprocess/line.js new file mode 100644 index 00000000000..937dae84caa --- /dev/null +++ b/docs/src/preprocess/line.js @@ -0,0 +1,42 @@ +// A dev line is a directory docs/lines/<name>/ with its own aztec-node pin, pages and examples, +// following aztec-node's <name> branch. Everything else under docs/ is main's or shared. +const fs = require("fs"); +const path = require("path"); + +const LINE_PATH = /^docs\/lines\/(v(\d+))\//; +const LINE_NAME = /^v\d+$/; + +/** + * The line a file belongs to, from its path. + * + * @param {string} filePath absolute, or relative to `rootDir` + * @param {string | undefined} rootDir the repository root + * @returns {{name: string, major: number, root: string} | null} null for main's and shared files + */ +function lineOf(filePath, rootDir) { + if (!rootDir) return null; + const rel = path.relative(rootDir, path.resolve(rootDir, filePath)).split(path.sep).join("/"); + const m = LINE_PATH.exec(rel); + return m ? { name: m[1], major: Number(m[2]), root: path.join(rootDir, "docs", "lines", m[1]) } : null; +} + +/** The lines, as scripts/aztec_node.mts finds them: docs/lines/v<major>/ directories holding a pin. */ +function lineNames(rootDir) { + const dir = path.join(rootDir, "docs", "lines"); + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir).filter((l) => LINE_NAME.test(l) && fs.existsSync(path.join(dir, l, "aztec-node.json"))).sort(); +} + +/** The aztec-node pin a page quotes: its line's, else docs/aztec-node.json. */ +function readPin(rootDir, line) { + const dir = line ? line.root : path.join(rootDir, "docs"); + return JSON.parse(fs.readFileSync(path.join(dir, "aztec-node.json"), "utf8")); +} + +// Absolute links into the dev docs, which the line's own instances serve at /<base>-<line>/. +const DOCS_LINK = /(\]\(|href=["'])\/(developers|operate)\//g; + +/** `content` with its links into the dev docs moved onto `line`'s instances. */ +const lineLinks = (content, line) => content.replace(DOCS_LINK, (_, lead, base) => `${lead}/${base}-${line.name}/`); + +module.exports = { lineLinks, lineNames, lineOf, readPin }; From ce0f7610a89db689362959c4d3dd8a5c093ce2cb Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Thu, 5 Nov 2026 16:16:07 +0000 Subject: [PATCH 09/32] test(docs): keep the line test's external link off docs.aztec.network check_source_urls requires every docs.aztec.network URL in tracked files to resolve. --- docs/src/preprocess/__tests__/line.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/preprocess/__tests__/line.test.js b/docs/src/preprocess/__tests__/line.test.js index 74ba7441476..1d6ad9efeee 100644 --- a/docs/src/preprocess/__tests__/line.test.js +++ b/docs/src/preprocess/__tests__/line.test.js @@ -13,10 +13,10 @@ it("finds a page's line from its path, and moves only its dev-docs links onto th const page = "[a](/operate/operators/setup) [b](/developers/docs/x#y) <a href=\"/operate/operators\">c</a> " + - "[d](/participate/x) [e](https://docs.aztec.network/operate/x) [f](/developers-v6/x)"; + "[d](/participate/x) [e](https://example.com/operate/x) [f](/developers-v6/x)"; assert.strictEqual( lineLinks(page, line), "[a](/operate-v6/operators/setup) [b](/developers-v6/docs/x#y) <a href=\"/operate-v6/operators\">c</a> " + - "[d](/participate/x) [e](https://docs.aztec.network/operate/x) [f](/developers-v6/x)", + "[d](/participate/x) [e](https://example.com/operate/x) [f](/developers-v6/x)", ); }); From c4ff022ea282553dd6450d0799f83bf9815266df Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Thu, 5 Nov 2026 16:16:07 +0000 Subject: [PATCH 10/32] feat(docs): build and check a dev line's examples and references examples/bootstrap.sh --line <line> builds the line's examples with its own pin. nargo takes the topmost Nargo.toml as its workspace, so the build runs in a copy outside the repository that mirrors docs/. The example scripts read DOCS_ROOT instead of a hardcoded docs/. check_doc_references.sh docs/lines/<line> and update_docs.sh --line <line> check the line's references in the line's own checkout. Main's examples build as before; execute stays main-only. --- docs/examples/bootstrap.sh | 88 ++++++++++++++----- docs/examples/ts/aztecjs_runner/run.sh | 10 +-- docs/examples/ts/bootstrap.sh | 14 +-- .../__tests__/check_doc_references.test.mts | 71 +++++++++++++++ .../update_docs.sh | 20 +++-- docs/scripts/check_doc_references.sh | 7 +- docs/scripts/lib/aztec_node.sh | 18 ++-- 7 files changed, 184 insertions(+), 44 deletions(-) create mode 100644 docs/scripts/__tests__/check_doc_references.test.mts diff --git a/docs/examples/bootstrap.sh b/docs/examples/bootstrap.sh index 902d142eeb9..5c568708bd4 100755 --- a/docs/examples/bootstrap.sh +++ b/docs/examples/bootstrap.sh @@ -8,10 +8,45 @@ REPO_ROOT=$(git rev-parse --show-toplevel) # dependencies name the default checkout (docs/.aztec-node/tree), so an AZTEC_NODE_DIR override never applies. unset AZTEC_NODE_DIR +# `--line <line> [cmd]` builds a dev line's examples (docs/lines/<line>/) with its pin instead of main's. +# nargo takes the topmost Nargo.toml as its workspace, so a line cannot build under docs/: its +# Nargo.toml and examples are staged outside the repository in a copy that mirrors docs/, with the +# line's .aztec-node (checkout and toolchain) and l1-artifacts linked in where docs/ has its own. +# DOCS_ROOT is where the examples build; DOCS_PATH is their source's path in the repository. +DOCS_LINE= STAGE= +aztec_node=(node "$REPO_ROOT/docs/scripts/aztec_node.mts") +if [ "$cmd" = --line ]; then + DOCS_LINE=${1:?usage: examples/bootstrap.sh --line <line> [cmd]} + shift + cmd=${1:-} + [ -n "$cmd" ] && shift + export DOCS_PATH=docs/lines/$DOCS_LINE + line_root=$REPO_ROOT/$DOCS_PATH + if [ ! -f "$line_root/aztec-node.json" ]; then + echo_stderr "No line $DOCS_LINE: $DOCS_PATH/aztec-node.json does not exist." + exit 1 + fi + aztec_node+=(--line "$DOCS_LINE") + # On disk rather than in /tmp: the contract artifacts are large. + mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" + STAGE=$(mktemp -d "${XDG_CACHE_HOME:-$HOME/.cache}/aztec-docs-$DOCS_LINE.XXXXXX") + cp -R "$line_root/Nargo.toml" "$line_root/examples" "$REPO_ROOT/docs/.yarnrc.yml" "$STAGE/" + mkdir -p "$line_root/.aztec-node/l1-artifacts" "$STAGE/node_modules/@aztec-foundation" + ln -s "$line_root/.aztec-node" "$STAGE/.aztec-node" + ln -s "$line_root/.aztec-node/l1-artifacts" "$STAGE/node_modules/@aztec-foundation/l1-artifacts" + export DOCS_ROOT=$STAGE +else + export DOCS_PATH=docs DOCS_ROOT=$REPO_ROOT/docs +fi + # Safety net: ensure all TS example yarn.lock files are empty on exit. # Both validate-ts and execute-examples (via Docker volume mount) can populate # these files, and their per-project cleanup may not run if processes are killed. -trap 'for lf in "$REPO_ROOT"/docs/examples/ts/*/yarn.lock; do [ -f "$lf" ] && > "$lf"; done' EXIT +function on_exit { + for lf in "$DOCS_ROOT"/examples/ts/*/yarn.lock; do [ -f "$lf" ] && > "$lf"; done + if [ -n "$STAGE" ]; then rm -rf "$STAGE"; fi +} +trap on_exit EXIT # The pin names the aztec-node commit and every toolchain version, so it keys the examples. hash=$(cache_content_hash .rebuild_patterns) @@ -19,10 +54,10 @@ hash=$(cache_content_hash .rebuild_patterns) # Fetches the pinned checkout, installs the pinned nargo, bb and aztec CLI, and exports NARGO, BB, AZTEC # and the versions. Each step is a local check when already current, so concurrent test commands only read. function pin_toolchain { - local tool="$REPO_ROOT/docs/scripts/aztec_node.mts" pin - node "$tool" fetch - node "$tool" provision nargo bb cli - pin=$(node "$tool" env) + local pin + "${aztec_node[@]}" fetch + "${aztec_node[@]}" provision nargo bb cli + pin=$("${aztec_node[@]}" env) eval "$pin" } @@ -46,7 +81,7 @@ export -f compile_contract function compile-circuits { echo_header "Compiling vanilla Noir circuits" pin_toolchain - local CIRCUITS_DIR="$REPO_ROOT/docs/examples/circuits" + local CIRCUITS_DIR="$DOCS_ROOT/examples/circuits" if [ ! -d "$CIRCUITS_DIR" ]; then echo_stderr "No circuits directory found at $CIRCUITS_DIR" @@ -66,7 +101,7 @@ function compile-circuits { local name=$(basename "$circuit") if [ -f "$circuit/Nargo.toml" ]; then echo_stderr " Compiling $name..." - (cd "$REPO_ROOT/docs" && $NARGO compile --package "$name") + (cd "$DOCS_ROOT" && $NARGO compile --package "$name") fi done } @@ -74,7 +109,7 @@ function compile-circuits { function compile { echo_header "Compiling example contracts" pin_toolchain - local CONTRACTS_DIR="$REPO_ROOT/docs/examples/contracts" + local CONTRACTS_DIR="$DOCS_ROOT/examples/contracts" if [ ! -d "$CONTRACTS_DIR" ]; then echo_stderr "No contracts directory found at $CONTRACTS_DIR" @@ -102,21 +137,26 @@ function compile { # nargo resolves packages from the docs/ workspace, so it runs there. ( - cd "$REPO_ROOT/docs" + cd "$DOCS_ROOT" parallel -j"${PARALLELISM:-4}" --halt now,fail=1 --line-buffer --tag compile_contract examples/{} ::: "${contracts[@]}" ) } function compile-solidity { echo_header "Compiling Solidity examples" - local SOLIDITY_DIR="$REPO_ROOT/docs/examples/solidity" - local OUTPUT_DIR="$REPO_ROOT/docs/target/solidity" + local SOLIDITY_DIR="$DOCS_ROOT/examples/solidity" + local OUTPUT_DIR="$DOCS_ROOT/target/solidity" # The examples compile against the l1-contracts sources shipped in the # @aztec-foundation/l1-artifacts package (see foundry.toml), so docs' node_modules # must be populated. Unconditional so that a pin bump refreshes an - # existing node_modules instead of compiling against stale sources. - (cd "$REPO_ROOT/docs" && npm_install_deps) + # existing node_modules instead of compiling against stale sources. A line's + # copy is the one its fetch provisions, linked into the stage. + if [ -n "$DOCS_LINE" ]; then + "${aztec_node[@]}" fetch + else + (cd "$DOCS_ROOT" && npm_install_deps) + fi # Find all .sol files recursively local sol_files @@ -152,14 +192,15 @@ function compile-solidity { function validate-ts { echo_header "Validating TypeScript examples" pin_toolchain - (cd ts && ./bootstrap.sh "$@") + # It validates the projects under $DOCS_ROOT/examples/ts. + (cd "$REPO_ROOT/docs/examples/ts" && ./bootstrap.sh "$@") } function validate-webapp-tutorial { echo_header "Validating webapp-tutorial build" pin_toolchain - local TUTORIAL_DIR="$REPO_ROOT/docs/examples/webapp-tutorial" - local ARTIFACTS_DIR="$REPO_ROOT/docs/target" + local TUTORIAL_DIR="$DOCS_ROOT/examples/webapp-tutorial" + local ARTIFACTS_DIR="$DOCS_ROOT/target" compile webapp-tutorial/contracts @@ -234,8 +275,13 @@ function validate-webapp-tutorial { function execute-examples { echo_header "Executing TypeScript documentation examples" + # The compose file mounts the repository and runs main's examples. + if [ -n "$DOCS_LINE" ]; then + echo_stderr "execute runs main's examples only: a line's run against a network at its cut." + return 1 + fi pin_toolchain - local COMPOSE_DIR="$REPO_ROOT/docs/examples/ts" + local COMPOSE_DIR="$DOCS_ROOT/examples/ts" run_compose_test "docs_examples" "docs-examples" "$COMPOSE_DIR" } @@ -248,9 +294,9 @@ function test-contracts { pin_toolchain local test_packages=() local member - for member in $(sed -n -E 's/^ *"(examples\/[^"]+)",?$/\1/p' "$REPO_ROOT/docs/Nargo.toml"); do - if grep -rqs '#\[test' "$REPO_ROOT/docs/$member/src"; then - test_packages+=("$(sed -n -E 's/^name *= *"([^"]+)".*/\1/p' "$REPO_ROOT/docs/$member/Nargo.toml")") + for member in $(sed -n -E 's/^ *"(examples\/[^"]+)",?$/\1/p' "$DOCS_ROOT/Nargo.toml"); do + if grep -rqs '#\[test' "$DOCS_ROOT/$member/src"; then + test_packages+=("$(sed -n -E 's/^name *= *"([^"]+)".*/\1/p' "$DOCS_ROOT/$member/Nargo.toml")") fi done local txe_port=${TXE_PORT:-14745} @@ -282,7 +328,7 @@ function test-contracts { export RAYON_NUM_THREADS=1 export NARGO_FOREIGN_CALL_TIMEOUT=300000 - cd "$REPO_ROOT/docs" + cd "$DOCS_ROOT" for pkg in "${test_packages[@]}"; do echo_stderr "Running $pkg..." $NARGO test --skip-brillig-constraints-check \ diff --git a/docs/examples/ts/aztecjs_runner/run.sh b/docs/examples/ts/aztecjs_runner/run.sh index 359f809f057..11ede045f43 100755 --- a/docs/examples/ts/aztecjs_runner/run.sh +++ b/docs/examples/ts/aztecjs_runner/run.sh @@ -19,12 +19,12 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" EXAMPLES_DIR="$(dirname "$SCRIPT_DIR")" source "$EXAMPLES_DIR/lib.sh" -# Derive repo root from known path (docs/examples/ts/aztecjs_runner) to avoid -# git safe.directory failures when running inside Docker containers. -REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +# The docs tree from this script's known path (<docs>/examples/ts/aztecjs_runner), not git, which +# fails safe.directory inside Docker; a line's stage (examples/bootstrap.sh --line) mirrors docs/. +DOCS_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" export AZTEC_NODE_URL="${AZTEC_NODE_URL:-http://localhost:8080}" # In the compose run the host's CLI path does not exist, so the default names it inside the mount. -AZTEC="${AZTEC:-$REPO_ROOT/docs/.aztec-node/cli/node_modules/.bin/aztec}" +AZTEC="${AZTEC:-$DOCS_ROOT/.aztec-node/cli/node_modules/.bin/aztec}" # Colors for output RED='\033[0;31m' @@ -81,7 +81,7 @@ setup_project() { contract_count="$(yq eval '.contracts | length' config.yaml 2>/dev/null || echo "0")" if [ "$contract_count" -gt 0 ]; then - local ARTIFACTS_DIR="$REPO_ROOT/docs/target" + local ARTIFACTS_DIR="$DOCS_ROOT/target" while IFS= read -r contract_name; do local artifact="$ARTIFACTS_DIR/${contract_name}.json" diff --git a/docs/examples/ts/bootstrap.sh b/docs/examples/ts/bootstrap.sh index 959705cb6c2..ea5d3c4276b 100755 --- a/docs/examples/ts/bootstrap.sh +++ b/docs/examples/ts/bootstrap.sh @@ -5,7 +5,11 @@ source "$(git rev-parse --show-toplevel)/ci3/source_bootstrap" source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" export REPO_ROOT=$(git rev-parse --show-toplevel) -export ARTIFACTS_DIR="$REPO_ROOT/docs/target" +# The docs tree to validate, set by examples/bootstrap.sh: docs/, or a line's stage outside the +# repository (which is why this runs from docs/examples/ts and moves there only after the ci3 setup). +export DOCS_ROOT=${DOCS_ROOT:-$REPO_ROOT/docs} DOCS_PATH=${DOCS_PATH:-docs} +export ARTIFACTS_DIR="$DOCS_ROOT/target" +cd "$DOCS_ROOT/examples/ts" : "${AZTEC:?the pinned aztec CLI is not exported: run docs/examples/bootstrap.sh validate-ts}" # Each project starts from an empty yarn.lock that its own install fills. ci3 exports CI=0, which @@ -172,11 +176,11 @@ validate_project() { yarn add -D "typescript@^5.3.3" # Create tsconfig.json from template - if [ ! -f "$REPO_ROOT/docs/examples/ts/tsconfig.template.json" ]; then - echo_stderr "ERROR: tsconfig template not found at '$REPO_ROOT/docs/examples/ts/tsconfig.template.json'" + if [ ! -f "$DOCS_ROOT/examples/ts/tsconfig.template.json" ]; then + echo_stderr "ERROR: tsconfig template not found at '$DOCS_ROOT/examples/ts/tsconfig.template.json'" return 1 fi - cp "$REPO_ROOT/docs/examples/ts/tsconfig.template.json" tsconfig.json + cp "$DOCS_ROOT/examples/ts/tsconfig.template.json" tsconfig.json # Type check echo_stderr "Type checking '${project_name}'..." @@ -209,7 +213,7 @@ get_all_projects() { if [ "${CI:-0}" != "0" ]; then for lockfile in */yarn.lock; do [ -f "$lockfile" ] || continue - if [ -n "$(git show HEAD:"docs/examples/ts/$lockfile" 2>/dev/null)" ]; then + if [ -n "$(git -C "$REPO_ROOT" show HEAD:"$DOCS_PATH/examples/ts/$lockfile" 2>/dev/null)" ]; then echo_stderr "ERROR: $lockfile is not empty in git. These files must be committed empty." echo_stderr " Run: > $lockfile && git add $lockfile" exit 1 diff --git a/docs/scripts/__tests__/check_doc_references.test.mts b/docs/scripts/__tests__/check_doc_references.test.mts new file mode 100644 index 00000000000..3e082aa60ab --- /dev/null +++ b/docs/scripts/__tests__/check_doc_references.test.mts @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { after, before, test } from "node:test"; + +const scripts = path.resolve(import.meta.dirname, ".."); +const SCRIPTS = ["check_doc_references.sh", "lib/extract_doc_references.sh", "lib/aztec_node.sh", "aztec_node.mts"]; +const git = (cwd: string, ...args: string[]) => + execFileSync("git", ["-C", cwd, "-c", "commit.gpgsign=false", "-c", "user.name=t", "-c", "user.email=t@t", ...args], { encoding: "utf8" }).trim(); + +function write(file: string, text: string) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, text); +} + +// A pinned checkout holding `files`, and the pin that names it. +function pinned(stateRoot: string, ref: string, files: string[]) { + const tree = path.join(stateRoot, ".aztec-node/tree"); + write(path.join(tree, "labs-aztec-toolchain/bootstrap.sh"), "BB_VERSION=1.0.0\nNOIR_VERSION=0.1.0\n"); + for (const file of files) write(path.join(tree, file), "export {};\n"); + git(tree, "init", "-q"); + git(tree, "add", "-A"); + git(tree, "commit", "-q", "-m", ref); + const pin = { + repository: "https://github.com/aztec-labs-eng/aztec-node", + ref, + commit: git(tree, "rev-parse", "HEAD"), + toolchain: { aztec: ref.slice(1), bb: "1.0.0", noir: "0.1.0", l1Artifacts: "1.0.0" }, + noirArchives: { "x86_64-unknown-linux-gnu": `sha256:${"0".repeat(64)}` }, + }; + write(path.join(stateRoot, "aztec-node.json"), JSON.stringify(pin)); +} + +const page = (refs: string[]) => `---\ntitle: p\nreferences: [${refs.map((r) => `"${r}"`).join(", ")}]\n---\n`; + +// The repository's docs/ with main's checkout holding a file the v1 line's checkout lacks, and a +// page in each referencing it. +let root: string; +before(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), "doc-references-")); + git(root, "init", "-q"); + const docs = path.join(root, "docs"); + for (const script of SCRIPTS) { + fs.mkdirSync(path.dirname(path.join(docs, "scripts", script)), { recursive: true }); + fs.copyFileSync(path.join(scripts, script), path.join(docs, "scripts", script)); + } + write(path.join(docs, "package.json"), JSON.stringify({ devDependencies: { "@aztec-foundation/l1-artifacts": "1.0.0" } })); + pinned(docs, "v1.1.0", ["yarn-project/shared.ts", "yarn-project/main_only.ts"]); + const line = path.join(docs, "lines/v1"); + pinned(line, "v1.0.0", ["yarn-project/shared.ts"]); + write(path.join(line, ".aztec-node/l1-artifacts/package.json"), '{"version":"1.0.0"}'); + const refs = page(["labs/yarn-project/shared.ts", "labs/yarn-project/main_only.ts"]); + write(path.join(docs, "docs-developers/page.md"), refs); + write(path.join(line, "docs-developers/page.md"), refs); +}); +after(() => fs.rmSync(root, { recursive: true, force: true })); + +const check = (docsDir: string) => + spawnSync("bash", [path.join(root, "docs/scripts/check_doc_references.sh"), docsDir], { cwd: root, encoding: "utf8" }); + +test("a line's references are checked in the line's checkout, main's in main's", () => { + const main = check("docs"); + assert.equal(main.status, 0, main.stdout + main.stderr); + + const line = check("docs/lines/v1"); + assert.equal(line.status, 1, line.stdout + line.stderr); + assert.match(line.stdout, /labs\/yarn-project\/main_only\.ts \(referenced in docs\/lines\/v1\/docs-developers\/page\.md\)/); + assert.doesNotMatch(line.stdout, /shared\.ts \(/); +}); diff --git a/docs/scripts/aztecjs_reference_generation/update_docs.sh b/docs/scripts/aztecjs_reference_generation/update_docs.sh index 4cd4666f6af..b50caf83407 100755 --- a/docs/scripts/aztecjs_reference_generation/update_docs.sh +++ b/docs/scripts/aztecjs_reference_generation/update_docs.sh @@ -7,28 +7,38 @@ # ./scripts/aztecjs_reference_generation/update_docs.sh current # Updates current only # ./scripts/aztecjs_reference_generation/update_docs.sh v2.0.2 # Updates v2.0.2 only # ./scripts/aztecjs_reference_generation/update_docs.sh --check # Fails if current is stale +# ./scripts/aztecjs_reference_generation/update_docs.sh --line v6 --check # The same for a line's current page set -euo pipefail CHECK_ONLY=false TARGET_VERSION="" +LINE="" while [[ $# -gt 0 ]]; do case "$1" in --check) CHECK_ONLY=true ;; + --line) LINE="${2:?--line needs a line, like v6}"; shift ;; *) TARGET_VERSION="$1" ;; esac shift done +# A line has only its current page; its released pages are main's versioned docs. +TARGET_VERSION="${TARGET_VERSION:-${LINE:+current}}" TARGET_VERSION="${TARGET_VERSION:-all}" +if [[ -n "$LINE" && "$TARGET_VERSION" != current ]]; then + echo "Error: --line $LINE updates the line's current page only, not $TARGET_VERSION" >&2 + exit 1 +fi # Constants SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOCS_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -readonly SCRIPT_DIR DOCS_ROOT TARGET_VERSION +CURRENT_DIR="$DOCS_ROOT${LINE:+/lines/$LINE}/docs-developers/docs/aztec-js" +readonly SCRIPT_DIR DOCS_ROOT TARGET_VERSION LINE CURRENT_DIR # shellcheck source-path=SCRIPTDIR source=../lib/aztec_node.sh source "$SCRIPT_DIR/../lib/aztec_node.sh" -require_aztec_node +require_aztec_node "$LINE" readonly AZTEC_JS_SRC="$AZTEC_NODE_DIR/yarn-project/aztec.js/src" readonly OUTPUT_FILE="aztec_js_reference.md" readonly SIDEBAR_POSITION="98" @@ -98,7 +108,7 @@ tail -n +2 "$TEMP_MD" >> "$TEMP_WITH_FRONTMATTER" if [[ "$CHECK_ONLY" == true ]]; then echo "" echo "Step 3: Comparing against the committed reference..." - readonly COMMITTED="$DOCS_ROOT/docs-developers/docs/aztec-js/$OUTPUT_FILE" + readonly COMMITTED="$CURRENT_DIR/$OUTPUT_FILE" if [[ ! -f "$COMMITTED" ]]; then echo "Error: no committed reference at $COMMITTED" @@ -131,7 +141,7 @@ The committed Aztec.js reference no longer matches yarn-project/aztec.js/src. Regenerate it and commit the result: - cd docs && ./scripts/aztecjs_reference_generation/update_docs.sh current + cd docs && ./scripts/aztecjs_reference_generation/update_docs.sh ${LINE:+--line $LINE }current EOF exit 1 fi @@ -145,7 +155,7 @@ update_version() { local target_dir="" if [[ "$version" == "current" ]]; then - target_dir="$DOCS_ROOT/docs-developers/docs/aztec-js" + target_dir="$CURRENT_DIR" else target_dir="$DOCS_ROOT/developer_versioned_docs/version-${version}/docs/aztec-js" fi diff --git a/docs/scripts/check_doc_references.sh b/docs/scripts/check_doc_references.sh index d2113cd1cae..47d6ffdffed 100755 --- a/docs/scripts/check_doc_references.sh +++ b/docs/scripts/check_doc_references.sh @@ -9,7 +9,8 @@ set -euo pipefail # Usage: check_doc_references.sh [docs_dir] # # Arguments: -# docs_dir - (Optional) Documentation directory. Default: docs +# docs_dir - (Optional) Documentation directory, relative to the repository root. Default: docs. +# A line root (docs/lines/<line>) checks its labs/ references in the line's checkout. # # Reference Format: # - Individual files: "yarn-project/stdlib/src/interfaces/aztec-node.ts" @@ -27,10 +28,12 @@ source "$SCRIPT_DIR/lib/extract_doc_references.sh" source "$SCRIPT_DIR/lib/aztec_node.sh" DOCS_DIR="${1:-docs}" +LINE= +[[ "$DOCS_DIR" =~ ^docs/lines/(v[0-9]+)/?$ ]] && LINE=${BASH_REMATCH[1]} # labs/ references name aztec-node files at the pinned commit, so they are tested in its checkout; # the check refuses untracked files there, which would pass for deleted ones. -require_aztec_node +require_aztec_node "$LINE" # Extract all reference file paths from markdown frontmatter # Expected format: references: ["path/from/repo/root/file.ts", "another/file.ts"] diff --git a/docs/scripts/lib/aztec_node.sh b/docs/scripts/lib/aztec_node.sh index 4c0958a82b4..1849f29f86d 100644 --- a/docs/scripts/lib/aztec_node.sh +++ b/docs/scripts/lib/aztec_node.sh @@ -1,13 +1,19 @@ # shellcheck shell=bash # aztec_node - The pinned aztec-node checkout, for scripts that read aztec-node code # -# require_aztec_node checks the checkout against docs/aztec-node.json (scripts/aztec_node.mts -# check) and exports AZTEC_NODE_DIR, docs/.aztec-node/tree unless already set. Scripts read -# aztec-node code there, never in the labs/ submodule, which the foundation moves on its own. +# require_aztec_node [line] checks the checkout against its pin (scripts/aztec_node.mts check) and +# exports AZTEC_NODE_DIR: for main, docs/.aztec-node/tree unless already set; for a line, +# docs/lines/<line>/.aztec-node/tree always. Scripts read aztec-node code there, never in the labs/ +# submodule, which the foundation moves on its own. require_aztec_node() { - local docs + local docs line=${1:-} docs=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) - node "$docs/scripts/aztec_node.mts" check || return 1 - export AZTEC_NODE_DIR=${AZTEC_NODE_DIR:-$docs/.aztec-node/tree} + if [ -n "$line" ]; then + node "$docs/scripts/aztec_node.mts" --line "$line" check || return 1 + export AZTEC_NODE_DIR=$docs/lines/$line/.aztec-node/tree + else + node "$docs/scripts/aztec_node.mts" check || return 1 + export AZTEC_NODE_DIR=${AZTEC_NODE_DIR:-$docs/.aztec-node/tree} + fi } From 74ec9b672e368580fc944ed45d742460622e2ff2 Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Thu, 5 Nov 2026 16:24:28 +0000 Subject: [PATCH 11/32] fix(docs): find a line from any docs path and stage it without touching its state check_doc_references.sh also recognises an absolute docs/lines/<line>. The staged examples build no longer creates the line's .aztec-node/l1-artifacts before fetch provisions it. --- docs/examples/bootstrap.sh | 3 ++- docs/scripts/check_doc_references.sh | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/examples/bootstrap.sh b/docs/examples/bootstrap.sh index 5c568708bd4..9483ae9cff3 100755 --- a/docs/examples/bootstrap.sh +++ b/docs/examples/bootstrap.sh @@ -31,7 +31,8 @@ if [ "$cmd" = --line ]; then mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" STAGE=$(mktemp -d "${XDG_CACHE_HOME:-$HOME/.cache}/aztec-docs-$DOCS_LINE.XXXXXX") cp -R "$line_root/Nargo.toml" "$line_root/examples" "$REPO_ROOT/docs/.yarnrc.yml" "$STAGE/" - mkdir -p "$line_root/.aztec-node/l1-artifacts" "$STAGE/node_modules/@aztec-foundation" + # The links may dangle until pin_toolchain fetches the line. + mkdir -p "$STAGE/node_modules/@aztec-foundation" ln -s "$line_root/.aztec-node" "$STAGE/.aztec-node" ln -s "$line_root/.aztec-node/l1-artifacts" "$STAGE/node_modules/@aztec-foundation/l1-artifacts" export DOCS_ROOT=$STAGE diff --git a/docs/scripts/check_doc_references.sh b/docs/scripts/check_doc_references.sh index 47d6ffdffed..c51c9df6b9b 100755 --- a/docs/scripts/check_doc_references.sh +++ b/docs/scripts/check_doc_references.sh @@ -29,7 +29,7 @@ source "$SCRIPT_DIR/lib/aztec_node.sh" DOCS_DIR="${1:-docs}" LINE= -[[ "$DOCS_DIR" =~ ^docs/lines/(v[0-9]+)/?$ ]] && LINE=${BASH_REMATCH[1]} +[[ "$DOCS_DIR" =~ (^|/)docs/lines/(v[0-9]+)/?$ ]] && LINE=${BASH_REMATCH[2]} # labs/ references name aztec-node files at the pinned commit, so they are tested in its checkout; # the check refuses untracked files there, which would pass for deleted ones. From cfa814aa25dfc9567098f49bb08bbf958e458970 Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Thu, 5 Nov 2026 16:34:12 +0000 Subject: [PATCH 12/32] fix(docs): check each line's checkout, keep canonical links in the fingerprint include_code caches a passed checkout check per pin, not per directory: main's AZTEC_NODE_DIR may name a line's checkout. The production fingerprint keeps <link> tags, so a changed canonical or alternate URL shows. The staged examples build sets its cleanup before copying and refuses a malformed line name. A refused l1-artifacts upgrade keeps the installed package (now tested). --- docs/examples/bootstrap.sh | 28 +++++++++++-------- docs/scripts/__tests__/aztec_node.test.mts | 9 +++++- .../__tests__/production_fingerprint.test.mts | 16 +++++++---- docs/scripts/aztec_node.mts | 3 +- docs/scripts/production_fingerprint.mts | 11 +++----- .../preprocess/__tests__/include_code.test.js | 18 ++++++++++++ docs/src/preprocess/include_code.js | 7 +++-- 7 files changed, 63 insertions(+), 29 deletions(-) diff --git a/docs/examples/bootstrap.sh b/docs/examples/bootstrap.sh index 9483ae9cff3..b8bdb84e5aa 100755 --- a/docs/examples/bootstrap.sh +++ b/docs/examples/bootstrap.sh @@ -13,7 +13,20 @@ unset AZTEC_NODE_DIR # Nargo.toml and examples are staged outside the repository in a copy that mirrors docs/, with the # line's .aztec-node (checkout and toolchain) and l1-artifacts linked in where docs/ has its own. # DOCS_ROOT is where the examples build; DOCS_PATH is their source's path in the repository. -DOCS_LINE= STAGE= +DOCS_LINE= STAGE= DOCS_ROOT= + +# Safety net: ensure all TS example yarn.lock files are empty on exit. +# Both validate-ts and execute-examples (via Docker volume mount) can populate +# these files, and their per-project cleanup may not run if processes are killed. +# Set before staging, so a failed copy also removes the stage. +function on_exit { + if [ -n "$DOCS_ROOT" ]; then + for lf in "$DOCS_ROOT"/examples/ts/*/yarn.lock; do [ -f "$lf" ] && > "$lf"; done + fi + if [ -n "$STAGE" ]; then rm -rf "$STAGE"; fi +} +trap on_exit EXIT + aztec_node=(node "$REPO_ROOT/docs/scripts/aztec_node.mts") if [ "$cmd" = --line ]; then DOCS_LINE=${1:?usage: examples/bootstrap.sh --line <line> [cmd]} @@ -22,8 +35,8 @@ if [ "$cmd" = --line ]; then [ -n "$cmd" ] && shift export DOCS_PATH=docs/lines/$DOCS_LINE line_root=$REPO_ROOT/$DOCS_PATH - if [ ! -f "$line_root/aztec-node.json" ]; then - echo_stderr "No line $DOCS_LINE: $DOCS_PATH/aztec-node.json does not exist." + if [[ ! "$DOCS_LINE" =~ ^v[0-9]+$ ]] || [ ! -f "$line_root/aztec-node.json" ]; then + echo_stderr "No line $DOCS_LINE: a line is a docs/lines/v<major>/ directory holding an aztec-node.json." exit 1 fi aztec_node+=(--line "$DOCS_LINE") @@ -40,15 +53,6 @@ else export DOCS_PATH=docs DOCS_ROOT=$REPO_ROOT/docs fi -# Safety net: ensure all TS example yarn.lock files are empty on exit. -# Both validate-ts and execute-examples (via Docker volume mount) can populate -# these files, and their per-project cleanup may not run if processes are killed. -function on_exit { - for lf in "$DOCS_ROOT"/examples/ts/*/yarn.lock; do [ -f "$lf" ] && > "$lf"; done - if [ -n "$STAGE" ]; then rm -rf "$STAGE"; fi -} -trap on_exit EXIT - # The pin names the aztec-node commit and every toolchain version, so it keys the examples. hash=$(cache_content_hash .rebuild_patterns) diff --git a/docs/scripts/__tests__/aztec_node.test.mts b/docs/scripts/__tests__/aztec_node.test.mts index 4ea49ef716b..2b51f86d687 100644 --- a/docs/scripts/__tests__/aztec_node.test.mts +++ b/docs/scripts/__tests__/aztec_node.test.mts @@ -466,11 +466,18 @@ test("a line refuses another major's tag before any network access", async () => test("l1-artifacts installs for a line only, from a tarball matching its registry integrity", async () => { const lo = withLine(); + const refused = (e: unknown) => e instanceof PinError && /does not match its registry integrity/.test(String(e.message)); serveL1Artifacts("1.1.0", true); - await assert.rejects(provision(lo, f.pinB, ["l1-artifacts"]), (e) => e instanceof PinError && /does not match its registry integrity/.test(e.message)); + await assert.rejects(provision(lo, f.pinB, ["l1-artifacts"]), refused); assert.ok(!fs.existsSync(path.join(lo.docsDir, ".aztec-node/l1-artifacts"))); fetchTree(lo, f.pinB); assert.match(problems(lo, f.pinB).join(), /holds @aztec-foundation\/l1-artifacts nothing, not the pinned 1\.1\.0/); + + // A refused upgrade keeps the installed package. + serveL1Artifacts("1.0.0-rc.1"); + await provision(lo, f.pinA, ["l1-artifacts"]); + await assert.rejects(provision(lo, f.pinB, ["l1-artifacts"]), refused); + assert.equal(l1ArtifactsVersion(lo), "1.0.0-rc.1"); await assert.rejects(provision(f.o, f.pinA, ["l1-artifacts"]), (e) => e instanceof PinError && /line only/.test(e.message)); }); diff --git a/docs/scripts/__tests__/production_fingerprint.test.mts b/docs/scripts/__tests__/production_fingerprint.test.mts index 78b9572705e..9f3e01ba660 100644 --- a/docs/scripts/__tests__/production_fingerprint.test.mts +++ b/docs/scripts/__tests__/production_fingerprint.test.mts @@ -6,7 +6,7 @@ import { test } from "node:test"; import zlib from "node:zlib"; import { fingerprint } from "../production_fingerprint.mts"; -function build(bundle: string, href: string) { +function build(bundle: string, href: string, canonical = "/developers/page") { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fingerprint-")); const write = (rel: string, data: string | Uint8Array) => { fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); @@ -14,7 +14,7 @@ function build(bundle: string, href: string) { }; write( "developers/page.html", - `<link rel=stylesheet href=/assets/css/styles.${bundle}.css /><script src=/assets/js/main.${bundle}.js defer></script>` + + `<link rel=canonical href=https://example.com${canonical} /><link rel=stylesheet href=/assets/css/styles.${bundle}.css /><script src=/assets/js/main.${bundle}.js defer></script>` + `<script>window.chunk="runtime~main.${bundle}.js"</script><main><a href=${href}>Next</a></main>`, ); write("developers/page.md", "# Page\n"); @@ -24,12 +24,18 @@ function build(bundle: string, href: string) { return dir; } -test("the fingerprint ignores bundle names and changes with a rendered link", () => { - const dirs = [build("aaaaaaaa", "/developers/next"), build("bbbbbbbb", "/developers/next"), build("aaaaaaaa", "/operate/next")]; +test("the fingerprint ignores bundle names and changes with a rendered or canonical link", () => { + const dirs = [ + build("aaaaaaaa", "/developers/next"), + build("bbbbbbbb", "/developers/next"), + build("aaaaaaaa", "/operate/next"), + build("aaaaaaaa", "/developers/next", "/developers-v6/page"), + ]; try { - const [base, rebundled, relinked] = dirs.map(fingerprint); + const [base, rebundled, relinked, recanonical] = dirs.map(fingerprint); assert.deepEqual(rebundled, base); assert.notDeepEqual(relinked, base); + assert.notDeepEqual(recanonical, base); assert.ok(base.includes("sitemap /developers/page")); assert.ok(base.includes("search-tag docs-developer-v6.0.0 1")); } finally { diff --git a/docs/scripts/aztec_node.mts b/docs/scripts/aztec_node.mts index 93930d1b904..38423afe70b 100644 --- a/docs/scripts/aztec_node.mts +++ b/docs/scripts/aztec_node.mts @@ -438,7 +438,8 @@ async function provisionL1Artifacts(o: Options, pin: Pin, log: (s: string) => vo if (packageVersion(dir) === version) return; const bytes = await npmTarball(o, L1_ARTIFACTS, version, log); fs.mkdirSync(stateDir(o), { recursive: true }); - // Unpacked beside its destination, so a failure leaves the previous package and the swap is a rename. + // Unpacked beside its destination, so a failed download or extraction leaves the previous package + // and the swap is a rename; an interrupted swap leaves none, which check reports and fetch repairs. const tmp = fs.mkdtempSync(path.join(stateDir(o), "l1-artifacts-")); try { fs.writeFileSync(path.join(tmp, "package.tgz"), bytes); diff --git a/docs/scripts/production_fingerprint.mts b/docs/scripts/production_fingerprint.mts index 0a272d232b1..57993bd4078 100644 --- a/docs/scripts/production_fingerprint.mts +++ b/docs/scripts/production_fingerprint.mts @@ -1,7 +1,7 @@ // Prints what a production build serves as sorted lines, so two builds compare with diff: a hash of // each page's HTML and markdown twin, the llms files, the sitemap paths, and the search index with -// its pages per tag. Any theme code change renames the bundles, so the HTML is hashed without them; -// its text, links and structured data stay. +// its pages per tag. Any theme code change renames the bundles, so the HTML is hashed without its +// scripts and their content hashes; its text, links, canonical URLs and structured data stay. // // node scripts/production_fingerprint.mts <build-dir> import { createHash } from "node:crypto"; @@ -13,12 +13,9 @@ import { sitemapPaths } from "./check_search_index.mts"; const sha256 = (data: string | Uint8Array) => createHash("sha256").update(data).digest("hex"); -/** `html` without `<link>` tags, `<script src>` tags and the content hashes in bundle names. */ +/** `html` without `<script src>` tags and the content hashes in bundle names. */ export const normalizeHtml = (html: string) => - html - .replace(/<script\b[^>]*\bsrc=[^>]*>\s*<\/script>/g, "") - .replace(/<link\b[^>]*>/g, "") - .replace(/\.[0-9a-f]{8}\.(js|css)\b/g, ".$1"); + html.replace(/<script\b[^>]*\bsrc=[^>]*>\s*<\/script>/g, "").replace(/\.[0-9a-f]{8}\.(js|css)\b/g, ".$1"); function* files(root: string, rel = ""): Generator<string> { for (const entry of fs.readdirSync(path.join(root, rel), { withFileTypes: true })) { diff --git a/docs/src/preprocess/__tests__/include_code.test.js b/docs/src/preprocess/__tests__/include_code.test.js index 68ee36a2dee..eac35153b20 100644 --- a/docs/src/preprocess/__tests__/include_code.test.js +++ b/docs/src/preprocess/__tests__/include_code.test.js @@ -140,6 +140,24 @@ describe("include_code", () => { await assert.rejects(preprocessIncludeCode(include("docs/lines/v1/examples/a.ts"), "page.md", root.rootDir), /is line v1's, but this page is main's/); }); + it("checks a line's checkout even after main's check passed on the same directory", async () => { + const line = path.join(root.rootDir, "docs/lines/v1"); + const tree = path.join(line, ".aztec-node/tree"); + fs.mkdirSync(path.dirname(tree), { recursive: true }); + execFileSync("git", ["clone", "-q", root.tree, tree]); // main's commit, while the line pins another + const pin = JSON.parse(fs.readFileSync(path.join(root.rootDir, "docs/aztec-node.json"), "utf8")); + const toolchain = { ...pin.toolchain, aztec: "1.1.0" }; + write(path.join(line, "aztec-node.json"), JSON.stringify({ ...pin, ref: "v1.1.0", commit: "f".repeat(40), toolchain })); + write(path.join(line, ".aztec-node/l1-artifacts/package.json"), '{"version":"1.0.0"}'); + process.env.AZTEC_NODE_DIR = tree; + + await preprocessIncludeCode(page, "page.md", root.rootDir); + await assert.rejects( + preprocessIncludeCode(page, path.join(line, "docs-developers/page.md"), root.rootDir), + /not the pinned f{40}/, + ); + }); + it("links l1-artifacts snippets to aztec-packages at the installed version", () => { write(path.join(root.rootDir, "docs/node_modules/@aztec-foundation/l1-artifacts/package.json"), '{"version":"6.0.0-rc.6"}'); assert.deepStrictEqual( diff --git a/docs/src/preprocess/include_code.js b/docs/src/preprocess/include_code.js index 72a6371db65..0d03fb402cd 100644 --- a/docs/src/preprocess/include_code.js +++ b/docs/src/preprocess/include_code.js @@ -35,11 +35,12 @@ function aztecNodeDir(rootDir, line) { ); } -// Checked once per process before the first labs/ snippet is read: an edited, untracked or stale -// file would render code the linked commit does not have. +// Checked once per process and pin before the first labs/ snippet is read: an edited, untracked or +// stale file would render code the linked commit does not have. Main's override may name a line's +// checkout, so a directory checked against one pin is not checked against another. const checkedCheckouts = new Set(); function checkAztecNode(rootDir, line) { - const key = `${rootDir}\0${aztecNodeDir(rootDir, line)}`; + const key = `${rootDir}\0${line?.name ?? ""}\0${aztecNodeDir(rootDir, line)}`; if (checkedCheckouts.has(key)) return; const tool = path.join(rootDir, "docs", "scripts", "aztec_node.mts"); try { From a40d0a987ea908b0cb7f94b3d94f5bb3ee6d30fe Mon Sep 17 00:00:00 2001 From: alejoamiras <alejo.amiras@gmail.com> Date: Thu, 5 Nov 2026 16:41:11 +0000 Subject: [PATCH 13/32] docs: fork the v6 line (pure copy) Copies the dev docs' developer and operator pages, their sidebars, the aztec-node pin, the Noir workspace and the example sources into docs/lines/v6, byte for byte. The examples' build tooling (bootstrap scripts, compose file, rebuild patterns) stays main-only, since it builds a line from main's copy. The line's .aztec-node state is gitignored. --- docs/.gitignore | 1 + docs/lines/v6/Nargo.toml | 16 + docs/lines/v6/aztec-node.json | 17 + docs/lines/v6/docs-developers/ai_tooling.md | 135 + .../docs/aztec-js/_category_.json | 6 + .../docs/aztec-js/aztec_js_reference.md | 7045 ++++++++++++++ .../how_to_connect_to_local_network.md | 61 + .../docs/aztec-js/how_to_create_account.md | 75 + .../docs/aztec-js/how_to_deploy_contract.md | 225 + .../docs/aztec-js/how_to_pay_fees.md | 226 + .../docs/aztec-js/how_to_read_data.md | 145 + .../docs/aztec-js/how_to_send_transaction.md | 108 + .../how_to_simulate_without_signing.md | 113 + .../docs/aztec-js/how_to_test.md | 111 + .../docs/aztec-js/how_to_use_authwit.md | 83 + .../aztec-js/how_to_use_private_fee_juice.md | 114 + .../v6/docs-developers/docs/aztec-js/index.md | 53 + .../aztec-js/typescript_api_reference.mdx | 137 + .../docs/aztec-nr/_category_.json | 6 + .../v6/docs-developers/docs/aztec-nr/api.mdx | 81 + .../docs/aztec-nr/compiling_contracts.md | 76 + .../aztec-nr/contract_readiness_states.md | 85 + .../docs/aztec-nr/debugging.md | 196 + .../framework-description/_category_.json | 6 + .../advanced/_category_.json | 6 + .../advanced/how_to_profile_transactions.md | 218 + .../advanced/how_to_prove_history.md | 154 + .../advanced/how_to_retrieve_filter_notes.md | 166 + .../advanced/how_to_use_capsules.md | 76 + .../advanced/partial_notes.md | 137 + .../partial_notes_as_payment_endpoints.md | 159 + .../advanced/protocol_oracles.md | 36 + .../advanced/writing_efficient_contracts.md | 386 + .../authentication_witnesses.md | 79 + .../calling_contracts.md | 112 + .../contract_artifact.md | 117 + .../contract_structure.md | 175 + .../contract_upgrades.md | 148 + .../framework-description/custom_notes.md | 204 + .../framework-description/data_packing.md | 220 + .../framework-description/dependencies.md | 120 + .../ethereum_aztec_messaging.md | 99 + .../framework-description/events_and_logs.md | 122 + .../functions/attributes.md | 285 + .../functions/context.md | 133 + .../functions/function_transforms.md | 174 + .../functions/how_to_define_functions.md | 125 + .../framework-description/functions/index.md | 30 + .../functions/visibility.md | 56 + .../aztec-nr/framework-description/globals.md | 83 + .../framework-description/immutables.md | 33 + .../aztec-nr/framework-description/macros.md | 51 + .../framework-description/note_delivery.md | 267 + .../offchain_message_delivery.md | 119 + .../framework-description/state_variables.md | 618 ++ .../v6/docs-developers/docs/aztec-nr/index.md | 76 + .../docs/aztec-nr/installation.md | 31 + .../docs-developers/docs/aztec-nr/logging.md | 240 + .../docs/aztec-nr/standards/_category_.json | 6 + .../docs/aztec-nr/standards/aip-20.md | 112 + .../docs/aztec-nr/standards/aip-4626.md | 66 + .../docs/aztec-nr/standards/aip-721.md | 72 + .../docs/aztec-nr/standards/dripper.md | 9 + .../docs/aztec-nr/standards/escrow.md | 79 + .../docs/aztec-nr/standards/generic-proxy.md | 42 + .../docs/aztec-nr/standards/index.md | 33 + .../docs/aztec-nr/testing_contracts.md | 383 + .../docs-developers/docs/cli/_category_.json | 6 + .../docs/cli/aztec_cli_reference.md | 1951 ++++ .../docs/cli/aztec_up_cli_reference.md | 151 + .../docs/cli/aztec_wallet_cli_reference.md | 415 + .../docs/foundational-topics/_category_.json | 6 + .../accounts/_category_.json | 6 + .../foundational-topics/accounts/index.md | 275 + .../docs/foundational-topics/accounts/keys.md | 230 + .../advanced/_category_.json | 6 + .../foundational-topics/advanced/authwit.md | 200 + .../advanced/circuits/_category_.json | 6 + .../advanced/circuits/avm_compatibility.md | 69 + .../advanced/circuits/index.md | 67 + .../advanced/circuits/private_kernel.md | 128 + .../advanced/circuits/public_execution.md | 87 + .../advanced/circuits/rollup_circuits.md | 103 + .../advanced/storage/_category_.json | 6 + .../advanced/storage/indexed_merkle_tree.mdx | 396 + .../advanced/storage/note_discovery.md | 133 + .../advanced/storage/storage_slots.md | 70 + .../docs/foundational-topics/call_types.md | 234 + .../foundational-topics/contract_creation.md | 135 + .../ethereum-aztec-messaging/_category_.json | 6 + .../data_structures.md | 38 + .../ethereum-aztec-messaging/inbox.md | 44 + .../ethereum-aztec-messaging/index.md | 150 + .../ethereum-aztec-messaging/outbox.md | 89 + .../ethereum-aztec-messaging/registry.md | 54 + .../docs/foundational-topics/fees.md | 145 + .../docs/foundational-topics/index.md | 84 + .../pxe/execution_hooks.md | 137 + .../docs/foundational-topics/pxe/index.md | 122 + .../pxe/kernelless_simulations.md | 99 + .../foundational-topics/state_management.md | 133 + .../docs/foundational-topics/transactions.md | 148 + .../docs/foundational-topics/wallets.md | 79 + .../docs/resources/_category_.json | 6 + .../docs/resources/community_calls.md | 40 + .../resources/considerations/_category_.json | 6 + .../resources/considerations/limitations.md | 162 + .../considerations/privacy_considerations.md | 133 + .../docs/resources/glossary.md | 195 + .../docs/resources/migration_notes.md | 8297 +++++++++++++++++ .../docs/resources/video_lessons.mdx | 54 + .../docs/tutorials/_category_.json | 6 + .../contract_tutorials/_category_.json | 6 + .../contract_tutorials/counter_contract.md | 167 + .../recursive_verification.md | 706 ++ .../contract_tutorials/token_contract.md | 475 + .../tutorials/js_tutorials/_category_.json | 6 + .../tutorials/js_tutorials/aave_bridge.md | 532 ++ .../js_tutorials/aztecjs-getting-started.md | 135 + .../tutorials/js_tutorials/token_bridge.md | 560 ++ .../tutorials/js_tutorials/uniswap_swap.md | 411 + .../wallet-extension/01-architecture.md | 184 + .../wallet-extension/02-wallet-protocol.md | 212 + .../wallet-extension/03-pxe-integration.md | 162 + .../wallet-extension/04-accounts.md | 220 + .../wallet-extension/05-transactions.md | 377 + .../wallet-extension/06-approval-ui.md | 241 + .../wallet-extension/07-testing.md | 207 + .../js_tutorials/wallet-extension/index.md | 156 + .../js_tutorials/webapp/01-the-contract.md | 192 + .../js_tutorials/webapp/02-project-setup.md | 87 + .../webapp/03-network-and-wallet.md | 183 + .../webapp/04-contract-interaction.md | 95 + .../webapp/05-transactions-and-fees.md | 91 + .../webapp/06-putting-it-together.md | 118 + .../js_tutorials/webapp/_category_.json | 4 + .../tutorials/js_tutorials/webapp/index.md | 95 + .../webapp/wallet-sdk/01-dapp-integration.md | 226 + .../wallet-sdk/02-wallet-integration.md | 205 + .../webapp/wallet-sdk/_category_.json | 4 + .../js_tutorials/webapp/wallet-sdk/index.md | 81 + .../testing_governance_rollup_upgrade.md | 423 + .../getting_started_on_local_network.md | 284 + .../getting_started_on_testnet.md | 220 + docs/lines/v6/docs-developers/overview.md | 97 + docs/lines/v6/docs-developers/support.md | 130 + .../docs-operate/operators/_node_hardware.mdx | 6 + .../operators/concepts/claiming-rewards.md | 103 + .../operators/concepts/hardware.md | 89 + .../operators/concepts/identity-model.md | 79 + .../docs-operate/operators/concepts/l1-rpc.md | 112 + .../operators/concepts/monitoring.md | 137 + .../concepts/sequencer-troubleshooting.md | 78 + .../operators/concepts/slashing.md | 121 + .../operators/full-node/overview.md | 27 + .../docs-operate/operators/full-node/setup.md | 112 + .../operators/full-node/troubleshooting.md | 41 + .../operators/full-node/verification.md | 49 + .../lines/v6/docs-operate/operators/index.mdx | 37 + .../operators/keystore/advanced-patterns.md | 284 + .../operators/keystore/creating-keystores.md | 520 ++ .../operators/keystore/examples.md | 322 + .../docs-operate/operators/keystore/index.md | 90 + .../operators/keystore/storage-methods.md | 227 + .../operators/keystore/troubleshooting.md | 464 + .../operators/monitoring/grafana-setup.md | 171 + .../operators/monitoring/index.md | 62 + .../operators/monitoring/metrics-reference.md | 546 ++ .../operators/monitoring/otel-setup.md | 127 + .../operators/monitoring/prometheus-setup.md | 107 + .../operators/monitoring/troubleshooting.md | 153 + .../v6/docs-operate/operators/operator-faq.md | 465 + .../docs-operate/operators/prerequisites.md | 144 + .../operators/prover/claiming-rewards.mdx | 72 + .../docs-operate/operators/prover/overview.md | 58 + .../v6/docs-operate/operators/prover/setup.md | 243 + .../operators/prover/troubleshooting.md | 65 + .../operators/prover/verification.md | 113 + .../provider/configure-environment.mdx | 138 + .../operators/provider/day-2-essentials.mdx | 106 + .../operators/provider/docker-compose.mdx | 163 + .../operators/provider/fund-accounts.mdx | 123 + .../operators/provider/generate-keystore.mdx | 194 + .../docs-operate/operators/provider/index.mdx | 47 + .../operators/provider/install-toolchain.mdx | 79 + .../operators/provider/pre-flight.mdx | 141 + .../provider/provider-operations.mdx | 214 + .../operators/provider/register-sequencer.mdx | 146 + .../operators/provider/start-node.mdx | 122 + .../operators/provider/verify.mdx | 142 + .../operators/reference/changelog/index.md | 125 + .../operators/reference/changelog/v2.0.2.md | 366 + .../operators/reference/changelog/v4.2.md | 32 + .../operators/reference/changelog/v4.3.md | 57 + .../operators/reference/changelog/v4.md | 334 + .../operators/reference/changelog/v5.2.md | 81 + .../operators/reference/changelog/v6.md | 413 + .../operators/reference/cli-reference.md | 1153 +++ .../reference/ethereum-rpc-reference.md | 311 + .../operators/reference/glossary.md | 307 + .../operators/reference/node-api-reference.md | 1495 +++ .../operators/reference/reading-logs.md | 108 + .../sequencer-management/claiming-rewards.md | 217 + .../exiting-a-sequencer.md | 263 + .../governance-participation.md | 421 + .../operators/sequencer-management/index.md | 120 + .../slashing-configuration.md | 378 + .../sequencer-management/useful-commands.md | 352 + .../operators/setup/blob_storage.md | 213 + .../operators/setup/blob_upload.md | 201 + .../operators/setup/bootnode-operation.md | 238 + .../operators/setup/building-from-source.md | 325 + .../operators/setup/high-availability.md | 771 ++ .../operators/setup/registering-sequencer.md | 198 + .../operators/setup/running-a-node.md | 210 + .../operators/setup/running-a-prover.md | 385 + .../operators/setup/sequencer-setup.md | 628 ++ .../operators/setup/staking-provider.md | 609 ++ .../operators/setup/syncing-best-practices.md | 250 + .../solo-sequencer/configure-environment.mdx | 80 + .../operators/solo-sequencer/dappnode.mdx | 50 + .../solo-sequencer/day-2-essentials.mdx | 86 + .../solo-sequencer/docker-compose.mdx | 163 + .../solo-sequencer/fund-accounts.mdx | 113 + .../solo-sequencer/generate-keystore.mdx | 193 + .../operators/solo-sequencer/index.mdx | 50 + .../solo-sequencer/install-toolchain.mdx | 79 + .../operators/solo-sequencer/pre-flight.mdx | 147 + .../solo-sequencer/register-sequencer.mdx | 126 + .../operators/solo-sequencer/start-node.mdx | 122 + .../operators/solo-sequencer/verify.mdx | 128 + .../docs-operate/operators/tooling/index.md | 88 + .../v6/docs-operate/reference/changelog/v4.md | 333 + docs/lines/v6/examples/circuits/Nargo.toml | 4 + .../circuits/hello_circuit/Nargo.toml | 8 + .../circuits/hello_circuit/src/main.nr | 10 + .../examples/contracts/aave_bridge/Nargo.toml | 8 + .../contracts/aave_bridge/src/config.nr | 13 + .../contracts/aave_bridge/src/main.nr | 134 + .../contracts/bob_token_contract/Nargo.toml | 7 + .../contracts/bob_token_contract/src/main.nr | 171 + .../contracts/counter_contract/Nargo.toml | 9 + .../contracts/counter_contract/src/main.nr | 50 + .../counter_contract_test/Nargo.toml | 9 + .../counter_contract_test/src/lib.nr | 33 + .../contracts/example_uniswap/Nargo.toml | 9 + .../contracts/example_uniswap/src/main.nr | 221 + .../contracts/example_uniswap/src/util.nr | 110 + .../contracts/logging_example/Nargo.toml | 8 + .../contracts/logging_example/src/main.nr | 87 + .../contracts/logging_example_test/Nargo.toml | 9 + .../contracts/logging_example_test/src/lib.nr | 52 + .../v6/examples/contracts/nft/Nargo.toml | 6 + .../v6/examples/contracts/nft/src/main.nr | 95 + .../v6/examples/contracts/nft/src/nft.nr | 9 + .../examples/contracts/nft_bridge/Nargo.toml | 7 + .../examples/contracts/nft_bridge/src/main.nr | 66 + .../contracts/packing_example/Nargo.toml | 8 + .../contracts/packing_example/src/main.nr | 39 + .../contracts/packing_example/src/types.nr | 225 + .../Nargo.toml | 9 + .../src/main.nr | 64 + .../solidity/aave_bridge/AavePortal.sol | 127 + .../solidity/aave_bridge/MockAToken.sol | 18 + .../solidity/aave_bridge/MockAavePool.sol | 57 + .../solidity/aave_bridge/MockERC20.sol | 14 + .../solidity/example_swap/ExampleERC20.sol | 15 + .../example_swap/ExampleTokenPortal.sol | 96 + .../example_swap/ExampleUniswapPortal.sol | 156 + docs/lines/v6/examples/solidity/foundry.toml | 16 + .../solidity/nft_bridge/NFTPortal.sol | 73 + .../solidity/nft_bridge/SimpleNFT.sol | 18 + docs/lines/v6/examples/ts/.gitignore | 12 + .../v6/examples/ts/aave_bridge/config.yaml | 12 + .../lines/v6/examples/ts/aave_bridge/index.ts | 445 + .../v6/examples/ts/aave_bridge/yarn.lock | 0 .../examples/ts/aztecjs_advanced/config.yaml | 16 + .../v6/examples/ts/aztecjs_advanced/index.ts | 499 + .../v6/examples/ts/aztecjs_advanced/yarn.lock | 0 .../examples/ts/aztecjs_authwit/config.yaml | 14 + .../v6/examples/ts/aztecjs_authwit/index.ts | 147 + .../v6/examples/ts/aztecjs_authwit/yarn.lock | 0 .../ts/aztecjs_connection/config.yaml | 16 + .../examples/ts/aztecjs_connection/index.ts | 197 + .../examples/ts/aztecjs_connection/yarn.lock | 0 .../ts/aztecjs_getting_started/config.yaml | 14 + .../ts/aztecjs_getting_started/index.ts | 70 + .../ts/aztecjs_getting_started/yarn.lock | 0 .../aztecjs_kernelless_simulation/config.yaml | 16 + .../ts/aztecjs_kernelless_simulation/index.ts | 167 + .../aztecjs_kernelless_simulation/yarn.lock | 0 .../v6/examples/ts/aztecjs_runner/config.yaml | 15 + .../v6/examples/ts/aztecjs_runner/index.ts | 23 + .../v6/examples/ts/aztecjs_runner/run.sh | 282 + .../v6/examples/ts/aztecjs_runner/yarn.lock | 0 .../examples/ts/aztecjs_testing/config.yaml | 14 + .../v6/examples/ts/aztecjs_testing/index.ts | 158 + .../v6/examples/ts/aztecjs_testing/yarn.lock | 0 .../ts/bob_token_contract/config.yaml | 17 + .../examples/ts/bob_token_contract/index.ts | 124 + .../examples/ts/bob_token_contract/yarn.lock | 0 .../v6/examples/ts/example_swap/config.yaml | 14 + .../v6/examples/ts/example_swap/index.ts | 658 ++ .../v6/examples/ts/example_swap/yarn.lock | 0 docs/lines/v6/examples/ts/lib.sh | 45 + .../ts/recursive_verification/config.yaml | 22 + .../ts/recursive_verification/index.ts | 125 + .../scripts/generate_data.ts | 74 + .../scripts/sponsored_fpc.ts | 16 + .../ts/recursive_verification/yarn.lock | 0 .../v6/examples/ts/token_bridge/config.yaml | 19 + .../v6/examples/ts/token_bridge/index.ts | 363 + .../v6/examples/ts/token_bridge/yarn.lock | 0 .../v6/examples/ts/tsconfig.template.json | 13 + .../v6/examples/webapp-tutorial/.env.example | 1 + .../v6/examples/webapp-tutorial/.gitignore | 13 + .../v6/examples/webapp-tutorial/README.md | 157 + .../webapp-tutorial/contracts/Nargo.toml | 8 + .../contracts/src/game_round_note.nr | 24 + .../webapp-tutorial/contracts/src/main.nr | 182 + .../webapp-tutorial/contracts/src/race.nr | 166 + .../webapp-tutorial/esbuild.extension.mjs | 100 + .../v6/examples/webapp-tutorial/index.html | 12 + .../v6/examples/webapp-tutorial/package.json | 53 + .../scripts/deploy-and-interact.ts | 72 + .../v6/examples/webapp-tutorial/setup.sh | 38 + .../v6/examples/webapp-tutorial/src/App.css | 533 ++ .../v6/examples/webapp-tutorial/src/App.tsx | 131 + .../src/components/AccountInfo.tsx | 23 + .../src/components/ErrorBoundary.tsx | 47 + .../src/components/GameBoard.tsx | 151 + .../src/components/GameLobby.tsx | 154 + .../src/components/GameStatus.tsx | 36 + .../src/components/NetworkPicker.tsx | 29 + .../src/components/TransactionLog.tsx | 110 + .../src/components/TwoPlayerLocal.tsx | 463 + .../src/components/TxStatus.tsx | 34 + .../src/components/WalletConnect.tsx | 198 + .../v6/examples/webapp-tutorial/src/config.ts | 33 + .../examples/webapp-tutorial/src/contract.ts | 120 + .../webapp-tutorial/src/embedded-wallet.ts | 130 + .../v6/examples/webapp-tutorial/src/fees.ts | 48 + .../webapp-tutorial/src/game-constants.ts | 5 + .../v6/examples/webapp-tutorial/src/main.tsx | 12 + .../webapp-tutorial/src/wallet-connection.ts | 112 + .../webapp-tutorial/test-extension/README.md | 192 + .../test-extension/manifest.json | 35 + .../test-extension/popup/popup.css | 723 ++ .../test-extension/popup/popup.html | 13 + .../test-extension/src/account-utils.ts | 61 + .../test-extension/src/aztec-imports.ts | 142 + .../test-extension/src/background.ts | 1442 +++ .../test-extension/src/config.ts | 96 + .../test-extension/src/content-script.ts | 21 + .../src/offscreen/console-intercept.ts | 24 + .../src/offscreen/offscreen-dist.html | 15 + .../src/offscreen/offscreen.html | 15 + .../test-extension/src/offscreen/offscreen.ts | 777 ++ .../src/popup/AccountSwitcher.tsx | 51 + .../test-extension/src/popup/ApprovalView.tsx | 509 + .../src/popup/CreateAccountView.tsx | 49 + .../test-extension/src/popup/Header.tsx | 104 + .../test-extension/src/popup/LockScreen.tsx | 60 + .../test-extension/src/popup/MainScreen.tsx | 81 + .../test-extension/src/popup/SettingsPage.tsx | 154 + .../test-extension/src/popup/SetupScreen.tsx | 130 + .../test-extension/src/popup/helpers.ts | 100 + .../test-extension/src/popup/popup.tsx | 467 + .../test-extension/src/popup/types.ts | 18 + .../test-extension/src/shared-types.ts | 97 + .../test-extension/src/utils.ts | 44 + .../test-extension/src/wallet/storage.ts | 333 + .../test-extension/src/wallet/wallet-impl.ts | 157 + .../tests/e2e/embedded-wallet.test.ts | 147 + .../tests/e2e/extension-wallet.test.ts | 197 + .../webapp-tutorial/tests/unit/crypto.test.ts | 154 + .../tests/unit/wallet-manager.test.ts | 111 + .../webapp-tutorial/tsconfig.app.json | 17 + .../v6/examples/webapp-tutorial/tsconfig.json | 4 + .../examples/webapp-tutorial/vite.config.ts | 67 + .../webapp-tutorial/vite.extension.config.ts | 121 + .../examples/webapp-tutorial/vitest.config.ts | 13 + docs/lines/v6/sidebars-developer.js | 55 + docs/lines/v6/sidebars-operate.js | 280 + 384 files changed, 72283 insertions(+) create mode 100644 docs/lines/v6/Nargo.toml create mode 100644 docs/lines/v6/aztec-node.json create mode 100644 docs/lines/v6/docs-developers/ai_tooling.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/aztec_js_reference.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/how_to_connect_to_local_network.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/how_to_create_account.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/how_to_deploy_contract.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/how_to_pay_fees.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/how_to_read_data.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/how_to_send_transaction.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/how_to_test.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_authwit.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_private_fee_juice.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/index.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-js/typescript_api_reference.mdx create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/api.mdx create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/compiling_contracts.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/contract_readiness_states.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/debugging.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_profile_transactions.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_retrieve_filter_notes.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes_as_payment_endpoints.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/writing_efficient_contracts.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/authentication_witnesses.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/calling_contracts.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_artifact.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_structure.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_upgrades.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/custom_notes.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/data_packing.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/dependencies.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/ethereum_aztec_messaging.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/events_and_logs.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/attributes.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/context.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/function_transforms.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/index.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/visibility.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/globals.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/immutables.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/macros.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/note_delivery.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/offchain_message_delivery.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/state_variables.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/index.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/installation.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/logging.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/standards/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-20.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-4626.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-721.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/standards/dripper.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/standards/escrow.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/standards/generic-proxy.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/standards/index.md create mode 100644 docs/lines/v6/docs-developers/docs/aztec-nr/testing_contracts.md create mode 100644 docs/lines/v6/docs-developers/docs/cli/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/cli/aztec_cli_reference.md create mode 100644 docs/lines/v6/docs-developers/docs/cli/aztec_up_cli_reference.md create mode 100644 docs/lines/v6/docs-developers/docs/cli/aztec_wallet_cli_reference.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/accounts/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/accounts/index.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/accounts/keys.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/authwit.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/avm_compatibility.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/index.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/private_kernel.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/public_execution.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/rollup_circuits.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/indexed_merkle_tree.mdx create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/note_discovery.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/storage_slots.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/call_types.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/contract_creation.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/data_structures.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/inbox.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/index.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/outbox.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/registry.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/fees.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/index.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/pxe/execution_hooks.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/pxe/index.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/pxe/kernelless_simulations.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/state_management.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/transactions.md create mode 100644 docs/lines/v6/docs-developers/docs/foundational-topics/wallets.md create mode 100644 docs/lines/v6/docs-developers/docs/resources/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/resources/community_calls.md create mode 100644 docs/lines/v6/docs-developers/docs/resources/considerations/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/resources/considerations/limitations.md create mode 100644 docs/lines/v6/docs-developers/docs/resources/considerations/privacy_considerations.md create mode 100644 docs/lines/v6/docs-developers/docs/resources/glossary.md create mode 100644 docs/lines/v6/docs-developers/docs/resources/migration_notes.md create mode 100644 docs/lines/v6/docs-developers/docs/resources/video_lessons.mdx create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/token_contract.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aztecjs-getting-started.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/uniswap_swap.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/01-architecture.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/02-wallet-protocol.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/03-pxe-integration.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/05-transactions.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/06-approval-ui.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/07-testing.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/index.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/01-the-contract.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/02-project-setup.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/03-network-and-wallet.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/04-contract-interaction.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/05-transactions-and-fees.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/06-putting-it-together.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/index.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/01-dapp-integration.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/02-wallet-integration.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/_category_.json create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/index.md create mode 100644 docs/lines/v6/docs-developers/docs/tutorials/testing_governance_rollup_upgrade.md create mode 100644 docs/lines/v6/docs-developers/getting_started_on_local_network.md create mode 100644 docs/lines/v6/docs-developers/getting_started_on_testnet.md create mode 100644 docs/lines/v6/docs-developers/overview.md create mode 100644 docs/lines/v6/docs-developers/support.md create mode 100644 docs/lines/v6/docs-operate/operators/_node_hardware.mdx create mode 100644 docs/lines/v6/docs-operate/operators/concepts/claiming-rewards.md create mode 100644 docs/lines/v6/docs-operate/operators/concepts/hardware.md create mode 100644 docs/lines/v6/docs-operate/operators/concepts/identity-model.md create mode 100644 docs/lines/v6/docs-operate/operators/concepts/l1-rpc.md create mode 100644 docs/lines/v6/docs-operate/operators/concepts/monitoring.md create mode 100644 docs/lines/v6/docs-operate/operators/concepts/sequencer-troubleshooting.md create mode 100644 docs/lines/v6/docs-operate/operators/concepts/slashing.md create mode 100644 docs/lines/v6/docs-operate/operators/full-node/overview.md create mode 100644 docs/lines/v6/docs-operate/operators/full-node/setup.md create mode 100644 docs/lines/v6/docs-operate/operators/full-node/troubleshooting.md create mode 100644 docs/lines/v6/docs-operate/operators/full-node/verification.md create mode 100644 docs/lines/v6/docs-operate/operators/index.mdx create mode 100644 docs/lines/v6/docs-operate/operators/keystore/advanced-patterns.md create mode 100644 docs/lines/v6/docs-operate/operators/keystore/creating-keystores.md create mode 100644 docs/lines/v6/docs-operate/operators/keystore/examples.md create mode 100644 docs/lines/v6/docs-operate/operators/keystore/index.md create mode 100644 docs/lines/v6/docs-operate/operators/keystore/storage-methods.md create mode 100644 docs/lines/v6/docs-operate/operators/keystore/troubleshooting.md create mode 100644 docs/lines/v6/docs-operate/operators/monitoring/grafana-setup.md create mode 100644 docs/lines/v6/docs-operate/operators/monitoring/index.md create mode 100644 docs/lines/v6/docs-operate/operators/monitoring/metrics-reference.md create mode 100644 docs/lines/v6/docs-operate/operators/monitoring/otel-setup.md create mode 100644 docs/lines/v6/docs-operate/operators/monitoring/prometheus-setup.md create mode 100644 docs/lines/v6/docs-operate/operators/monitoring/troubleshooting.md create mode 100644 docs/lines/v6/docs-operate/operators/operator-faq.md create mode 100644 docs/lines/v6/docs-operate/operators/prerequisites.md create mode 100644 docs/lines/v6/docs-operate/operators/prover/claiming-rewards.mdx create mode 100644 docs/lines/v6/docs-operate/operators/prover/overview.md create mode 100644 docs/lines/v6/docs-operate/operators/prover/setup.md create mode 100644 docs/lines/v6/docs-operate/operators/prover/troubleshooting.md create mode 100644 docs/lines/v6/docs-operate/operators/prover/verification.md create mode 100644 docs/lines/v6/docs-operate/operators/provider/configure-environment.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/day-2-essentials.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/docker-compose.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/fund-accounts.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/generate-keystore.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/index.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/install-toolchain.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/pre-flight.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/provider-operations.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/register-sequencer.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/start-node.mdx create mode 100644 docs/lines/v6/docs-operate/operators/provider/verify.mdx create mode 100644 docs/lines/v6/docs-operate/operators/reference/changelog/index.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/changelog/v2.0.2.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/changelog/v4.2.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/changelog/v4.3.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/changelog/v4.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/changelog/v5.2.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/changelog/v6.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/cli-reference.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/ethereum-rpc-reference.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/glossary.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/node-api-reference.md create mode 100644 docs/lines/v6/docs-operate/operators/reference/reading-logs.md create mode 100644 docs/lines/v6/docs-operate/operators/sequencer-management/claiming-rewards.md create mode 100644 docs/lines/v6/docs-operate/operators/sequencer-management/exiting-a-sequencer.md create mode 100644 docs/lines/v6/docs-operate/operators/sequencer-management/governance-participation.md create mode 100644 docs/lines/v6/docs-operate/operators/sequencer-management/index.md create mode 100644 docs/lines/v6/docs-operate/operators/sequencer-management/slashing-configuration.md create mode 100644 docs/lines/v6/docs-operate/operators/sequencer-management/useful-commands.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/blob_storage.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/blob_upload.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/bootnode-operation.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/building-from-source.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/high-availability.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/registering-sequencer.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/running-a-node.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/running-a-prover.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/sequencer-setup.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/staking-provider.md create mode 100644 docs/lines/v6/docs-operate/operators/setup/syncing-best-practices.md create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/configure-environment.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/dappnode.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/day-2-essentials.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/docker-compose.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/fund-accounts.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/generate-keystore.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/index.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/install-toolchain.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/pre-flight.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/register-sequencer.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/start-node.mdx create mode 100644 docs/lines/v6/docs-operate/operators/solo-sequencer/verify.mdx create mode 100644 docs/lines/v6/docs-operate/operators/tooling/index.md create mode 100644 docs/lines/v6/docs-operate/reference/changelog/v4.md create mode 100644 docs/lines/v6/examples/circuits/Nargo.toml create mode 100644 docs/lines/v6/examples/circuits/hello_circuit/Nargo.toml create mode 100644 docs/lines/v6/examples/circuits/hello_circuit/src/main.nr create mode 100644 docs/lines/v6/examples/contracts/aave_bridge/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/aave_bridge/src/config.nr create mode 100644 docs/lines/v6/examples/contracts/aave_bridge/src/main.nr create mode 100644 docs/lines/v6/examples/contracts/bob_token_contract/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr create mode 100644 docs/lines/v6/examples/contracts/counter_contract/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/counter_contract/src/main.nr create mode 100644 docs/lines/v6/examples/contracts/counter_contract_test/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/counter_contract_test/src/lib.nr create mode 100644 docs/lines/v6/examples/contracts/example_uniswap/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/example_uniswap/src/main.nr create mode 100644 docs/lines/v6/examples/contracts/example_uniswap/src/util.nr create mode 100644 docs/lines/v6/examples/contracts/logging_example/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/logging_example/src/main.nr create mode 100644 docs/lines/v6/examples/contracts/logging_example_test/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/logging_example_test/src/lib.nr create mode 100644 docs/lines/v6/examples/contracts/nft/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/nft/src/main.nr create mode 100644 docs/lines/v6/examples/contracts/nft/src/nft.nr create mode 100644 docs/lines/v6/examples/contracts/nft_bridge/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/nft_bridge/src/main.nr create mode 100644 docs/lines/v6/examples/contracts/packing_example/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/packing_example/src/main.nr create mode 100644 docs/lines/v6/examples/contracts/packing_example/src/types.nr create mode 100644 docs/lines/v6/examples/contracts/recursive_verification_contract/Nargo.toml create mode 100644 docs/lines/v6/examples/contracts/recursive_verification_contract/src/main.nr create mode 100644 docs/lines/v6/examples/solidity/aave_bridge/AavePortal.sol create mode 100644 docs/lines/v6/examples/solidity/aave_bridge/MockAToken.sol create mode 100644 docs/lines/v6/examples/solidity/aave_bridge/MockAavePool.sol create mode 100644 docs/lines/v6/examples/solidity/aave_bridge/MockERC20.sol create mode 100644 docs/lines/v6/examples/solidity/example_swap/ExampleERC20.sol create mode 100644 docs/lines/v6/examples/solidity/example_swap/ExampleTokenPortal.sol create mode 100644 docs/lines/v6/examples/solidity/example_swap/ExampleUniswapPortal.sol create mode 100644 docs/lines/v6/examples/solidity/foundry.toml create mode 100644 docs/lines/v6/examples/solidity/nft_bridge/NFTPortal.sol create mode 100644 docs/lines/v6/examples/solidity/nft_bridge/SimpleNFT.sol create mode 100644 docs/lines/v6/examples/ts/.gitignore create mode 100644 docs/lines/v6/examples/ts/aave_bridge/config.yaml create mode 100644 docs/lines/v6/examples/ts/aave_bridge/index.ts create mode 100644 docs/lines/v6/examples/ts/aave_bridge/yarn.lock create mode 100644 docs/lines/v6/examples/ts/aztecjs_advanced/config.yaml create mode 100644 docs/lines/v6/examples/ts/aztecjs_advanced/index.ts create mode 100644 docs/lines/v6/examples/ts/aztecjs_advanced/yarn.lock create mode 100644 docs/lines/v6/examples/ts/aztecjs_authwit/config.yaml create mode 100644 docs/lines/v6/examples/ts/aztecjs_authwit/index.ts create mode 100644 docs/lines/v6/examples/ts/aztecjs_authwit/yarn.lock create mode 100644 docs/lines/v6/examples/ts/aztecjs_connection/config.yaml create mode 100644 docs/lines/v6/examples/ts/aztecjs_connection/index.ts create mode 100644 docs/lines/v6/examples/ts/aztecjs_connection/yarn.lock create mode 100644 docs/lines/v6/examples/ts/aztecjs_getting_started/config.yaml create mode 100644 docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts create mode 100644 docs/lines/v6/examples/ts/aztecjs_getting_started/yarn.lock create mode 100644 docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/config.yaml create mode 100644 docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/index.ts create mode 100644 docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/yarn.lock create mode 100644 docs/lines/v6/examples/ts/aztecjs_runner/config.yaml create mode 100644 docs/lines/v6/examples/ts/aztecjs_runner/index.ts create mode 100755 docs/lines/v6/examples/ts/aztecjs_runner/run.sh create mode 100644 docs/lines/v6/examples/ts/aztecjs_runner/yarn.lock create mode 100644 docs/lines/v6/examples/ts/aztecjs_testing/config.yaml create mode 100644 docs/lines/v6/examples/ts/aztecjs_testing/index.ts create mode 100644 docs/lines/v6/examples/ts/aztecjs_testing/yarn.lock create mode 100644 docs/lines/v6/examples/ts/bob_token_contract/config.yaml create mode 100644 docs/lines/v6/examples/ts/bob_token_contract/index.ts create mode 100644 docs/lines/v6/examples/ts/bob_token_contract/yarn.lock create mode 100644 docs/lines/v6/examples/ts/example_swap/config.yaml create mode 100644 docs/lines/v6/examples/ts/example_swap/index.ts create mode 100644 docs/lines/v6/examples/ts/example_swap/yarn.lock create mode 100644 docs/lines/v6/examples/ts/lib.sh create mode 100644 docs/lines/v6/examples/ts/recursive_verification/config.yaml create mode 100644 docs/lines/v6/examples/ts/recursive_verification/index.ts create mode 100644 docs/lines/v6/examples/ts/recursive_verification/scripts/generate_data.ts create mode 100644 docs/lines/v6/examples/ts/recursive_verification/scripts/sponsored_fpc.ts create mode 100644 docs/lines/v6/examples/ts/recursive_verification/yarn.lock create mode 100644 docs/lines/v6/examples/ts/token_bridge/config.yaml create mode 100644 docs/lines/v6/examples/ts/token_bridge/index.ts create mode 100644 docs/lines/v6/examples/ts/token_bridge/yarn.lock create mode 100644 docs/lines/v6/examples/ts/tsconfig.template.json create mode 100644 docs/lines/v6/examples/webapp-tutorial/.env.example create mode 100644 docs/lines/v6/examples/webapp-tutorial/.gitignore create mode 100644 docs/lines/v6/examples/webapp-tutorial/README.md create mode 100644 docs/lines/v6/examples/webapp-tutorial/contracts/Nargo.toml create mode 100644 docs/lines/v6/examples/webapp-tutorial/contracts/src/game_round_note.nr create mode 100644 docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr create mode 100644 docs/lines/v6/examples/webapp-tutorial/contracts/src/race.nr create mode 100644 docs/lines/v6/examples/webapp-tutorial/esbuild.extension.mjs create mode 100644 docs/lines/v6/examples/webapp-tutorial/index.html create mode 100644 docs/lines/v6/examples/webapp-tutorial/package.json create mode 100644 docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts create mode 100755 docs/lines/v6/examples/webapp-tutorial/setup.sh create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/App.css create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/App.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/components/AccountInfo.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/components/ErrorBoundary.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/components/GameBoard.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/components/GameLobby.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/components/GameStatus.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/components/NetworkPicker.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/components/TransactionLog.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/components/TwoPlayerLocal.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/components/TxStatus.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/components/WalletConnect.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/config.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/contract.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/embedded-wallet.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/fees.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/game-constants.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/main.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/README.md create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/manifest.json create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/popup/popup.css create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/popup/popup.html create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/account-utils.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/aztec-imports.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/config.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/content-script.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/console-intercept.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen-dist.html create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.html create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/Header.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/MainScreen.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/helpers.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/popup.tsx create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/types.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/shared-types.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/utils.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/storage.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/tests/e2e/embedded-wallet.test.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/tests/e2e/extension-wallet.test.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/tests/unit/crypto.test.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/tests/unit/wallet-manager.test.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/tsconfig.app.json create mode 100644 docs/lines/v6/examples/webapp-tutorial/tsconfig.json create mode 100644 docs/lines/v6/examples/webapp-tutorial/vite.config.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/vite.extension.config.ts create mode 100644 docs/lines/v6/examples/webapp-tutorial/vitest.config.ts create mode 100644 docs/lines/v6/sidebars-developer.js create mode 100644 docs/lines/v6/sidebars-operate.js diff --git a/docs/.gitignore b/docs/.gitignore index ebe7e8ef2fa..40e22c22a09 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -38,3 +38,4 @@ static/aztec-nr-api/next # aztec-node checkout and binaries pinned by aztec-node.json (scripts/aztec_node.mts) /.aztec-node +/lines/*/.aztec-node diff --git a/docs/lines/v6/Nargo.toml b/docs/lines/v6/Nargo.toml new file mode 100644 index 00000000000..5864c380f38 --- /dev/null +++ b/docs/lines/v6/Nargo.toml @@ -0,0 +1,16 @@ +[workspace] +members = [ + "examples/circuits/hello_circuit", + "examples/contracts/counter_contract", + "examples/contracts/counter_contract_test", + "examples/contracts/bob_token_contract", + "examples/contracts/nft", + "examples/contracts/nft_bridge", + "examples/contracts/recursive_verification_contract", + "examples/contracts/example_uniswap", + "examples/webapp-tutorial/contracts", + "examples/contracts/aave_bridge", + "examples/contracts/logging_example", + "examples/contracts/logging_example_test", + "examples/contracts/packing_example" +] diff --git a/docs/lines/v6/aztec-node.json b/docs/lines/v6/aztec-node.json new file mode 100644 index 00000000000..1e0e0dd998e --- /dev/null +++ b/docs/lines/v6/aztec-node.json @@ -0,0 +1,17 @@ +{ + "repository": "https://github.com/aztec-labs-eng/aztec-node", + "ref": "v6.1.0-nightly.20261029", + "commit": "a6ca030427db42b5187ce983a92621c38b4380c0", + "toolchain": { + "aztec": "6.1.0-nightly.20261029", + "bb": "6.0.0-rc.7", + "noir": "1.0.0-rc.3", + "l1Artifacts": "6.0.0-rc.7" + }, + "noirArchives": { + "x86_64-unknown-linux-gnu": "sha256:5792dd962afce1eed834570f95d9f72cc4a09d45cd846d3b80b7a57392732973", + "x86_64-apple-darwin": "sha256:9014ea4b7e88ab2441a949454f2b3dbc2920a82a9353a7a3649a444afed78910", + "aarch64-unknown-linux-gnu": "sha256:98be47cd6b2a81f3857c029acd8298fd590eebeb153ef51e1dc6f10c973e00e9", + "aarch64-apple-darwin": "sha256:44157443519c8ede9a2df7e6a4ee513b3d49ef9fdd5774c43311fe6f32887a59" + } +} diff --git a/docs/lines/v6/docs-developers/ai_tooling.md b/docs/lines/v6/docs-developers/ai_tooling.md new file mode 100644 index 00000000000..c2ed218a7ef --- /dev/null +++ b/docs/lines/v6/docs-developers/ai_tooling.md @@ -0,0 +1,135 @@ +--- +title: AI Tooling +description: Set up AI coding tools like Claude Code, Cursor, and Codex for Aztec and Noir development. +sidebar_position: 2 +tags: [ai, tooling, getting_started] +references: ["context7.json"] +--- + +Aztec is new, rapidly evolving, and spans novel concepts like private state, notes, and nullifiers. AI coding tools can accelerate your learning and development, but they need up-to-date context to be useful. This page shows you how to set that up. + +:::caution +LLMs have limited training data for zero-knowledge circuit development. Noir and Aztec.nr are newer languages with smaller codebases than mainstream languages, so AI tools will make more mistakes than you might be used to. The tools on this page help by providing up-to-date context, but you should always verify generated code and test thoroughly. +::: + +## Project-level instructions (CLAUDE.md / AGENTS.md files) + +MCP servers and skills provide context on demand, but AI tools don't always invoke them at the right time. The most reliable way to prevent common mistakes is to add **project-level instruction files** that your AI tool reads automatically at the start of every conversation. You can add to these files over time as you discover new gotchas or best practices. They ensure your AI tool always has the critical context it needs, without relying on you to remember to invoke the right skills or MCP servers. + +For Claude Code, create a `CLAUDE.md` file in your project root. For Codex, create an `AGENTS.md` file in your project root. For other tools, check their documentation for equivalent configuration. + +### Recommended CLAUDE.md / AGENTS.md + +```markdown +# Aztec Project + +## Critical: Use the `aztec` CLI, not `nargo` or `bb` directly + +This is an Aztec smart contract project. Always use the `aztec` CLI wrapper instead of calling `nargo` or `bb` (the Barretenberg prover) directly: + +- **Compile**: `aztec compile` (NOT `nargo compile`). Using `nargo compile` alone produces incomplete artifacts. +- **Test**: `aztec test` (NOT `nargo test`). +- **Prove**: NEVER call `bb` directly. Proof generation is handled for you by the PXE through the `aztec` CLI and `aztec.js`. There is no contract-development workflow that runs `bb` by hand. +- **Other nargo commands** like `aztec-nargo fmt` and `aztec-nargo doc` are fine to use directly. The Aztec installer exposes the bundled `nargo` as `aztec-nargo`; bare `nargo` resolves to your own install (if any), not the bundled one. + +## Error Handling + +- NEVER silently swallow errors or fall back to default values. If a value is required, throw if it's missing. +- NEVER use fallback values like `AztecAddress.ZERO`, `"unknown"`, `0`, or `null` to mask missing data. These hide bugs and cause failures elsewhere that are harder to trace. +- NEVER add retry/polling logic unless explicitly asked. Retry loops with long timeouts may brick application loops and mask the real error. +- NEVER wrap calls in try/catch that returns null or a default. Let errors propagate. +- If a precondition isn't met, throw immediately with a descriptive message — don't try to "work around" it. +- Prefer `T` return types over `T | null` when null would indicate a bug rather than a valid state. +- Do not add `.catch(() => defaultValue)` to promises. If something fails, the caller needs to know. + +## Version Compatibility + +The Aztec developer SDK/aztec-nr version (used for writing and compiling contracts) may differ from the node version (used by operators to run the network). Check the [Networks page](https://docs.aztec.network/networks) for current network versions. When in doubt, use the version from the developer docs you are reading, it is the correct SDK version for contract development on that network. + +## Hashing: Default to Poseidon2 + +When writing Aztec.nr contract code that requires hashing, **always use Poseidon2** unless a specific protocol or interoperability requirement calls for a different hash. + +- **Default**: `use aztec::protocol::hash::poseidon2_hash;` +- **Do NOT** default to Pedersen (`pedersen_hash`). Pedersen is available but Poseidon2 is cheaper in circuits and is the standard across Aztec. +- If you are unsure which hash to use, use Poseidon2. +``` + +This prevents the two most common AI mistakes: using `nargo compile`/`nargo test` instead of their Aztec wrappers, and defaulting to Pedersen hashes instead of Poseidon2. + +### Why this matters + +LLMs have extensive training data for `nargo` (the standalone Noir compiler) and `bb` (the Barretenberg prover CLI) but limited exposure to the `aztec` CLI wrapper. Without explicit instructions, they default to `nargo compile` (which produces artifacts missing the AVM transpilation step) or reach for `bb` to generate proofs. In an Aztec project, compilation and proving both go through the `aztec` tooling. + +## MCP servers + +The highest-leverage tools are the Aztec and Noir MCP servers. They clone reference repositories locally and give your AI tool code search, documentation search, and example discovery across the Aztec and Noir ecosystems. They work with any AI coding tool that supports MCP (Claude Code, Cursor, Windsurf, Codex, and others). + +The MCP servers help manage the problem of focusing LLMs on the correct Aztec versions for your project. Aztec is under active development and there may be multiple versions in use at any given time (e.g. mainnet, devnet and testnet may be on different versions). They make it easy to switch between versions if needed, and to keep your context up to date as the repos evolve. + +Start here if you're unsure what to set up. + +### Claude Code + +Add the MCP servers directly: + +```bash +claude mcp add aztec -- npx @aztec/mcp-server@latest +claude mcp add noir -- npx noir-mcp-server@latest +``` + +### Cursor / Windsurf / other MCP clients + +Add the servers to your MCP configuration JSON: + +```json +{ + "mcpServers": { + "aztec": { + "command": "npx", + "args": ["-y", "@aztec/mcp-server@latest"] + }, + "noir": { + "command": "npx", + "args": ["-y", "noir-mcp-server@latest"] + } + } +} +``` + +### OpenAI Codex + +Use the same MCP configuration format, pointing at `@aztec/mcp-server` and `noir-mcp-server`. + +## For learning and exploration + +These resources help you understand Aztec concepts, read docs, or provide additional context to your AI tool. + +- **API reference docs** - The docs site publishes auto-generated API references that are useful to feed to AI tools: + - [Aztec.nr API reference](./docs/aztec-nr/api.mdx) - generated from aztec-nr source with `nargo doc` + - [TypeScript API reference](./docs/aztec-js/typescript_api_reference.mdx) - generated from yarn-project packages with TypeDoc + + These are especially useful as context for code generation since they reflect the current API surface. Agents can read both as markdown: the [Aztec.nr index](https://docs.aztec.network/aztec-nr-api/mainnet/llms.txt) links a page per module and item, and the [TypeScript index](https://docs.aztec.network/typescript-api/mainnet/llms.txt) a page per exported name. + +- **llms.txt** - The docs site publishes [llms.txt](https://docs.aztec.network/llms.txt), a navigational index of every page following the [llmstxt.org](https://llmstxt.org) standard, and [llms-full.txt](https://docs.aztec.network/llms-full.txt), the complete documentation in a single file. Many AI tools can consume these files directly to index documentation. +- **Markdown for any page** - Append `.md` to any page URL (for example `https://docs.aztec.network/developers/overview.md`), or request the normal URL with an `Accept: text/markdown` header, to get the page as clean markdown instead of HTML. This is the best format to hand to an agent. +- **Reference repositories** - Point your AI tool at these repos for additional context: + - [AztecProtocol/aztec-packages](https://github.com/AztecProtocol/aztec-packages) - main monorepo, best general reference + - [AztecProtocol/aztec-starter](https://github.com/AztecProtocol/aztec-starter) - smaller starter project, easier for onboarding + - [AztecProtocol/aztec-examples](https://github.com/AztecProtocol/aztec-examples) - official contract examples + - [noir-lang/noir](https://github.com/noir-lang/noir) - Noir language source of truth + - [noir-lang/noir-examples](https://github.com/noir-lang/noir-examples) - common Noir patterns + - [awesome-noir](https://github.com/noir-lang/awesome-noir) - community Noir resources + - [awesome-aztec](https://github.com/AztecProtocol/awesome-aztec) - community Aztec resources +- **Copy docs into context** - The "Copy page" button at the top of each page copies the page as markdown. Its menu also opens the page in ChatGPT or Claude so you can ask questions about it, and links to the markdown version of the page. +- **Context7** - [Context7](https://context7.com) is a generic MCP server. The Aztec index at [context7.com/aztecprotocol/aztec-packages](https://context7.com/aztecprotocol/aztec-packages) is scoped by [`context7.json`](https://github.com/AztecProtocol/aztec-packages/blob/next/context7.json) to the developer and operator docs of the version the networks run, and excludes the repository's build and CI infrastructure. Note that it may be less current than the MCP servers above. + +## Aztec and Noir tool reference + +| Tool | Works with | Description | +| --------------------------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| [@aztec/mcp-server](https://github.com/AztecProtocol/mcp-server) | Any MCP client (Claude Code, Cursor, Windsurf, Codex) | Clones Aztec repos locally, provides code search, doc search, and example discovery | +| [noir-claude-plugin](https://github.com/critesjosh/noir-claude-plugin) | Claude Code | Skills and commands for Noir circuit development | +| [noir-mcp-server](https://github.com/critesjosh/noir-mcp-server) | Any MCP client | Clones Noir repos, stdlib, and community libraries; provides search and examples | +| [aztec-skills](https://github.com/NethermindEth/aztec-skills) | Claude Code, Codex | Installable skills for Aztec contracts, deployment, Aztec.js, and testing | +| [noir skills](https://github.com/noir-lang/noir/tree/master/.claude/skills) | Claude Code, Codex | Skills for Noir compiler development, SSA debugging, fuzzing, and ACIR optimization | diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/_category_.json b/docs/lines/v6/docs-developers/docs/aztec-js/_category_.json new file mode 100644 index 00000000000..5e4bf38d69a --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Aztec.js", + "position": 3, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/aztec_js_reference.md b/docs/lines/v6/docs-developers/docs/aztec-js/aztec_js_reference.md new file mode 100644 index 00000000000..347d08410b2 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/aztec_js_reference.md @@ -0,0 +1,7045 @@ +--- +title: Reference +description: Comprehensive auto-generated reference for the Aztec.js TypeScript library with all classes, interfaces, types, and functions. +tags: [api, reference, autogenerated, aztec.js, typescript] +sidebar_position: 98 +--- + +# Reference + +*This documentation is auto-generated from the Aztec.js TypeScript source code.* + +:::info +This is an auto-generated reference. For tutorials and guides, see the [Aztec.js Guide](./index.md). +::: + +:::tip Looking up one name? +Every class, function and type also has its own short page. Start from the [TypeScript API reference](./typescript_api_reference.mdx), or, for AI agents, its [index of names](pathname:///typescript-api/#api_ref_version/llms.txt). +::: + + +*Package: @aztec-labs/aztec.js* + +*Generated: 2026-10-22T19:05:54.138Z* + +This document provides a comprehensive reference for all public APIs in the Aztec.js library. + +Each section is organized by module, with classes, interfaces, types, and functions documented with their full signatures, parameters, and return types. + +## Table of Contents + +- [Account](#account) + - [AuthorizationProvider](#authorizationprovider) + - [Account](#account-1) + - [BaseAccount](#baseaccount) + - [AccountContract](#accountcontract) + - [getAccountContractAddress](#getaccountcontractaddress) +- [Authorization](#authorization) + - [CallAuthorizationRequest](#callauthorizationrequest) +- [Contract](#contract) + - [BaseContractInteraction](#basecontractinteraction) + - [BatchCall](#batchcall) + - [abiChecker](#abichecker) + - [Contract](#contract-1) + - [ContractMethod](#contractmethod) + - [ContractStorageLayout](#contractstoragelayout) + - [ContractBase](#contractbase) + - [ContractFunctionInteraction](#contractfunctioninteraction) + - [DeployInstantiationOptions](#deployinstantiationoptions) + - [BoundInstantiationOptions](#boundinstantiationoptions) + - [UniversalInstantiationOptions](#universalinstantiationoptions) + - [PendingInstantiationOptions](#pendinginstantiationoptions) + - [DeployMethodContract](#deploymethodcontract) + - [DeployMethodPayload](#deploymethodpayload) + - [RequestDeployOptions](#requestdeployoptions) + - [DeployOptionsWithoutWait](#deployoptionswithoutwait) + - [DeployOptions](#deployoptions) + - [SimulateDeployOptions](#simulatedeployoptions) + - [DeployResultMined](#deployresultmined) + - [DeployReturn](#deployreturn) + - [DeployMethod](#deploymethod) + - [BoundDeployMethod](#bounddeploymethod) + - [UniversalDeployMethod](#universaldeploymethod) + - [PendingDeployMethod](#pendingdeploymethod) + - [fastForwardContractUpdate](#fastforwardcontractupdate) + - [FeePaymentMethodOption](#feepaymentmethodoption) + - [GasSettingsOption](#gassettingsoption) + - [InteractionFeeOptions](#interactionfeeoptions) + - [RequestInteractionOptions](#requestinteractionoptions) + - [NO_WAIT](#no_wait) + - [NoWait](#nowait) + - [NO_FROM](#no_from) + - [NoFrom](#nofrom) + - [InteractionWaitOptions](#interactionwaitoptions) + - [SendInteractionOptionsWithoutWait](#sendinteractionoptionswithoutwait) + - [SendInteractionOptions](#sendinteractionoptions) + - [SimulateInteractionOptions](#simulateinteractionoptions) + - [ProfileInteractionOptions](#profileinteractionoptions) + - [OffchainMessage](#offchainmessage) + - [OffchainOutput](#offchainoutput) + - [extractOffchainOutput](#extractoffchainoutput) + - [SimulationResult](#simulationresult) + - [TxSendResultImmediate](#txsendresultimmediate) + - [TxSendResultMined](#txsendresultmined) + - [SendReturn](#sendreturn) + - [toSendOptions](#tosendoptions) + - [toSimulateOptions](#tosimulateoptions) + - [toProfileOptions](#toprofileoptions) + - [WaitForProvenOpts](#waitforprovenopts) + - [DefaultWaitForProvenOpts](#defaultwaitforprovenopts) + - [waitForProven](#waitforproven) + - [WaitOpts](#waitopts) + - [DefaultWaitOpts](#defaultwaitopts) +- [Deployment](#deployment) + - [ContractDeployer](#contractdeployer) + - [publishContractClass](#publishcontractclass) + - [publishInstance](#publishinstance) +- [Ethereum](#ethereum) + - [L2Claim](#l2claim) + - [L2AmountClaim](#l2amountclaim) + - [L2AmountClaimWithRecipient](#l2amountclaimwithrecipient) + - [generateClaimSecret](#generateclaimsecret) + - [L1TokenManager](#l1tokenmanager) + - [L1FeeJuicePortalManager](#l1feejuiceportalmanager) + - [L1ToL2TokenPortalManager](#l1tol2tokenportalmanager) + - [L1TokenPortalManager](#l1tokenportalmanager) +- [Fee](#fee-4) + - [FeeJuicePaymentMethodWithClaim](#feejuicepaymentmethodwithclaim) + - [FeePaymentMethod](#feepaymentmethod) + - [PrivateFeePaymentMethod](#privatefeepaymentmethod) + - [PublicFeePaymentMethod](#publicfeepaymentmethod) + - [SponsoredFeePaymentMethod](#sponsoredfeepaymentmethod) +- [Utils](#utils) + - [FieldLike](#fieldlike) + - [EthAddressLike](#ethaddresslike) + - [AztecAddressLike](#aztecaddresslike) + - [FunctionSelectorLike](#functionselectorlike) + - [EventSelectorLike](#eventselectorlike) + - [U128Like](#u128like) + - [WrappedFieldLike](#wrappedfieldlike) + - [OptionLike](#optionlike) + - [IntentInnerHash](#intentinnerhash) + - [CallIntent](#callintent) + - [ContractFunctionInteractionCallIntent](#contractfunctioninteractioncallintent) + - [isContractFunctionInteractionCallIntent](#iscontractfunctioninteractioncallintent) + - [computeAuthWitMessageHash](#computeauthwitmessagehash) + - [getMessageHashFromIntent](#getmessagehashfromintent) + - [computeInnerAuthWitHashFromAction](#computeinnerauthwithashfromaction) + - [lookupValidity](#lookupvalidity) + - [SetPublicAuthwitContractInteraction](#setpublicauthwitcontractinteraction) + - [waitForL1ToL2MessageReady](#waitforl1tol2messageready) + - [isL1ToL2MessageReady](#isl1tol2messageready) + - [getFeeJuiceBalance](#getfeejuicebalance) + - [readFieldCompressedString](#readfieldcompressedstring) + - [waitForNode](#waitfornode) + - [waitForTx](#waitfortx) + - [createAztecNodeClient](#createaztecnodeclient) + - [AztecNode](#aztecnode) + - [AztecNodeClientOptions](#aztecnodeclientoptions) + - [generatePublicKey](#generatepublickey) +- [Wallet](#wallet) + - [AccountEntrypointMetaPaymentMethod](#accountentrypointmetapaymentmethod) + - [AccountManagerCreateOptions](#accountmanagercreateoptions) + - [AccountManager](#accountmanager) + - [CAPABILITY_VERSION](#capability_version) + - [ContractFunctionPattern](#contractfunctionpattern) + - [AccountsCapability](#accountscapability) + - [GrantedAccountsCapability](#grantedaccountscapability) + - [ContractsCapability](#contractscapability) + - [GrantedContractsCapability](#grantedcontractscapability) + - [ContractClassesCapability](#contractclassescapability) + - [GrantedContractClassesCapability](#grantedcontractclassescapability) + - [SimulationCapability](#simulationcapability) + - [GrantedSimulationCapability](#grantedsimulationcapability) + - [TransactionCapability](#transactioncapability) + - [GrantedTransactionCapability](#grantedtransactioncapability) + - [DataCapability](#datacapability) + - [GrantedDataCapability](#granteddatacapability) + - [Capability](#capability) + - [GrantedCapability](#grantedcapability) + - [AppCapabilities](#appcapabilities) + - [WalletCapabilities](#walletcapabilities) + - [DeployAccountFeePaymentMethodOption](#deployaccountfeepaymentmethodoption) + - [RequestDeployAccountOptions](#requestdeployaccountoptions) + - [DeployAccountOptions](#deployaccountoptions) + - [SimulateDeployAccountOptions](#simulatedeployaccountoptions) + - [DeployAccountMethod](#deployaccountmethod) + - [TxSimulationResultWithAppOffset](#txsimulationresultwithappoffset) + - [Aliased](#aliased) + - [SimulateOptions](#simulateoptions) + - [ProfileOptions](#profileoptions) + - [SendOptions](#sendoptions) + - [BatchableMethods](#batchablemethods) + - [BatchedMethod](#batchedmethod) + - [BatchedMethodResult](#batchedmethodresult) + - [BatchedMethodResultWrapper](#batchedmethodresultwrapper) + - [BatchResults](#batchresults) + - [EventFilterBase](#eventfilterbase) + - [PrivateEventFilter](#privateeventfilter) + - [PublicEventFilter](#publiceventfilter) + - [Event](#event) + - [PrivateEvent](#privateevent) + - [PublicEvent](#publicevent) + - [ContractMetadata](#contractmetadata) + - [ContractClassMetadata](#contractclassmetadata) + - [ExecuteUtilityOptions](#executeutilityoptions) + - [Wallet](#wallet-2) + - [ExecutionPayloadSchema](#executionpayloadschema) + - [GasSettingsOptionSchema](#gassettingsoptionschema) + - [WaitOptsSchema](#waitoptsschema) + - [SendOptionsSchema](#sendoptionsschema) + - [SimulateOptionsSchema](#simulateoptionsschema) + - [ProfileOptionsSchema](#profileoptionsschema) + - [MessageHashOrIntentSchema](#messagehashorintentschema) + - [EventMetadataDefinitionSchema](#eventmetadatadefinitionschema) + - [PrivateEventFilterSchema](#privateeventfilterschema) + - [PublicEventFilterSchema](#publiceventfilterschema) + - [PrivateEventSchema](#privateeventschema) + - [PublicEventSchema](#publiceventschema) + - [ContractMetadataSchema](#contractmetadataschema) + - [ContractClassMetadataSchema](#contractclassmetadataschema) + - [ContractFunctionPatternSchema](#contractfunctionpatternschema) + - [AccountsCapabilitySchema](#accountscapabilityschema) + - [GrantedAccountsCapabilitySchema](#grantedaccountscapabilityschema) + - [ContractsCapabilitySchema](#contractscapabilityschema) + - [GrantedContractsCapabilitySchema](#grantedcontractscapabilityschema) + - [ContractClassesCapabilitySchema](#contractclassescapabilityschema) + - [GrantedContractClassesCapabilitySchema](#grantedcontractclassescapabilityschema) + - [SimulationCapabilitySchema](#simulationcapabilityschema) + - [GrantedSimulationCapabilitySchema](#grantedsimulationcapabilityschema) + - [TransactionCapabilitySchema](#transactioncapabilityschema) + - [GrantedTransactionCapabilitySchema](#grantedtransactioncapabilityschema) + - [DataCapabilitySchema](#datacapabilityschema) + - [GrantedDataCapabilitySchema](#granteddatacapabilityschema) + - [CapabilitySchema](#capabilityschema) + - [GrantedCapabilitySchema](#grantedcapabilityschema) + - [AppCapabilitiesSchema](#appcapabilitiesschema) + - [WalletCapabilitiesSchema](#walletcapabilitiesschema) + - [BatchedMethodSchema](#batchedmethodschema) + - [BatchedResultSchema](#batchedresultschema) + - [WalletSchema](#walletschema) + +--- + + +## Account + + +--- + +### `account/account.ts` + + +#### AuthorizationProvider + +**Type:** Interface + +Provides authorization for actions via the AuthWitness mechanism. + + +#### Methods + + +##### createAuthWit + +Creates an authentication witness from an inner hash with consumer, or a call intent + +**Signature:** + +```typescript +createAuthWit( + intent: IntentInnerHash | CallIntent, + chainInfo: ChainInfo +): Promise<AuthWitness> +``` + +**Parameters:** + +- `intent`: `IntentInnerHash | CallIntent` + - The action (or inner hash) to authorize +- `chainInfo`: `ChainInfo` + - Chain information needed for message hash computation + +**Returns:** + +`Promise<AuthWitness>` + +#### Account + +**Type:** Type Alias + +Minimal interface for transaction execution and authorization. + +**Signature:** + +```typescript +export type Account = EntrypointInterface & + AuthorizationProvider & { + getCompleteAddress(): CompleteAddress; + getAddress(): AztecAddress; + }; +``` + +**Type Members:** + +##### getCompleteAddress + +Returns the complete address for this account. + +**Signature:** + +```typescript +getCompleteAddress(): CompleteAddress +``` + +**Returns:** + +`CompleteAddress` +##### getAddress + +Returns the address for this account. + +**Signature:** + +```typescript +getAddress(): AztecAddress +``` + +**Returns:** + +`AztecAddress` + +#### BaseAccount + +**Type:** Class + +An account implementation that uses authwits as an authentication mechanism and can assemble transaction execution requests for an entrypoint. + +**Implements:** `Account` + + +#### Constructor + +**Signature:** + +```typescript +constructor( + private entrypoint: EntrypointInterface, + private authWitnessProvider: AuthWitnessProvider, + private completeAddress: CompleteAddress +) +``` + +**Parameters:** + +- `entrypoint`: `EntrypointInterface` +- `authWitnessProvider`: `AuthWitnessProvider` +- `completeAddress`: `CompleteAddress` + +#### Methods + + +##### createTxExecutionRequest + +**Signature:** + +```typescript +createTxExecutionRequest( + exec: ExecutionPayload, + gasSettings: GasSettings, + chainInfo: ChainInfo, + options: DefaultAccountEntrypointOptions +): Promise<TxExecutionRequest> +``` + +**Parameters:** + +- `exec`: `ExecutionPayload` +- `gasSettings`: `GasSettings` +- `chainInfo`: `ChainInfo` +- `options`: `DefaultAccountEntrypointOptions` + +**Returns:** + +`Promise<TxExecutionRequest>` + +##### wrapExecutionPayload + +**Signature:** + +```typescript +wrapExecutionPayload( + exec: ExecutionPayload, + chainInfo: ChainInfo, + options?: any +): Promise<ExecutionPayload> +``` + +**Parameters:** + +- `exec`: `ExecutionPayload` +- `chainInfo`: `ChainInfo` +- `options` (optional): `any` + +**Returns:** + +`Promise<ExecutionPayload>` + +##### createAuthWit + +**Signature:** + +```typescript +async createAuthWit( + messageHashOrIntent: CallIntent | IntentInnerHash, + chainInfo: ChainInfo +): Promise<AuthWitness> +``` + +**Parameters:** + +- `messageHashOrIntent`: `CallIntent | IntentInnerHash` +- `chainInfo`: `ChainInfo` + +**Returns:** + +`Promise<AuthWitness>` + +##### getCompleteAddress + +**Signature:** + +```typescript +getCompleteAddress(): CompleteAddress +``` + +**Returns:** + +`CompleteAddress` + +##### getAddress + +**Signature:** + +```typescript +getAddress(): AztecAddress +``` + +**Returns:** + +`AztecAddress` + +--- + +### `account/account_contract.ts` + + +#### AccountContract + +**Type:** Interface + +An account contract instance. Knows its artifact, deployment arguments, how to create transaction execution requests out of function calls, and how to authorize actions. + + +#### Methods + + +##### getContractArtifact + +Returns the artifact of this account contract. + +**Signature:** + +```typescript +getContractArtifact(): Promise<ContractArtifact> +``` + +**Returns:** + +`Promise<ContractArtifact>` + +##### getInitializationFunctionAndArgs + +Returns the initializer function name and arguments for this instance, or undefined if this contract does not require initialization. + +**Signature:** + +```typescript +getInitializationFunctionAndArgs(): Promise<{ + constructorName: string; + constructorArgs: any[]; + } | undefined> +``` + +**Returns:** + +```typescript +Promise< + | { + /** The name of the function used to initialize the contract */ + constructorName: string; + /** The args to the function used to initialize the contract */ + constructorArgs: any[]; + } + | undefined + > +``` + +##### getImmutablesHash + +The hash of this account's immutable instantiation params, committed into its address. Returns undefined for accounts that have no immutables (these are instead deployed via an onchain initializer, which contributes to the address through its initialization hash). + +**Signature:** + +```typescript +getImmutablesHash(): Promise<Fr | undefined> +``` + +**Returns:** + +`Promise<Fr | undefined>` + +##### getAccount + +Returns the account implementation for this account contract given an instance at the provided address. The account is responsible for assembling tx requests given requested function calls, and for creating signed auth witnesses given action identifiers (message hashes). + +**Signature:** + +```typescript +getAccount(address: CompleteAddress): Account +``` + +**Parameters:** + +- `address`: `CompleteAddress` + - Address of this account contract. + +**Returns:** + +`Account` - An account instance for creating tx requests and authorizing actions. + +##### getAuthWitnessProvider + +Returns the auth witness provider for the given address. + +**Signature:** + +```typescript +getAuthWitnessProvider(address: CompleteAddress): AuthWitnessProvider +``` + +**Parameters:** + +- `address`: `CompleteAddress` + - Address for which to create auth witnesses. + +**Returns:** + +`AuthWitnessProvider` + +#### getAccountContractAddress + +**Type:** Function + +Compute the address of an account contract from secret, salt and optional immutables hash + +**Signature:** + +```typescript +export async getAccountContractAddress( + accountContract: AccountContract, + secret: Fr, + salt: Fr, + immutablesHash?: Fr +): Promise<AztecAddress> +``` + +**Parameters:** + +- `accountContract`: `AccountContract` +- `secret`: `Fr` +- `salt`: `Fr` +- `immutablesHash` (optional): `Fr` + +**Returns:** + +`Promise<AztecAddress>` + +## Authorization + + +--- + +### `authorization/call_authorization_request.ts` + + +#### CallAuthorizationRequest + +**Type:** Class + +An authwit request for a function call. Includes the preimage of the data to be signed, as opposed of just the inner hash. + + +#### Constructor + +**Signature:** + +```typescript +private constructor( + public selector: AuthorizationSelector, + public innerHash: Fr, + public onBehalfOf: AztecAddress, + public msgSender: AztecAddress, + public functionSelector: FunctionSelector, + public argsHash: Fr, + public args: Fr[] +) +``` + +**Parameters:** + +- `selector`: `AuthorizationSelector` + - The selector of the authwit type, used to identify it when emitted from `emit_offchain_effect`oracle. Computed as poseidon2("CallAuthwit((Field),(u32),Field)".to_bytes()) +- `innerHash`: `Fr` + - The inner hash of the authwit, computed as poseidon2([msg_sender, selector, args_hash]) +- `onBehalfOf`: `AztecAddress` + - The address on whose behalf the auth witness should be created. This is the account that must sign the authorization. +- `msgSender`: `AztecAddress` + - The address performing the call +- `functionSelector`: `FunctionSelector` + - The selector of the function that is to be authorized +- `argsHash`: `Fr` + - The hash of the arguments to the function call, +- `args`: `Fr[]` + - The arguments to the function call. + +#### Methods + + +##### getSelector + +**Signature:** + +```typescript +static getSelector(): Promise<AuthorizationSelector> +``` + +**Returns:** + +`Promise<AuthorizationSelector>` + +##### fromFields + +**Signature:** + +```typescript +static async fromFields(fields: Fr[]): Promise<CallAuthorizationRequest> +``` + +**Parameters:** + +- `fields`: `Fr[]` + +**Returns:** + +`Promise<CallAuthorizationRequest>` + +## Contract + + +--- + +### `contract/base_contract_interaction.ts` + + +#### BaseContractInteraction + +**Type:** Class + +Base class for an interaction with a contract, be it a deployment, a function call, or a batch. Implements the sequence create/simulate/send. + + +#### Constructor + +**Signature:** + +```typescript +constructor( + protected wallet: Wallet, + protected authWitnesses: AuthWitness[] = [], + protected capsules: Capsule[] = [] +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `authWitnesses` (optional): `AuthWitness[]` +- `capsules` (optional): `Capsule[]` + +#### Properties + + +##### log + +**Type:** `any` + + +#### Methods + + +##### request + +Returns an execution request that represents this operation. Can be used as a building block for constructing batch requests. + +**Signature:** + +```typescript +public abstract request(options?: RequestInteractionOptions): Promise<ExecutionPayload> +``` + +**Parameters:** + +- `options` (optional): `RequestInteractionOptions` + - An optional object containing additional configuration for the transaction. + +**Returns:** + +`Promise<ExecutionPayload>` - An execution request wrapped in promise. + +##### send + +Sends a transaction to the contract function with the specified options. By default, waits for the transaction to be mined and returns the receipt (or custom type). + +**Signature:** + +```typescript +public send<TReturn = TxReceipt>(options: SendInteractionOptionsWithoutWait): Promise<TxSendResultMined<TReturn>> +``` + +**Parameters:** + +- `options`: `SendInteractionOptionsWithoutWait` + - An object containing 'from' property representing the AztecAddress of the sender, optional fee configuration, and optional wait settings + +**Returns:** + +`Promise<TxSendResultMined<TReturn>>` - TReturn (if wait is undefined/WaitOpts) or TxHash (if wait is NO_WAIT) + +##### send + +**Signature:** + +```typescript +public send<TReturn = TxReceipt, W extends InteractionWaitOptions = undefined>(options: SendInteractionOptions<W>): Promise<SendReturn<W, TReturn>> +``` + +**Parameters:** + +- `options`: `SendInteractionOptions<W>` + +**Returns:** + +`Promise<SendReturn<W, TReturn>>` + +##### send + +**Signature:** + +```typescript +public async send<TReturn = TxReceipt>(options: SendInteractionOptions<InteractionWaitOptions>): Promise<SendReturn<typeof options.wait, TReturn>> +``` + +**Parameters:** + +- `options`: `SendInteractionOptions<InteractionWaitOptions>` + +**Returns:** + +`Promise<SendReturn<typeof options.wait, TReturn>>` + +--- + +### `contract/batch_call.ts` + + +#### BatchCall + +**Type:** Class + +A batch of function calls to be sent as a single transaction through a wallet. + +**Extends:** `BaseContractInteraction` + + +#### Constructor + +**Signature:** + +```typescript +constructor( + wallet: Wallet, + protected interactions: (BaseContractInteraction | ExecutionPayload)[], + private extraHashedArgs: HashedValues[] = [] +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `interactions`: `(BaseContractInteraction | ExecutionPayload)[]` +- `extraHashedArgs` (optional): `HashedValues[]` + +#### Methods + + +##### request + +Returns an execution request that represents this operation. + +**Signature:** + +```typescript +public async request(options: RequestInteractionOptions = {}): Promise<ExecutionPayload> +``` + +**Parameters:** + +- `options` (optional): `RequestInteractionOptions` + - An optional object containing additional configuration for the request generation. + +**Returns:** + +`Promise<ExecutionPayload>` - An execution payload wrapped in promise. + +##### simulate + +Simulates/executes the batch, supporting private, public and utility functions. Although this is a single interaction with the wallet, private and public functions will be grouped into a single ExecutionPayload that the wallet will simulate as a single transaction. Utility function calls will be executed one by one. + +**Signature:** + +```typescript +public async simulate(options: SimulateInteractionOptions): Promise<SimulationResult> +``` + +**Parameters:** + +- `options`: `SimulateInteractionOptions` + - An optional object containing additional configuration for the interaction. + +**Returns:** + +`Promise<SimulationResult>` - The results of all the interactions that make up the batch + +##### getExecutionPayloads + +**Signature:** + +```typescript +protected async getExecutionPayloads(): Promise<ExecutionPayload[]> +``` + +**Returns:** + +`Promise<ExecutionPayload[]>` + +--- + +### `contract/checker.ts` + + +#### abiChecker + +**Type:** Function + +Validates the given ContractArtifact object by checking its functions and their parameters. Ensures that the ABI has at least one function, a constructor, valid bytecode, and correct parameter types. Throws an error if any inconsistency is detected during the validation process. + +**Signature:** + +```typescript +export abiChecker(artifact: ContractArtifact) +``` + +**Parameters:** + +- `artifact`: `ContractArtifact` + - The ContractArtifact object to be validated. + +**Returns:** + +`boolean` - A boolean value indicating whether the artifact is valid or not. + +--- + +### `contract/contract.ts` + + +#### Contract + +**Type:** Class + +The Contract class represents a contract and provides utility methods for interacting with it. It enables the creation of ContractFunctionInteraction instances for each function in the contract's ABI, allowing users to call or send transactions to these functions. Additionally, the Contract class can be used to attach the contract instance to a deployed contract onchain through the PXE, which facilitates interaction with Aztec's privacy protocol. + +**Extends:** `ContractBase` + + +#### Methods + + +##### at + +Gets a contract instance. + +**Signature:** + +```typescript +public static at( + address: AztecAddress, + artifact: ContractArtifact, + wallet: Wallet +): Contract +``` + +**Parameters:** + +- `address`: `AztecAddress` + - The address of the contract instance. +- `artifact`: `ContractArtifact` + - Build artifact of the contract. +- `wallet`: `Wallet` + - The wallet to use when interacting with the contract. + +**Returns:** + +`Contract` - A promise that resolves to a new Contract instance. + +##### deploy + +Creates a tx to deploy (initialize and/or publish) a new instance of a contract. + +**Signature:** + +```typescript +public static deploy( + wallet: Wallet, + artifact: ContractArtifact, + args: any[], + constructorName?: string, + instantiation?: DeployInstantiationOptions +) +``` + +**Parameters:** + +- `wallet`: `Wallet` + - The wallet for executing the deployment. +- `artifact`: `ContractArtifact` + - Build artifact of the contract to deploy +- `args`: `any[]` + - Arguments for the constructor. +- `constructorName` (optional): `string` + - The name of the constructor function to call. +- `instantiation` (optional): `DeployInstantiationOptions` + - Other address-affecting parameters (salt, deployer / universalDeploy, publicKeys). + +**Returns:** + +`DeployMethod<Contract>` + +--- + +### `contract/contract_base.ts` + + +#### ContractMethod + +**Type:** Type Alias + +Type representing a contract method that returns a ContractFunctionInteraction instance and has a readonly 'selector' property of type Buffer. Takes any number of arguments. + +**Signature:** + +```typescript +export type ContractMethod = ((...args: any[]) => ContractFunctionInteraction) & { + selector: () => Promise<FunctionSelector>; +}; +``` + +**Type Members:** + +##### selector + +The unique identifier for a contract function in bytecode. + +**Type:** `() => Promise<FunctionSelector>` + + +#### ContractStorageLayout + +**Type:** Type Alias + +Type representing the storage layout of a contract. + +**Signature:** + +```typescript +export type ContractStorageLayout<T extends string> = { + [K in T]: FieldLayout; +}; +``` + +**Type Members:** + +##### [K in T] + +**Signature:** `[K in T]: FieldLayout` + +**Key Type:** `T` + +**Value Type:** `FieldLayout` + + +#### ContractBase + +**Type:** Class + +Abstract implementation of a contract extended by the Contract class and generated contract types. + + +#### Constructor + +**Signature:** + +```typescript +protected constructor( + public readonly address: AztecAddress, + public readonly artifact: ContractArtifact, + public wallet: Wallet +) +``` + +**Parameters:** + +- `address`: `AztecAddress` + - The contract's address. +- `artifact`: `ContractArtifact` + - The Application Binary Interface for the contract. +- `wallet`: `Wallet` + - The wallet used for interacting with this contract. + +#### Properties + + +##### methods + +An object containing contract methods mapped to their respective names. + +**Type:** `{ [name: string]: ContractMethod }` + + +#### Methods + + +##### withWallet + +Creates a new instance of the contract wrapper attached to a different wallet. + +**Signature:** + +```typescript +public withWallet(wallet: Wallet): this +``` + +**Parameters:** + +- `wallet`: `Wallet` + - Wallet to use for sending txs. + +**Returns:** + +`this` - A new contract instance. + +--- + +### `contract/contract_function_interaction.ts` + + +#### ContractFunctionInteraction + +**Type:** Class + +This is the class that is returned when calling e.g. `contract.methods.myMethod(arg0, arg1)`. It contains available interactions one can call on a method, including view. + +**Extends:** `BaseContractInteraction` + + +#### Constructor + +**Signature:** + +```typescript +constructor( + wallet: Wallet, + protected contractAddress: AztecAddress, + protected functionDao: FunctionAbi, + protected args: any[], + authWitnesses: AuthWitness[] = [], + capsules: Capsule[] = [], + private extraHashedArgs: HashedValues[] = [] +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `contractAddress`: `AztecAddress` +- `functionDao`: `FunctionAbi` +- `args`: `any[]` +- `authWitnesses` (optional): `AuthWitness[]` +- `capsules` (optional): `Capsule[]` +- `extraHashedArgs` (optional): `HashedValues[]` + +#### Methods + + +##### getFunctionCall + +Returns the encoded function call wrapped by this interaction Useful when generating authwits + +**Signature:** + +```typescript +public async getFunctionCall(): Promise<FunctionCall> +``` + +**Returns:** + +`Promise<FunctionCall>` - An encoded function call + +##### request + +Returns the execution payload that allows this operation to happen on chain. + +**Signature:** + +```typescript +public override async request(options: RequestInteractionOptions = {}): Promise<ExecutionPayload> +``` + +**Parameters:** + +- `options` (optional): `RequestInteractionOptions` + - Configuration options. + +**Returns:** + +`Promise<ExecutionPayload>` - The execution payload for this operation + +##### simulate + +Simulate a transaction and get information from its execution. Differs from prove in a few important ways: 1. It returns the values of the function execution, plus additional metadata if requested 2. It supports `utility`, `private` and `public` functions + +**Signature:** + +```typescript +public async simulate(options: SimulateInteractionOptions = {} as SimulateInteractionOptions): Promise<SimulationResult> +``` + +**Parameters:** + +- `options` (optional): `SimulateInteractionOptions` + - An optional object containing additional configuration for the simulation. + +**Returns:** + +`Promise<SimulationResult>` - Depending on the simulation options, this method directly returns the result value of the executed function or a rich object containing extra metadata, such as estimated gas costs (if requested via options), execution statistics and emitted offchain effects + +##### profile + +Simulate a transaction and profile the gate count for each function in the transaction. + +**Signature:** + +```typescript +public async profile(options: ProfileInteractionOptions): Promise<TxProfileResult> +``` + +**Parameters:** + +- `options`: `ProfileInteractionOptions` + - Same options as `simulate`, plus profiling method + +**Returns:** + +`Promise<TxProfileResult>` - An object containing the function return value and profile result. + +##### with + +Augments this ContractFunctionInteraction with additional metadata, such as authWitnesses, capsules, and extraHashedArgs. This is useful when creating a "batteries included" interaction, such as registering a contract class with its associated capsule instead of having the user provide them externally. + +**Signature:** + +```typescript +public with({ authWitnesses = [], capsules = [], extraHashedArgs = [], }: { + authWitnesses?: AuthWitness[]; + capsules?: Capsule[]; + extraHashedArgs?: HashedValues[]; +}): ContractFunctionInteraction +``` + +**Parameters:** + +- `{ authWitnesses = [], capsules = [], extraHashedArgs = [], }`: + + ```typescript + { + /** The authWitnesses to add to the interaction */ + authWitnesses?: AuthWitness[]; + /** The capsules to add to the interaction */ + capsules?: Capsule[]; + /** The extra hashed args to add to the interaction */ + extraHashedArgs?: HashedValues[]; + } + ``` + + +**Returns:** + +`ContractFunctionInteraction` - A new ContractFunctionInteraction with the added metadata, but calling the same original function in the same manner + +--- + +### `contract/deploy_method.ts` + + +#### DeployInstantiationOptions + +**Type:** Type Alias + +Inputs that determine the contract's deployment address. `salt` and `publicKeys` are optional and default to a random Fr and `PublicKeys.default()` respectively. `deployer` and `universalDeploy` are mutually exclusive and both optional: - If neither is supplied, the deployer is locked lazily on the first `send` / `simulate` / `profile` call from `options.from` (NO_FROM/undefined → universal). This preserves the ergonomics of `MyContract.deploy(wallet, ...args).send({ from: alice })`. - If `deployer` or `universalDeploy: true` is supplied, the deployer is locked at construction. Once locked, the deployer cannot change. Subsequent calls with a `from` that would imply a different deployer throw — except when locked to `AztecAddress.ZERO` (universal), which is compatible with any sender. + +**Signature:** + +```typescript +export type DeployInstantiationOptions = { + salt?: Fr; + deployer?: AztecAddress; + universalDeploy?: boolean; + publicKeys?: PublicKeys; + immutablesHash?: Fr; +}; +``` + +**Type Members:** + +##### salt + +Salt used to derive the contract address. Defaults to a random Fr. + +**Type:** `Fr` + +##### deployer + +Deployer address mixed into the address preimage. Mutually exclusive with `universalDeploy`. + +**Type:** `AztecAddress` + +##### universalDeploy + +If true, the contract is deployed universally (deployer = AztecAddress.ZERO in the address preimage). Mutually exclusive with `deployer`. + +**Type:** `boolean` + +##### publicKeys + +Public keys mixed into the address. Defaults to PublicKeys.default(). + +**Type:** `PublicKeys` + +##### immutablesHash + +Commitment to the contract's immutable storage values. Folded into the salted initialization hash, so a non-zero value affects the derived address. Defaults to `Fr.ZERO`. + +**Type:** `Fr` + + +#### BoundInstantiationOptions + +**Type:** Type Alias + +Narrowed `DeployInstantiationOptions` accepted by BoundDeployMethod: requires a concrete `deployer` and forbids `universalDeploy`. The runtime check that `deployer` is non-zero stays as defense in depth (it's a value-level invariant the type system can't model). + +**Signature:** + +```typescript +export type BoundInstantiationOptions = SharedInstantiationOptions & { + deployer: AztecAddress; + universalDeploy?: never; +}; +``` + +**Type Members:** + +##### deployer + +Concrete deployer mixed into the address preimage. Required, must be non-zero. + +**Type:** `AztecAddress` + +##### universalDeploy + +Forbidden on `BoundDeployMethod`; use `UniversalDeployMethod` for universal deploys. + +**Type:** `never` + + +#### UniversalInstantiationOptions + +**Type:** Type Alias + +Narrowed `DeployInstantiationOptions` accepted by UniversalDeployMethod: forbids `deployer` and requires `universalDeploy: true` (so the call site reads as a universal deploy). + +**Signature:** + +```typescript +export type UniversalInstantiationOptions = SharedInstantiationOptions & { + deployer?: never; + universalDeploy: true; +}; +``` + +**Type Members:** + +##### deployer + +Forbidden on `UniversalDeployMethod`; use `BoundDeployMethod` if you need a concrete deployer. + +**Type:** `never` + +##### universalDeploy + +Marks this as a universal deploy. Required for clarity at the call site. + +**Type:** `true` + + +#### PendingInstantiationOptions + +**Type:** Type Alias + +Narrowed `DeployInstantiationOptions` accepted by PendingDeployMethod: forbids both `deployer` and `universalDeploy`. The deploy is locked from the first send-time `from` instead. + +**Signature:** + +```typescript +export type PendingInstantiationOptions = SharedInstantiationOptions & { + deployer?: never; + universalDeploy?: never; +}; +``` + +**Type Members:** + +##### deployer + +Forbidden on `PendingDeployMethod`; use `BoundDeployMethod` for a concrete deployer. + +**Type:** `never` + +##### universalDeploy + +Forbidden on `PendingDeployMethod`; use `UniversalDeployMethod` for a universal deploy. + +**Type:** `never` + + +#### DeployMethodContract + +**Type:** Type Alias + +Identifies *which contract* is being deployed and *with what initializer*. + +**Signature:** + +```typescript +export type DeployMethodContract<TContract extends ContractBase = ContractBase> = { + artifact: ContractArtifact; + postDeployCtor: (instance: ContractInstanceWithAddress, wallet: Wallet) => TContract; + args?: any[]; + constructorNameOrArtifact?: string | FunctionArtifact; +}; +``` + +**Type Members:** + +##### artifact + +Build artifact of the contract being deployed. + +**Type:** `ContractArtifact` + +##### postDeployCtor + +Factory invoked after deployment to produce the typed contract handle. + +**Type:** `(instance: ContractInstanceWithAddress, wallet: Wallet) => TContract` + +##### args + +Encoded constructor arguments for the contract. Defaults to `[]`. + +**Type:** `any[]` + +##### constructorNameOrArtifact + +Name (or full artifact) of the initializer to call. + +**Type:** `string | FunctionArtifact` + + +#### DeployMethodPayload + +**Type:** Type Alias + +Execution-payload metadata propagated through `request` / `send` / `simulate` / `profile`. + +**Signature:** + +```typescript +export type DeployMethodPayload = { + authWitnesses?: AuthWitness[]; + capsules?: Capsule[]; + extraHashedArgs?: HashedValues[]; +}; +``` + +**Type Members:** + +##### authWitnesses + +Auth witnesses propagated to the deploy interaction. + +**Type:** `AuthWitness[]` + +##### capsules + +Capsules propagated to the deploy interaction. + +**Type:** `Capsule[]` + +##### extraHashedArgs + +Extra hashed args propagated to the deploy interaction. + +**Type:** `HashedValues[]` + + +#### RequestDeployOptions + +**Type:** Type Alias + +Options for deploying a contract on the Aztec network. Controls publication and registration policy for this deployment. + +**Signature:** + +```typescript +export type RequestDeployOptions = RequestInteractionOptions & { + skipClassPublication?: boolean; + skipInstancePublication?: boolean; + skipInitialization?: boolean; + skipRegistration?: boolean; +}; +``` + +**Type Members:** + +##### skipClassPublication + +Skip contract class publication. + +**Type:** `boolean` + +##### skipInstancePublication + +Skip publication, instead just privately initialize the contract. + +**Type:** `boolean` + +##### skipInitialization + +Skip contract initialization. + +**Type:** `boolean` + +##### skipRegistration + +Skip contract registration in the wallet + +**Type:** `boolean` + + +#### DeployOptionsWithoutWait + +**Type:** Type Alias + +Base deployment options without wait parameter. + +**Signature:** + +```typescript +export type DeployOptionsWithoutWait = RequestDeployOptions & + Pick<SendInteractionOptionsWithoutWait, 'from' | 'fee' | 'additionalScopes'>; +``` + +#### DeployOptions + +**Type:** Type Alias + +Extends the deployment options with the required parameters to send the transaction. + +**Signature:** + +```typescript +export type DeployOptions<W extends InteractionWaitOptions = undefined> = DeployOptionsWithoutWait & { + wait?: W; +}; +``` + +**Type Members:** + +##### wait + +Options for waiting for the transaction to be mined. - undefined (default): wait with default options and return the contract instance - WaitOpts: wait with custom options - NO_WAIT: return TxHash immediately without waiting + +**Type:** `W` + + +#### SimulateDeployOptions + +**Type:** Type Alias + +Options for simulating the deployment of a contract Allows skipping certain validations and computing gas estimations + +**Signature:** + +```typescript +export type SimulateDeployOptions = Omit<DeployOptionsWithoutWait, 'fee'> & { + fee?: InteractionFeeOptions; + skipTxValidation?: boolean; + skipFeeEnforcement?: boolean; + includeMetadata?: boolean; +}; +``` + +**Type Members:** + +##### fee + +The fee options for the transaction. + +**Type:** `InteractionFeeOptions` + +##### skipTxValidation + +Simulate without checking for the validity of the resulting transaction, e.g. whether it emits any existing nullifiers. + +**Type:** `boolean` + +##### skipFeeEnforcement + +Whether to ensure the fee payer is not empty and has enough balance to pay for the fee. + +**Type:** `boolean` + +##### includeMetadata + +Whether to include metadata such as offchain effects and performance statistics (e.g. timing information of the different circuits and oracles) in the simulation result, instead of just the return value of the function + +**Type:** `boolean` + + +#### DeployResultMined + +**Type:** Type Alias + +Result of deploying a contract when waiting for mining (default case). + +**Signature:** + +```typescript +export type DeployResultMined<TContract extends ContractBase> = { + contract: TContract; + instance: ContractInstanceWithAddress; + receipt: TxReceipt; +} & OffchainOutput; +``` + +**Type Members:** + +##### contract + +The deployed contract instance. + +**Type:** `TContract` + +##### instance + +The deployed contract instance with address and metadata. + +**Type:** `ContractInstanceWithAddress` + +##### receipt + +The deploy transaction receipt. + +**Type:** `TxReceipt` + + +#### DeployReturn + +**Type:** Type Alias + +Conditional return type for deploy based on wait options. + +**Signature:** + +```typescript +export type DeployReturn<TContract extends ContractBase, W extends InteractionWaitOptions> = W extends NoWait + ? TxSendResultImmediate + : DeployResultMined<TContract>; +``` + +#### DeployMethod + +**Type:** Class + +Umbrella type for a contract deployment interaction. `DeployMethod` is abstract: callers always interact with one of three concrete flavors — BoundDeployMethod, UniversalDeployMethod, or PendingDeployMethod — picked by DeployMethod.create based on the supplied DeployInstantiationOptions. The flavors only differ in their initial deployer-lock state; the full API (`request` / `send` / `simulate` / `profile` / `getInstance` / `getAddress` / `getPartialAddress` / `register` / `with`) lives on this base, so consumers can type variables as `DeployMethod<T>` and treat all three uniformly. The deployer (and therefore the deployed address) is locked once and never changes. Locking happens either at construction (via `deployer` or `universalDeploy: true` in the instantiation options) or lazily on the first `send` / `simulate` / `profile` call, which lock from `options.from`. Once locked: - The address is stable for the lifetime of this object. - Subsequent `send` / `simulate` / `profile` calls with a `from` that would imply a different deployer throw, to prevent silently deploying at a different address than `getAddress()` reported. - A locked universal deployer (`AztecAddress.ZERO`) is compatible with any `from`, since the address does not depend on the sender. Note that for some contracts, a tx is not required as part of its "creation": If there are no public functions, and if there are no initialization functions, then technically the contract has already been "created", and all of the contract's functions (private and utility) can be interacted-with immediately, without any "deployment tx". + +**Extends:** `BaseContractInteraction` + + +#### Constructor + +**Signature:** + +```typescript +protected constructor( + wallet: Wallet, + contract: DeployMethodContract<TContract>, + salt: Fr | undefined, + publicKeys: PublicKeys | undefined, + immutablesHash: Fr | undefined, + payload: DeployMethodPayload = {} +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `contract`: `DeployMethodContract<TContract>` +- `salt`: `Fr | undefined` +- `publicKeys`: `PublicKeys | undefined` +- `immutablesHash`: `Fr | undefined` +- `payload` (optional): `DeployMethodPayload` + +#### Properties + + +##### salt + +Salt used in the address preimage. + +**Type:** `Fr` + + +##### publicKeys + +Public keys mixed into the address preimage. + +**Type:** `PublicKeys` + + +##### immutablesHash + +Immutables hash folded into the salted initialization hash. + +**Type:** `Fr` + + +##### #instancePromise + +Cached instance promise; resolved once the deployer is known. + +**Type:** `Promise<ContractInstanceWithAddress>` + + +##### #resolvedInstance + +Resolved value of `#instancePromise`, populated synchronously once the promise settles. + +**Type:** `ContractInstanceWithAddress` + + +##### constructorArtifact + +Constructor function to call. + +**Type:** `FunctionAbi | undefined` + + +##### artifact + +Build artifact of the contract being deployed. + +**Type:** `ContractArtifact` + + +##### postDeployCtor + +Factory invoked after deployment to produce the typed contract handle. + +**Type:** `(instance: ContractInstanceWithAddress, wallet: Wallet) => TContract` + + +##### args + +Encoded constructor arguments for the contract. + +**Type:** `any[]` + + +##### extraHashedArgs + +Extra hashed args propagated through `with(...)` and into the deploy payload. + +**Type:** `HashedValues[]` + + +#### Methods + + +##### getDeployerAddress + +The address that will be mixed into the contract's address preimage. Owned returns the concrete deployer; Universal returns `AztecAddress.ZERO`; Pending throws unless a prior `send` / `simulate` / `profile` call has already locked it. + +**Signature:** + +```typescript +public abstract getDeployerAddress(): AztecAddress +``` + +**Returns:** + +`AztecAddress` + +##### lockDeployer + +Reconciles a send-time `from` with the deploy's deployer. Owned asserts an exact match; Universal accepts anything; Pending uses the first call to lock its deployer (transitioning into an Owned/Universal sibling), then defers to that sibling's assertion on subsequent calls. The "locks-or-asserts" name is intentional: only Pending mutates state, and only on its first invocation. Owned and Universal are pure assertions. + +**Signature:** + +```typescript +public abstract lockDeployer(from: SendInteractionOptionsWithoutWait['from'] | undefined): void +``` + +**Parameters:** + +- `from`: `SendInteractionOptionsWithoutWait['from'] | undefined` + - The send-time `from` value (`AztecAddress`, `NO_FROM`, or `undefined`). + +**Returns:** + +`void` + +##### cloneInstantiation + +Returns the DeployInstantiationOptions that match this flavor. Used by `with(...)` to spawn a sibling instance carrying the same lock state. + +**Signature:** + +```typescript +public abstract cloneInstantiation(): DeployInstantiationOptions +``` + +**Returns:** + +`DeployInstantiationOptions` + +##### create + +Constructs the right concrete `DeployMethod` flavor for the supplied instantiation options: - `{ deployer: <addr> }` → BoundDeployMethod - `{ universalDeploy: true }` → UniversalDeployMethod - neither set → PendingDeployMethod Mixing `deployer` and `universalDeploy` throws. Returns the umbrella `DeployMethod<T>` type so callers can use the result generically without narrowing. + +**Signature:** + +```typescript +public static create<TContract extends ContractBase>( + wallet: Wallet, + contract: DeployMethodContract<TContract>, + instantiation: DeployInstantiationOptions = {}, + payload: DeployMethodPayload = {} +): DeployMethod<TContract> +``` + +**Parameters:** + +- `wallet`: `Wallet` + - Wallet used to send / simulate the deploy tx. +- `contract`: `DeployMethodContract<TContract>` + - The contract being deployed (artifact, factory, args, initializer). +- `instantiation` (optional): `DeployInstantiationOptions` + - Address-affecting parameters (salt, deployer / universalDeploy, publicKeys). Defaults to pending. +- `payload` (optional): `DeployMethodPayload` + - Auth witnesses, capsules, and extra hashed args propagated to the deploy. Defaults to empty. + +**Returns:** + +`DeployMethod<TContract>` + +##### request + +Returns the execution payload that allows this operation to happen on chain. Requires the deployer to be known — call `getDeployerAddress()` first; on a `PendingDeployMethod` this throws unless a prior `send` / `simulate` / `profile` has already locked the deployer. + +**Signature:** + +```typescript +public async request(options: RequestDeployOptions = {}): Promise<ExecutionPayload> +``` + +**Parameters:** + +- `options` (optional): `RequestDeployOptions` + - Configuration options. + +**Returns:** + +`Promise<ExecutionPayload>` - The execution payload for this operation + +##### convertDeployOptionsToSendOptions + +Converts DeployOptions to SendOptions. + +**Signature:** + +```typescript +protected convertDeployOptionsToSendOptions<W extends InteractionWaitOptions>(options: DeployOptions<W>): SendOptions<W> +``` + +**Parameters:** + +- `options`: `DeployOptions<W>` + - Deploy options with wait parameter. + +**Returns:** + +`SendOptions<W>` + +##### convertDeployOptionsToSimulateOptions + +Converts deploy simulation options into wallet-level simulate options. + +**Signature:** + +```typescript +protected convertDeployOptionsToSimulateOptions(options: SimulateDeployOptions): SimulateOptions +``` + +**Parameters:** + +- `options`: `SimulateDeployOptions` + - The deploy simulation options to convert. + +**Returns:** + +`SimulateOptions` + +##### convertDeployOptionsToProfileOptions + +Converts deploy profile options into wallet-level profile options. + +**Signature:** + +```typescript +protected convertDeployOptionsToProfileOptions(options: DeployOptionsWithoutWait & ProfileInteractionOptions): ProfileOptions +``` + +**Parameters:** + +- `options`: `DeployOptionsWithoutWait & ProfileInteractionOptions` + - The deploy profile options to convert. + +**Returns:** + +`ProfileOptions` + +##### register + +Adds this contract to the wallet and returns the Contract object. + +**Signature:** + +```typescript +public async register(): Promise<TContract> +``` + +**Returns:** + +`Promise<TContract>` + +##### getPublicationExecutionPayload + +Returns an execution payload for: - publication of the contract class and - publication of the contract instance to enable public execution depending on the provided options. + +**Signature:** + +```typescript +protected async getPublicationExecutionPayload(options?: RequestDeployOptions): Promise<ExecutionPayload> +``` + +**Parameters:** + +- `options` (optional): `RequestDeployOptions` + - Contract creation options. + +**Returns:** + +`Promise<ExecutionPayload>` - An execution payload with potentially calls (and bytecode capsule) to the class registry and instance registry. + +##### getInitializationExecutionPayload + +Returns the calls necessary to initialize the contract. + +**Signature:** + +```typescript +protected async getInitializationExecutionPayload(options?: RequestDeployOptions): Promise<ExecutionPayload> +``` + +**Parameters:** + +- `options` (optional): `RequestDeployOptions` + - Deployment options. + +**Returns:** + +`Promise<ExecutionPayload>` - An array of function calls. + +##### send + +Send a contract deployment transaction (initialize and/or publish) using the provided options. By default, waits for the transaction to be mined and returns the deployed contract instance. + +**Signature:** + +```typescript +public override send(options: DeployOptionsWithoutWait): Promise<DeployResultMined<TContract>> +``` + +**Parameters:** + +- `options`: `DeployOptionsWithoutWait` + - An object containing various deployment options such as `from` and `fee`. + +**Returns:** + +`Promise<DeployResultMined<TContract>>` - TxHash (if wait is NO_WAIT), or DeployResultMined with contract, receipt, and instance (otherwise) + +##### send + +**Signature:** + +```typescript +public override send<W extends InteractionWaitOptions>(options: DeployOptions<W>): Promise<DeployReturn<TContract, W>> +``` + +**Parameters:** + +- `options`: `DeployOptions<W>` + +**Returns:** + +`Promise<DeployReturn<TContract, W>>` + +##### send + +**Signature:** + +```typescript +public override async send(options: DeployOptions<InteractionWaitOptions>): Promise<any> +``` + +**Parameters:** + +- `options`: `DeployOptions<InteractionWaitOptions>` + +**Returns:** + +`Promise<any>` + +##### getInstance + +Builds the contract instance and returns it. The instance is computed once and cached for the lifetime of this DeployMethod; subsequent calls return the same instance. On a PendingDeployMethod this throws unless a prior `send` / `simulate` / `profile` call has already locked the deployer — otherwise the resolved address could silently differ from the eventually-deployed one. + +**Signature:** + +```typescript +public getInstance(): Promise<ContractInstanceWithAddress> +``` + +**Returns:** + +`Promise<ContractInstanceWithAddress>` - An instance object. + +##### simulate + +Simulate the deployment + +**Signature:** + +```typescript +public async simulate(options: SimulateDeployOptions): Promise<SimulationResult> +``` + +**Parameters:** + +- `options`: `SimulateDeployOptions` + - An optional object containing additional configuration for the simulation. + +**Returns:** + +`Promise<SimulationResult>` - A simulation result object containing metadata of the execution, including gas estimations (if requested via options), execution statistics and emitted offchain effects + +##### profile + +Simulate a deployment and profile the gate count for each function in the transaction. + +**Signature:** + +```typescript +public async profile(options: DeployOptionsWithoutWait & ProfileInteractionOptions): Promise<TxProfileResult> +``` + +**Parameters:** + +- `options`: `DeployOptionsWithoutWait & ProfileInteractionOptions` + - Same options as `send`, plus extra profiling options. + +**Returns:** + +`Promise<TxProfileResult>` - An object containing the function return value and profile result. + +##### getAddress + +Returns the deployed contract address. + +**Signature:** + +```typescript +public async getAddress(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getPartialAddress + +Returns the partial address for this deployment. + +**Signature:** + +```typescript +public async getPartialAddress(): Promise<Fr> +``` + +**Returns:** + +`Promise<Fr>` + +##### getCachedInstanceOrThrow + +Returns the cached resolved instance synchronously, or throws if no instance has been computed yet. Intended for subclasses that run inside a code path where `getInstance()` is guaranteed to have already been awaited (e.g. `request()` invoked it). Not part of the public API. + +**Signature:** + +```typescript +protected getCachedInstanceOrThrow(): ContractInstanceWithAddress +``` + +**Returns:** + +`ContractInstanceWithAddress` + +##### with + +Augments this DeployMethod with additional metadata, such as authWitnesses and capsules. The deployer lock is preserved: a Pending that has not yet been locked stays Pending; a Pending that has already locked, along with Owned and Universal, returns the matching locked flavor so the cloned method deploys at the same address as `this`. + +**Signature:** + +```typescript +public with({ authWitnesses = [], capsules = [], extraHashedArgs = [], }: { + authWitnesses?: AuthWitness[]; + capsules?: Capsule[]; + extraHashedArgs?: HashedValues[]; +}): DeployMethod<TContract> +``` + +**Parameters:** + +- `{ authWitnesses = [], capsules = [], extraHashedArgs = [], }`: + + ```typescript + { + /** The authWitnesses to add to the deployment */ + authWitnesses?: AuthWitness[]; + /** The capsules to add to the deployment */ + capsules?: Capsule[]; + /** The extra hashed args to add to the deployment */ + extraHashedArgs?: HashedValues[]; + } + ``` + + +**Returns:** + +`DeployMethod<TContract>` - A new DeployMethod with the added metadata, but calling the same original function in the same manner + +#### BoundDeployMethod + +**Type:** Class + +Deploy method whose deployer is fixed at construction to a concrete AztecAddress. The deployer is mixed into the address preimage, so the contract address is fully determined. Sending from a different account throws — letting it through would silently produce a deployed address different from the one `getAddress()` reported. + +**Extends:** `DeployMethod` + + +#### Constructor + +**Signature:** + +```typescript +public constructor( + wallet: Wallet, + contract: DeployMethodContract<TContract>, + instantiation: BoundInstantiationOptions, + payload: DeployMethodPayload = {} +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `contract`: `DeployMethodContract<TContract>` +- `instantiation`: `BoundInstantiationOptions` +- `payload` (optional): `DeployMethodPayload` + +#### Properties + + +##### deployer + +The address baked into the address preimage. Read-only — set at construction. + +**Type:** `AztecAddress` + + +#### Methods + + +##### getDeployerAddress + +Returns the locked deployer baked into the address preimage. + +**Signature:** + +```typescript +public getDeployerAddress(): AztecAddress +``` + +**Returns:** + +`AztecAddress` + +##### lockDeployer + +Throws unless `from` matches the locked deployer; the deployer is part of the address. + +**Signature:** + +```typescript +public lockDeployer(from: SendInteractionOptionsWithoutWait['from'] | undefined): void +``` + +**Parameters:** + +- `from`: `SendInteractionOptionsWithoutWait['from'] | undefined` + - The send-time `from` value (`AztecAddress`, `NO_FROM`, or `undefined`). + +**Returns:** + +`void` + +##### cloneInstantiation + +Re-emits this method's `DeployInstantiationOptions` for `with(...)` to consume. + +**Signature:** + +```typescript +public cloneInstantiation(): DeployInstantiationOptions +``` + +**Returns:** + +`DeployInstantiationOptions` + +#### UniversalDeployMethod + +**Type:** Class + +Deploy method whose deployer is fixed at construction to AztecAddress.ZERO (universal deploy). The address does not depend on the sender, so any account may sign the deploy tx. + +**Extends:** `DeployMethod` + + +#### Constructor + +**Signature:** + +```typescript +public constructor( + wallet: Wallet, + contract: DeployMethodContract<TContract>, + instantiation: UniversalInstantiationOptions, + payload: DeployMethodPayload = {} +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `contract`: `DeployMethodContract<TContract>` +- `instantiation`: `UniversalInstantiationOptions` +- `payload` (optional): `DeployMethodPayload` + +#### Methods + + +##### getDeployerAddress + +Universal deploys are anchored at `AztecAddress.ZERO`; the sender does not enter the preimage. + +**Signature:** + +```typescript +public getDeployerAddress(): AztecAddress +``` + +**Returns:** + +`AztecAddress` + +##### lockDeployer + +Universal deploys accept any sender, including `NO_FROM` / `undefined`. + +**Signature:** + +```typescript +public lockDeployer(_from: SendInteractionOptionsWithoutWait['from'] | undefined): void +``` + +**Parameters:** + +- `_from`: `SendInteractionOptionsWithoutWait['from'] | undefined` + - Ignored. + +**Returns:** + +`void` + +##### cloneInstantiation + +Re-emits this method's `DeployInstantiationOptions` for `with(...)` to consume. + +**Signature:** + +```typescript +public cloneInstantiation(): DeployInstantiationOptions +``` + +**Returns:** + +`DeployInstantiationOptions` + +#### PendingDeployMethod + +**Type:** Class + +Deploy method whose deployer is not yet decided. The first `send` / `simulate` / `profile` call promotes this into an BoundDeployMethod or UniversalDeployMethod (depending on whether `options.from` is an address or `NO_FROM` / `undefined`); subsequent calls reuse that promotion and reject mismatching `from` values. Reading the address (`getInstance` / `getAddress` / `getPartialAddress`) or building a payload (`request`) before the promotion happens throws — the address would otherwise be ambiguous and could differ from what `send()` ends up deploying. + +**Extends:** `DeployMethod` + + +#### Constructor + +**Signature:** + +```typescript +public constructor( + wallet: Wallet, + contract: DeployMethodContract<TContract>, + instantiation: PendingInstantiationOptions = {}, + payload: DeployMethodPayload = {} +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `contract`: `DeployMethodContract<TContract>` +- `instantiation` (optional): `PendingInstantiationOptions` +- `payload` (optional): `DeployMethodPayload` + +#### Properties + + +##### #locked + +The locked sibling created on the first send-side call. Once set, all flavor-specific decisions (sender compatibility, address derivation, clone shape) delegate to it, so a second call with a mismatched `from` is rejected by `BoundDeployMethod.lockDeployer`. + +**Type:** `BoundDeployMethod<TContract> | UniversalDeployMethod<TContract>` + + +#### Methods + + +##### getDeployerAddress + +Returns the locked deployer once it has happened. Throws while still pending — the address would otherwise differ from what `send()` ends up deploying. + +**Signature:** + +```typescript +public getDeployerAddress(): AztecAddress +``` + +**Returns:** + +`AztecAddress` + +##### lockDeployer + +On the first call, promotes this pending method into a locked sibling and remembers it. On subsequent calls, defers to the locked sibling — so a mismatched `from` is rejected by the sibling's own policy, not a duplicate one here. + +**Signature:** + +```typescript +public lockDeployer(from: SendInteractionOptionsWithoutWait['from'] | undefined): void +``` + +**Parameters:** + +- `from`: `SendInteractionOptionsWithoutWait['from'] | undefined` + - The send-time `from` value (`AztecAddress`, `NO_FROM`, or `undefined`). + +**Returns:** + +`void` + +##### cloneInstantiation + +Re-emits this method's `DeployInstantiationOptions` for `with(...)` to consume. + +**Signature:** + +```typescript +public cloneInstantiation(): DeployInstantiationOptions +``` + +**Returns:** + +`DeployInstantiationOptions` + +##### #promoteFrom + +Builds the locked sibling implied by a send-time `from`: an `AztecAddress` becomes BoundDeployMethod; `NO_FROM` / `undefined` becomes UniversalDeployMethod. + +**Signature:** + +```typescript +#promoteFrom(from: SendInteractionOptionsWithoutWait['from'] | undefined): BoundDeployMethod<TContract> | UniversalDeployMethod<TContract> +``` + +**Parameters:** + +- `from`: `SendInteractionOptionsWithoutWait['from'] | undefined` + - The send-time `from` value. + +**Returns:** + +`BoundDeployMethod<TContract> | UniversalDeployMethod<TContract>` + +--- + +### `contract/fastforward_contract_update.ts` + + +#### fastForwardContractUpdate + +**Type:** Function + +Builds `SimulationOverrides` that simulate a deployed instance as if it had already been upgraded to a new contract class. Mirrors a real onchain upgrade (scheduling the new class and waiting out the delay): - `publicStorage` rewrites the `ContractInstanceRegistry`'s delayed-public-mutable storage so the AVM's `UpdateCheck` resolves to the new class id. - `contracts` swaps the deployed instance for one whose `currentContractClassId` is bumped to the new class. The new class must already be registered on chain. + +**Signature:** + +```typescript +export async fastForwardContractUpdate(args: { + instanceAddress: AztecAddress; + newClassId: Fr; + node: AztecNode; +}): Promise<SimulationOverrides> +``` + +**Parameters:** + +- `args`: + + ```typescript + { + /** Address of the deployed instance to upgrade. */ + instanceAddress: AztecAddress; + /** ID of the (already-registered) class to upgrade to. */ + newClassId: Fr; + /** Node used to fetch the existing instance and validate the class is registered. */ + node: AztecNode; + } + ``` + + +**Returns:** + +`Promise<SimulationOverrides>` + +--- + +### `contract/interaction_options.ts` + + +#### FeePaymentMethodOption + +**Type:** Type Alias + +Interactions allow configuring a custom fee payment method that gets bundled with the transaction before sending it to the wallet + +**Signature:** + +```typescript +export type FeePaymentMethodOption = { + paymentMethod?: FeePaymentMethod; +}; +``` + +**Type Members:** + +##### paymentMethod + +Fee payment method to embed in the interaction + +**Type:** `FeePaymentMethod` + + +#### GasSettingsOption + +**Type:** Type Alias + +User-defined partial gas settings for the interaction. This type is completely optional since the wallet will fill in the missing options + +**Signature:** + +```typescript +export type GasSettingsOption = { + gasSettings?: Partial<FieldsOf<GasSettings>>; + congestionEstimate?: ManaUsageEstimate; +}; +``` + +**Type Members:** + +##### gasSettings + +The gas settings + +**Type:** `Partial<FieldsOf<GasSettings>>` + +##### congestionEstimate + +Assumed network congestion level for fee prediction. Controls how aggressively the wallet estimates future fees: None assumes empty blocks, Target assumes steady-state usage, and Limit assumes blocks at maximum capacity. Higher estimates produce higher fee predictions, reducing the risk of underpriced transactions during congestion spikes. Defaults to Limit (worst case) when not specified. + +**Type:** `ManaUsageEstimate` + + +#### InteractionFeeOptions + +**Type:** Type Alias + +Fee options as set by a user. + +**Signature:** + +```typescript +export type InteractionFeeOptions = GasSettingsOption & FeePaymentMethodOption; +``` + +#### RequestInteractionOptions + +**Type:** Type Alias + +Represents the options to configure a request from a contract interaction. Allows specifying additional auth witnesses and capsules to use during execution + +**Signature:** + +```typescript +export type RequestInteractionOptions = { + authWitnesses?: AuthWitness[]; + capsules?: Capsule[]; + fee?: FeePaymentMethodOption; +}; +``` + +**Type Members:** + +##### authWitnesses + +Extra authwits to use during execution + +**Type:** `AuthWitness[]` + +##### capsules + +Extra capsules to use during execution + +**Type:** `Capsule[]` + +##### fee + +Fee payment method to embed in the interaction request + +**Type:** `FeePaymentMethodOption` + + +#### NO_WAIT + +**Type:** Constant + +Constant for explicitly not waiting for transaction confirmation. We use this instead of false to avoid confusion with falsy checks. + +**Value Type:** `any` + + +#### NoWait + +**Type:** Type Alias + +Type for the NO_WAIT constant. + +**Signature:** + +```typescript +export type NoWait = typeof NO_WAIT; +``` + +#### NO_FROM + +**Type:** Constant + +Constant for explicitly opting out of account contract mediation. When used as the `from` parameter, the wallet executes the payload directly via the DefaultEntrypoint without wrapping it in an account contract entrypoint. The app is responsible for assembling the complete execution payload, including any entrypoint wrapping (e.g. multicall) if needed. This will result in the first call of the chain receiving msg_sender as Option::none + +**Value Type:** `any` + + +#### NoFrom + +**Type:** Type Alias + +Type for the NO_FROM constant. + +**Signature:** + +```typescript +export type NoFrom = typeof NO_FROM; +``` + +#### InteractionWaitOptions + +**Type:** Type Alias + +Type for wait options in interactions. - NO_WAIT symbol: Don't wait for confirmation, return TxHash immediately - WaitOpts object: Wait with custom options and return receipt/result - undefined: Wait with default options and return receipt/result + +**Signature:** + +```typescript +export type InteractionWaitOptions = NoWait | WaitOpts | undefined; +``` + +#### SendInteractionOptionsWithoutWait + +**Type:** Type Alias + +Base options for calling a (constrained) function in a contract, without wait parameter. + +**Signature:** + +```typescript +export type SendInteractionOptionsWithoutWait = RequestInteractionOptions & { + from: AztecAddress | NoFrom; + fee?: InteractionFeeOptions; + additionalScopes?: AztecAddress[]; + sendMessagesAs?: AztecAddress; +}; +``` + +**Type Members:** + +##### from + +The sender's Aztec address, or NO_FROM to execute without account contract mediation. + +**Type:** `AztecAddress | NoFrom` + +##### fee + +The fee options for the transaction. + +**Type:** `InteractionFeeOptions` + +##### additionalScopes + +Additional addresses whose private state and keys should be accessible during execution, beyond the sender's. Required when the transaction needs to access private state or keys belonging to an address other than `from`, e.g. withdrawing from an escrow that holds its own private notes. + +**Type:** `AztecAddress[]` + +##### sendMessagesAs + +Overrides the sender address used to derive discovery tags for private messages (notes, events, logs). Recipients use these tags to find messages addressed to them. Defaults to `from`. Typically set when `from === NO_FROM`, since there is no account address to derive tags from. + +**Type:** `AztecAddress` + + +#### SendInteractionOptions + +**Type:** Type Alias + +Represents options for calling a (constrained) function in a contract. + +**Signature:** + +```typescript +export type SendInteractionOptions<W extends InteractionWaitOptions = undefined> = SendInteractionOptionsWithoutWait & { + wait?: W; +}; +``` + +**Type Members:** + +##### wait + +Whether to wait for the transaction to be mined. - undefined (default): wait with default options and return TxReceipt - WaitOpts object: wait with custom options and return TxReceipt - NO_WAIT: return txHash immediately without waiting + +**Type:** `W` + + +#### SimulateInteractionOptions + +**Type:** Type Alias + +Represents the options for simulating a contract function interaction. Allows specifying the address from which the method should be called. Disregarded for simulation of public functions + +**Signature:** + +```typescript +export type SimulateInteractionOptions = Omit<SendInteractionOptions, 'fee'> & { + fee?: InteractionFeeOptions; + skipTxValidation?: boolean; + skipFeeEnforcement?: boolean; + includeMetadata?: boolean; + overrides?: SimulationOverrides; +}; +``` + +**Type Members:** + +##### fee + +The fee options for the transaction. + +**Type:** `InteractionFeeOptions` + +##### skipTxValidation + +Simulate without checking for the validity of the resulting transaction, e.g. whether it emits any existing nullifiers. + +**Type:** `boolean` + +##### skipFeeEnforcement + +Whether to ensure the fee payer is not empty and has enough balance to pay for the fee. + +**Type:** `boolean` + +##### includeMetadata + +Whether to include metadata such as performance statistics (e.g. timing information of the different circuits and oracles) and simulated gas usage in the simulation result, in addition to the return value and offchain effects + +**Type:** `boolean` + +##### overrides + +Pre-simulation overrides applied to the ephemeral fork and contract DB (publicStorage writes, contract instance overrides). + +**Type:** `SimulationOverrides` + + +#### ProfileInteractionOptions + +**Type:** Type Alias + +Represents the options for profiling an interaction. + +**Signature:** + +```typescript +export type ProfileInteractionOptions = SimulateInteractionOptions & { + profileMode: 'gates' | 'execution-steps' | 'full'; + skipProofGeneration?: boolean; +}; +``` + +**Type Members:** + +##### profileMode + +Whether to return gates information or the bytecode/witnesses. + +**Type:** `'gates' | 'execution-steps' | 'full'` + +##### skipProofGeneration + +Whether to generate a Chonk proof or not + +**Type:** `boolean` + + +#### OffchainMessage + +**Type:** Type Alias + +A message emitted during execution or proving, to be delivered offchain. + +**Signature:** + +```typescript +export type OffchainMessage = { + recipient: AztecAddress; + payload: Fr[]; + contractAddress: AztecAddress; + anchorBlockTimestamp: bigint; +}; +``` + +**Type Members:** + +##### recipient + +The intended recipient of the message. + +**Type:** `AztecAddress` + +##### payload + +The message payload (typically encrypted). + +**Type:** `Fr[]` + +##### contractAddress + +The contract that emitted the message. + +**Type:** `AztecAddress` + +##### anchorBlockTimestamp + +Anchor block timestamp at message emission. + +**Type:** `bigint` + + +#### OffchainOutput + +**Type:** Type Alias + +Groups all unproven outputs from private execution that are returned to the client. + +**Signature:** + +```typescript +export type OffchainOutput = { + offchainEffects: OffchainEffect[]; + offchainMessages: OffchainMessage[]; +}; +``` + +**Type Members:** + +##### offchainEffects + +Raw offchain effects emitted during execution. + +**Type:** `OffchainEffect[]` + +##### offchainMessages + +Messages emitted during execution, to be delivered offchain. + +**Type:** `OffchainMessage[]` + + +#### extractOffchainOutput + +**Type:** Function + +Splits an array of offchain effects into decoded offchain messages and remaining effects. Effects whose data starts with `OFFCHAIN_MESSAGE_IDENTIFIER` are parsed as messages and removed from the effects array. + +**Signature:** + +```typescript +export extractOffchainOutput( + effects: OffchainEffect[], + anchorBlockTimestamp: bigint +): OffchainOutput +``` + +**Parameters:** + +- `effects`: `OffchainEffect[]` +- `anchorBlockTimestamp`: `bigint` + +**Returns:** + +`OffchainOutput` + +#### SimulationResult + +**Type:** Type Alias + +Represents the result of a simulation. Always includes the return value and offchain output. When `includeMetadata` is set, also includes stats and the simulated gas usage. + +**Signature:** + +```typescript +export type SimulationResult = { + result: any; + stats?: SimulationStats; + gasUsed?: GasUsed; +} & OffchainOutput; +``` + +**Type Members:** + +##### result + +Return value of the function + +**Type:** `any` + +##### stats + +Additional stats about the simulation. Present when `includeMetadata` is set. + +**Type:** `SimulationStats` + +##### gasUsed + +Raw gas consumed by the simulated transaction. Present when `includeMetadata` is set. Apps that want to declare explicit gas limits should derive their own from this (e.g. pad `totalGas`) and pass them via the fee options; otherwise the wallet fills in the network's per-tx admission limits automatically. + +**Type:** `GasUsed` + + +#### TxSendResultImmediate + +**Type:** Type Alias + +Result of sendTx when not waiting for mining. + +**Signature:** + +```typescript +export type TxSendResultImmediate = { + txHash: TxHash; +} & OffchainOutput; +``` + +**Type Members:** + +##### txHash + +The hash of the sent transaction. + +**Type:** `TxHash` + + +#### TxSendResultMined + +**Type:** Type Alias + +Result of sendTx when waiting for mining. + +**Signature:** + +```typescript +export type TxSendResultMined<TReturn = TxReceipt> = { + receipt: TReturn; +} & OffchainOutput; +``` + +**Type Members:** + +##### receipt + +The transaction receipt. + +**Type:** `TReturn` + + +#### SendReturn + +**Type:** Type Alias + +Represents the result type of sending a transaction. If `wait` is NO_WAIT, returns TxSendResultImmediate. Otherwise returns TxSendResultMined. + +**Signature:** + +```typescript +export type SendReturn<T extends InteractionWaitOptions, TReturn = TxReceipt> = T extends NoWait + ? TxSendResultImmediate + : TxSendResultMined<TReturn>; +``` + +#### toSendOptions + +**Type:** Function + +Transforms and cleans up the higher level SendInteractionOptions defined by the interaction into SendOptions, which are the ones that can be serialized and forwarded to the wallet + +**Signature:** + +```typescript +export toSendOptions<W extends InteractionWaitOptions = undefined>(options: SendInteractionOptions<W>): SendOptions<W> +``` + +**Parameters:** + +- `options`: `SendInteractionOptions<W>` + - The send interaction options with optional wait parameter + +**Returns:** + +`SendOptions<W>` - The send options to forward to the wallet + +#### toSimulateOptions + +**Type:** Function + +Transforms and cleans up the higher level SimulateInteractionOptions defined by the interaction into SimulateOptions, which are the ones that can be serialized and forwarded to the wallet + +**Signature:** + +```typescript +export toSimulateOptions(options: SimulateInteractionOptions): SimulateOptions +``` + +**Parameters:** + +- `options`: `SimulateInteractionOptions` + +**Returns:** + +`SimulateOptions` + +#### toProfileOptions + +**Type:** Function + +Transforms and cleans up the higher level ProfileInteractionOptions defined by the interaction into ProfileOptions, which are the ones that can be serialized and forwarded to the wallet + +**Signature:** + +```typescript +export toProfileOptions(options: ProfileInteractionOptions): ProfileOptions +``` + +**Parameters:** + +- `options`: `ProfileInteractionOptions` + +**Returns:** + +`ProfileOptions` + +--- + +### `contract/wait_for_proven.ts` + + +#### WaitForProvenOpts + +**Type:** Type Alias + +Options for waiting for a transaction to be proven. + +**Signature:** + +```typescript +export type WaitForProvenOpts = { + provenTimeout?: number; + interval?: number; +}; +``` + +**Type Members:** + +##### provenTimeout + +Time to wait for the tx to be proven before timing out + +**Type:** `number` + +##### interval + +Elapsed time between polls to the node + +**Type:** `number` + + +#### DefaultWaitForProvenOpts + +**Type:** Constant + +**Value Type:** `WaitForProvenOpts` + + +#### waitForProven + +**Type:** Function + +Wait for a transaction to be proven by polling the node + +**Signature:** + +```typescript +export async waitForProven( + node: AztecNode, + receipt: TxReceipt, + opts?: WaitForProvenOpts +): Promise<BlockNumber> +``` + +**Parameters:** + +- `node`: `AztecNode` +- `receipt`: `TxReceipt` +- `opts` (optional): `WaitForProvenOpts` + +**Returns:** + +`Promise<BlockNumber>` + +--- + +### `contract/wait_opts.ts` + + +#### WaitOpts + +**Type:** Type Alias + +Options related to waiting for a tx. + +**Signature:** + +```typescript +export type WaitOpts = { + ignoreDroppedReceiptsFor?: number; + timeout?: number; + interval?: number; + dontThrowOnRevert?: boolean; + waitForStatus?: TxStatus; + initialDelay?: number; +}; +``` + +**Type Members:** + +##### ignoreDroppedReceiptsFor + +The amount of time to ignore TxStatus.DROPPED receipts (in seconds) due to the presumption that it is being propagated by the p2p network. Defaults to 5. + +**Type:** `number` + +##### timeout + +The maximum time (in seconds) to wait for the transaction to be mined. Defaults to 300 (5 min). + +**Type:** `number` + +##### interval + +The time interval (in seconds) between retries to fetch the transaction receipt. Defaults to 1. + +**Type:** `number` + +##### dontThrowOnRevert + +Whether to accept a revert as a status code for the tx when waiting for it. If false, will throw if the tx reverts. + +**Type:** `boolean` + +##### waitForStatus + +The minimum inclusion status to wait for. If set, waits until the receipt reaches this status or higher. Defaults to CHECKPOINTED. + +**Type:** `TxStatus` + +##### initialDelay + +The time (in seconds) to wait before the first receipt poll. Defaults to 0. Used to avoid checking for a receipt right after sending a tx, when we know it cannot have been mined yet. Counts against `timeout`. + +**Type:** `number` + + +#### DefaultWaitOpts + +**Type:** Constant + +**Value Type:** `WaitOpts` + + +## Deployment + + +--- + +### `deployment/contract_deployer.ts` + + +#### ContractDeployer + +**Type:** Class + +A class for deploying contract. + + +#### Constructor + +**Signature:** + +```typescript +constructor( + private artifact: ContractArtifact, + private wallet: Wallet, + private constructorName?: string +) +``` + +**Parameters:** + +- `artifact`: `ContractArtifact` +- `wallet`: `Wallet` +- `constructorName` (optional): `string` + +#### Methods + + +##### deploy + +Deploy a contract using the provided instantiation parameters and constructor arguments. Creates a new DeployMethod instance that can be used to send the deployment transaction. The first argument is the DeployInstantiationOptions (salt, deployer) — pass `{}` to accept defaults (random salt, deployer = AztecAddress.ZERO). The remaining arguments are the constructor arguments for the contract. + +**Signature:** + +```typescript +public deploy( + args?: any[], + instantiation?: DeployInstantiationOptions +) +``` + +**Parameters:** + +- `args` (optional): `any[]` + - The constructor arguments for the contract being deployed. +- `instantiation` (optional): `DeployInstantiationOptions` + - Salt and deployer to mix into the address derivation. + +**Returns:** + +`DeployMethod<Contract>` - A DeployMethod instance configured with the ABI, PXE, and constructor arguments. + +--- + +### `deployment/publish_class.ts` + + +#### publishContractClass + +**Type:** Function + +Sets up a call to publish a contract class given its artifact. + +**Signature:** + +```typescript +export async publishContractClass( + wallet: Wallet, + artifact: ContractArtifact +): Promise<ContractFunctionInteraction> +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `artifact`: `ContractArtifact` + +**Returns:** + +`Promise<ContractFunctionInteraction>` + +--- + +### `deployment/publish_instance.ts` + + +#### publishInstance + +**Type:** Function + +Sets up a call to the canonical contract instance registry to publish a contract instance. + +**Signature:** + +```typescript +export publishInstance( + wallet: Wallet, + instance: ContractInstanceWithAddress +): ContractFunctionInteraction +``` + +**Parameters:** + +- `wallet`: `Wallet` + - The wallet to use for the publication (setup) tx. +- `instance`: `ContractInstanceWithAddress` + - The instance to publish. + +**Returns:** + +`ContractFunctionInteraction` + +## Ethereum + + +--- + +### `ethereum/portal_manager.ts` + + +#### L2Claim + +**Type:** Type Alias + +L1 to L2 message info to claim it on L2. + +**Signature:** + +```typescript +export type L2Claim = { + claimSecret: Fr; + claimSecretHash: Fr; + messageHash: Hex; + messageLeafIndex: bigint; +}; +``` + +**Type Members:** + +##### claimSecret + +Secret for claiming. + +**Type:** `Fr` + +##### claimSecretHash + +Hash of the secret for claiming. + +**Type:** `Fr` + +##### messageHash + +Hash of the message. + +**Type:** `Hex` + +##### messageLeafIndex + +Leaf index in the L1 to L2 message tree. + +**Type:** `bigint` + + +#### L2AmountClaim + +**Type:** Type Alias + +L1 to L2 message info that corresponds to an amount to claim. + +**Signature:** + +```typescript +export type L2AmountClaim = L2Claim & { claimAmount: bigint }; +``` + +**Type Members:** + +##### claimAmount + +**Type:** `bigint` + + +#### L2AmountClaimWithRecipient + +**Type:** Type Alias + +L1 to L2 message info that corresponds to an amount to claim with associated recipient. + +**Signature:** + +```typescript +export type L2AmountClaimWithRecipient = L2AmountClaim & { + recipient: AztecAddress; +}; +``` + +**Type Members:** + +##### recipient + +Address that will receive the newly minted notes. + +**Type:** `AztecAddress` + + +#### generateClaimSecret + +**Type:** Function + +Generates a pair secret and secret hash + +**Signature:** + +```typescript +export async generateClaimSecret(logger?: Logger): Promise<[ + Fr, + Fr + ]> +``` + +**Parameters:** + +- `logger` (optional): `Logger` + +**Returns:** + +`Promise<[Fr, Fr]>` + +#### L1TokenManager + +**Type:** Class + +Helper for managing an ERC20 on L1. + + +#### Constructor + +**Signature:** + +```typescript +public constructor( + public readonly tokenAddress: EthAddress, + public readonly handlerAddress: EthAddress | undefined, + private readonly extendedClient: ExtendedViemWalletClient, + private logger: Logger +) +``` + +**Parameters:** + +- `tokenAddress`: `EthAddress` + - Address of the ERC20 contract. +- `handlerAddress`: `EthAddress | undefined` + - Address of the handler/faucet contract. +- `extendedClient`: `ExtendedViemWalletClient` +- `logger`: `Logger` + +#### Methods + + +##### getMintAmount + +Returns the amount of tokens available to mint via the handler. + +**Signature:** + +```typescript +public async getMintAmount() +``` + +**Returns:** + +`Promise<any>` + +##### getL1TokenBalance + +Returns the balance of the given address. + +**Signature:** + +```typescript +public async getL1TokenBalance(address: Hex) +``` + +**Parameters:** + +- `address`: `Hex` + - Address to get the balance of. + +**Returns:** + +`Promise<any>` + +##### mint + +Mints a fixed amount of tokens for the given address. Returns once the tx has been mined. + +**Signature:** + +```typescript +public async mint( + address: Hex, + addressName?: string +) +``` + +**Parameters:** + +- `address`: `Hex` + - Address to mint the tokens for. +- `addressName` (optional): `string` + - Optional name of the address for logging. + +**Returns:** + +`Promise<void>` + +##### approve + +Approves tokens for the given address. Returns once the tx has been mined. + +**Signature:** + +```typescript +public async approve( + amount: bigint, + address: Hex, + addressName = '' +) +``` + +**Parameters:** + +- `amount`: `bigint` + - Amount to approve. +- `address`: `Hex` + - Address to approve the tokens for. +- `addressName` (optional): `any` + - Optional name of the address for logging. + +**Returns:** + +`Promise<void>` + +#### L1FeeJuicePortalManager + +**Type:** Class + +Helper for interacting with the FeeJuicePortal on L1. + + +#### Constructor + +**Signature:** + +```typescript +constructor( + portalAddress: EthAddress, + tokenAddress: EthAddress, + handlerAddress: EthAddress | undefined, + private readonly extendedClient: ExtendedViemWalletClient, + private readonly logger: Logger +) +``` + +**Parameters:** + +- `portalAddress`: `EthAddress` +- `tokenAddress`: `EthAddress` +- `handlerAddress`: `EthAddress | undefined` +- `extendedClient`: `ExtendedViemWalletClient` +- `logger`: `Logger` + +#### Methods + + +##### getTokenManager + +Returns the associated token manager for the L1 ERC20. + +**Signature:** + +```typescript +public getTokenManager() +``` + +**Returns:** + +`L1TokenManager` + +##### bridgeTokensPublic + +Bridges fee juice from L1 to L2 publicly. Handles L1 ERC20 approvals. Returns once the tx has been mined. + +**Signature:** + +```typescript +public async bridgeTokensPublic( + to: AztecAddress, + amount: bigint | undefined, + mint = false +): Promise<L2AmountClaim> +``` + +**Parameters:** + +- `to`: `AztecAddress` + - Address to send the tokens to on L2. +- `amount`: `bigint | undefined` + - Amount of tokens to send. +- `mint` (optional): `any` + - Whether to mint the tokens before sending (only during testing). + +**Returns:** + +`Promise<L2AmountClaim>` + +##### new + +Creates a new instance + +**Signature:** + +```typescript +public static async new( + node: AztecNode, + extendedClient: ExtendedViemWalletClient, + logger: Logger +): Promise<L1FeeJuicePortalManager> +``` + +**Parameters:** + +- `node`: `AztecNode` + - Aztec node client used for retrieving the L1 contract addresses. +- `extendedClient`: `ExtendedViemWalletClient` + - Wallet client, extended with public actions. +- `logger`: `Logger` + - Logger. + +**Returns:** + +`Promise<L1FeeJuicePortalManager>` + +#### L1ToL2TokenPortalManager + +**Type:** Class + +Helper for interacting with a test TokenPortal on L1 for sending tokens to L2. + + +#### Constructor + +**Signature:** + +```typescript +constructor( + portalAddress: EthAddress, + tokenAddress: EthAddress, + handlerAddress: EthAddress | undefined, + protected extendedClient: ExtendedViemWalletClient, + protected logger: Logger +) +``` + +**Parameters:** + +- `portalAddress`: `EthAddress` +- `tokenAddress`: `EthAddress` +- `handlerAddress`: `EthAddress | undefined` +- `extendedClient`: `ExtendedViemWalletClient` +- `logger`: `Logger` + +#### Properties + + +##### portal + +**Type:** `ViemContract<typeof TokenPortalAbi>` + + +##### tokenManager + +**Type:** `L1TokenManager` + + +##### l1TxUtils + +**Type:** `L1TxUtils` + + +#### Methods + + +##### getTokenManager + +Returns the token manager for the underlying L1 token. + +**Signature:** + +```typescript +public getTokenManager() +``` + +**Returns:** + +`L1TokenManager` + +##### bridgeTokensPublic + +Bridges tokens from L1 to L2. Handles token approvals. Returns once the tx has been mined. + +**Signature:** + +```typescript +public async bridgeTokensPublic( + to: AztecAddress, + amount: bigint, + mint = false +): Promise<L2AmountClaim> +``` + +**Parameters:** + +- `to`: `AztecAddress` + - Address to send the tokens to on L2. +- `amount`: `bigint` + - Amount of tokens to send. +- `mint` (optional): `any` + - Whether to mint the tokens before sending (only during testing). + +**Returns:** + +`Promise<L2AmountClaim>` + +##### bridgeTokensPrivate + +Bridges tokens from L1 to L2 privately. Handles token approvals. Returns once the tx has been mined. + +**Signature:** + +```typescript +public async bridgeTokensPrivate( + to: AztecAddress, + amount: bigint, + mint = false +): Promise<L2AmountClaimWithRecipient> +``` + +**Parameters:** + +- `to`: `AztecAddress` + - Address to send the tokens to on L2. +- `amount`: `bigint` + - Amount of tokens to send. +- `mint` (optional): `any` + - Whether to mint the tokens before sending (only during testing). + +**Returns:** + +`Promise<L2AmountClaimWithRecipient>` + +#### L1TokenPortalManager + +**Type:** Class + +Helper for interacting with a test TokenPortal on L1 for both withdrawing from and bridging to L2. + +**Extends:** `L1ToL2TokenPortalManager` + + +#### Constructor + +**Signature:** + +```typescript +constructor( + portalAddress: EthAddress, + tokenAddress: EthAddress, + handlerAddress: EthAddress | undefined, + outboxAddress: EthAddress, + extendedClient: ExtendedViemWalletClient, + logger: Logger +) +``` + +**Parameters:** + +- `portalAddress`: `EthAddress` +- `tokenAddress`: `EthAddress` +- `handlerAddress`: `EthAddress | undefined` +- `outboxAddress`: `EthAddress` +- `extendedClient`: `ExtendedViemWalletClient` +- `logger`: `Logger` + +#### Methods + + +##### withdrawFunds + +Withdraws funds from the portal by consuming an L2 to L1 message. Returns once the tx is mined on L1. + +**Signature:** + +```typescript +public async withdrawFunds( + amount: bigint, + recipient: EthAddress, + epochNumber: EpochNumber, + numCheckpointsInEpoch: number, + messageIndex: bigint, + siblingPath: SiblingPath<number> +) +``` + +**Parameters:** + +- `amount`: `bigint` + - Amount to withdraw. +- `recipient`: `EthAddress` + - Who will receive the funds. +- `epochNumber`: `EpochNumber` + - Epoch number of the message. +- `numCheckpointsInEpoch`: `number` + - The partial-proof depth (1-indexed) the witness was built against. +- `messageIndex`: `bigint` + - Index of the message. +- `siblingPath`: `SiblingPath<number>` + - Sibling path of the message. + +**Returns:** + +`Promise<void>` + +##### getL2ToL1MessageLeaf + +Computes the L2 to L1 message leaf for the given parameters. + +**Signature:** + +```typescript +public async getL2ToL1MessageLeaf( + amount: bigint, + recipient: EthAddress, + l2Bridge: AztecAddress, + callerOnL1: EthAddress = EthAddress.ZERO +): Promise<Fr> +``` + +**Parameters:** + +- `amount`: `bigint` + - Amount to bridge. +- `recipient`: `EthAddress` + - Recipient on L1. +- `l2Bridge`: `AztecAddress` + - Address of the L2 bridge. +- `callerOnL1` (optional): `EthAddress` + - Caller address on L1. + +**Returns:** + +`Promise<Fr>` + +## Fee + + +--- + +### `fee/fee_juice_payment_method_with_claim.ts` + + +#### FeeJuicePaymentMethodWithClaim + +**Type:** Class + +Pay fee directly with Fee Juice claimed in the same tx. Claiming consumes an L1 to L2 message that "contains" the fee juice bridged from L1. + +**Implements:** `FeePaymentMethod` + + +#### Constructor + +**Signature:** + +```typescript +constructor( + private sender: AztecAddress, + private claim: Pick<L2AmountClaim, 'claimAmount' | 'claimSecret' | 'messageLeafIndex'> +) +``` + +**Parameters:** + +- `sender`: `AztecAddress` +- `claim`: `Pick<L2AmountClaim, 'claimAmount' | 'claimSecret' | 'messageLeafIndex'>` + +#### Methods + + +##### getExecutionPayload + +Creates an execution payload to pay the fee in Fee Juice. + +**Signature:** + +```typescript +async getExecutionPayload(): Promise<ExecutionPayload> +``` + +**Returns:** + +`Promise<ExecutionPayload>` - An execution payload that just contains the `claim_and_end_setup` function call. + +##### getAsset + +**Signature:** + +```typescript +getAsset(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getFeePayer + +**Signature:** + +```typescript +getFeePayer(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getGasSettings + +**Signature:** + +```typescript +getGasSettings(): GasSettings | undefined +``` + +**Returns:** + +`GasSettings | undefined` + +--- + +### `fee/fee_payment_method.ts` + + +#### FeePaymentMethod + +**Type:** Interface + +Holds information about how the fee for a transaction is to be paid. + + +#### Methods + + +##### getAsset + +The asset used to pay the fee. + +**Signature:** + +```typescript +getAsset(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getExecutionPayload + +Returns the data to be added to the final execution request to pay the fee in the given asset + +**Signature:** + +```typescript +getExecutionPayload(): Promise<ExecutionPayload> +``` + +**Returns:** + +`Promise<ExecutionPayload>` - The function calls to pay the fee. + +##### getFeePayer + +The expected fee payer for this tx. + +**Signature:** + +```typescript +getFeePayer(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getGasSettings + +The gas settings (if any) used to compute the execution payload of the payment method + +**Signature:** + +```typescript +getGasSettings(): GasSettings | undefined +``` + +**Returns:** + +`GasSettings | undefined` + +--- + +### `fee/private_fee_payment_method.ts` + + +#### PrivateFeePaymentMethod + +**Type:** Class + +**Deprecated:** Is not supported on mainnet. Use FeeJuicePaymentMethodWithClaim or `SponsoredFeePaymentMethod` instead. + +Holds information about how the fee for a transaction is to be paid. + +**Implements:** `FeePaymentMethod` + + +#### Constructor + +**Signature:** + +```typescript +constructor( + private paymentContract: AztecAddress, + private sender: AztecAddress, + private wallet: Wallet, + protected gasSettings: GasSettings, + private setMaxFeeToOne = false +) +``` + +**Parameters:** + +- `paymentContract`: `AztecAddress` + - Address which will hold the fee payment. +- `sender`: `AztecAddress` + - Address of the account that will pay the fee +- `wallet`: `Wallet` + - A wallet to perform the simulation to get the accepted asset +- `gasSettings`: `GasSettings` + - Gas settings used to compute the maximum fee the user is willing to pay +- `setMaxFeeToOne` (optional): `any` + - If true, the max fee will be set to 1. TODO(#7694): Remove this param once the lacking feature in TXE is implemented. + +#### Methods + + +##### getAsset + +The asset used to pay the fee. + +**Signature:** + +```typescript +async getAsset(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` - The asset used to pay the fee. + +##### getFeePayer + +**Signature:** + +```typescript +getFeePayer(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getExecutionPayload + +Creates an execution payload to pay the fee using a private function through an FPC in the desired asset + +**Signature:** + +```typescript +async getExecutionPayload(): Promise<ExecutionPayload> +``` + +**Returns:** + +`Promise<ExecutionPayload>` - An execution payload that contains the required function calls and auth witnesses. + +##### getGasSettings + +**Signature:** + +```typescript +getGasSettings(): GasSettings | undefined +``` + +**Returns:** + +`GasSettings | undefined` + +--- + +### `fee/public_fee_payment_method.ts` + + +#### PublicFeePaymentMethod + +**Type:** Class + +**Deprecated:** Is not supported on mainnet. Use FeeJuicePaymentMethodWithClaim or `SponsoredFeePaymentMethod` instead. + +Holds information about how the fee for a transaction is to be paid. + +**Implements:** `FeePaymentMethod` + + +#### Constructor + +**Signature:** + +```typescript +constructor( + protected paymentContract: AztecAddress, + protected sender: AztecAddress, + protected wallet: Wallet, + protected gasSettings: GasSettings +) +``` + +**Parameters:** + +- `paymentContract`: `AztecAddress` + - Address which will hold the fee payment. +- `sender`: `AztecAddress` + - An auth witness provider to authorize fee payments +- `wallet`: `Wallet` + - A wallet to perform the simulation to get the accepted asset +- `gasSettings`: `GasSettings` + - Gas settings used to compute the maximum fee the user is willing to pay + +#### Methods + + +##### getAsset + +The asset used to pay the fee. + +**Signature:** + +```typescript +async getAsset(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` - The asset used to pay the fee. + +##### getFeePayer + +**Signature:** + +```typescript +getFeePayer(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getExecutionPayload + +Creates an execution payload to pay the fee using a public function through an FPC in the desired asset + +**Signature:** + +```typescript +async getExecutionPayload(): Promise<ExecutionPayload> +``` + +**Returns:** + +`Promise<ExecutionPayload>` - An execution payload that contains the required function calls. + +##### getGasSettings + +**Signature:** + +```typescript +getGasSettings(): GasSettings | undefined +``` + +**Returns:** + +`GasSettings | undefined` + +--- + +### `fee/sponsored_fee_payment.ts` + + +#### SponsoredFeePaymentMethod + +**Type:** Class + +A fee payment method that uses a contract that blindly sponsors transactions. This contract is expected to be prefunded in testing environments. + +**Implements:** `FeePaymentMethod` + + +#### Constructor + +**Signature:** + +```typescript +constructor(private paymentContract: AztecAddress) +``` + +**Parameters:** + +- `paymentContract`: `AztecAddress` + +#### Methods + + +##### getAsset + +**Signature:** + +```typescript +getAsset(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getFeePayer + +**Signature:** + +```typescript +getFeePayer(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getExecutionPayload + +**Signature:** + +```typescript +async getExecutionPayload(): Promise<ExecutionPayload> +``` + +**Returns:** + +`Promise<ExecutionPayload>` + +##### getGasSettings + +**Signature:** + +```typescript +getGasSettings(): GasSettings | undefined +``` + +**Returns:** + +`GasSettings | undefined` + +## Utils + + +--- + +### `utils/abi_types.ts` + + +#### FieldLike + +**Type:** Type Alias + +Any type that can be converted into a field for a contract call. + +**Signature:** + +```typescript +export type FieldLike = Fr | Buffer | bigint | number | { toField: () => Fr }; +``` + +#### EthAddressLike + +**Type:** Type Alias + +Any type that can be converted into an EthAddress Aztec.nr struct. + +**Signature:** + +```typescript +export type EthAddressLike = { address: FieldLike } | EthAddress; +``` + +#### AztecAddressLike + +**Type:** Type Alias + +Any type that can be converted into an AztecAddress Aztec.nr struct. + +**Signature:** + +```typescript +export type AztecAddressLike = { address: FieldLike } | AztecAddress; +``` + +#### FunctionSelectorLike + +**Type:** Type Alias + +Any type that can be converted into a FunctionSelector Aztec.nr struct. + +**Signature:** + +```typescript +export type FunctionSelectorLike = FieldLike | FunctionSelector; +``` + +#### EventSelectorLike + +**Type:** Type Alias + +Any type that can be converted into an EventSelector Aztec.nr struct. + +**Signature:** + +```typescript +export type EventSelectorLike = FieldLike | EventSelector; +``` + +#### U128Like + +**Type:** Type Alias + +Any type that can be converted into a U128. + +**Signature:** + +```typescript +export type U128Like = bigint | number; +``` + +#### WrappedFieldLike + +**Type:** Type Alias + +Any type that can be converted into a struct with a single `inner` field. + +**Signature:** + +```typescript +export type WrappedFieldLike = { inner: FieldLike } | FieldLike; +``` + +#### OptionLike + +**Type:** Type Alias + +Noir `Option<T>` lowered ABI shape, plus ergonomic direct `T | null | undefined` inputs. + +**Signature:** + +```typescript +export type OptionLike<T> = + | T + | null + | undefined + | { _is_some: boolean; _value: T }; +``` + +--- + +### `utils/authwit.ts` + + +#### IntentInnerHash + +**Type:** Type Alias + +Intent with an inner hash + +**Signature:** + +```typescript +export type IntentInnerHash = { + consumer: AztecAddress; + innerHash: Fr; +}; +``` + +**Type Members:** + +##### consumer + +The consumer + +**Type:** `AztecAddress` + +##### innerHash + +The action to approve + +**Type:** `Fr` + + +#### CallIntent + +**Type:** Type Alias + +Intent with a call + +**Signature:** + +```typescript +export type CallIntent = { + caller: AztecAddress; + call: FunctionCall; +}; +``` + +**Type Members:** + +##### caller + +The caller to approve + +**Type:** `AztecAddress` + +##### call + +The call to approve + +**Type:** `FunctionCall` + + +#### ContractFunctionInteractionCallIntent + +**Type:** Type Alias + +Intent with a ContractFunctionInteraction + +**Signature:** + +```typescript +export type ContractFunctionInteractionCallIntent = { + caller: AztecAddress; + action: ContractFunctionInteraction; +}; +``` + +**Type Members:** + +##### caller + +The caller to approve + +**Type:** `AztecAddress` + +##### action + +The action to approve + +**Type:** `ContractFunctionInteraction` + + +#### isContractFunctionInteractionCallIntent + +**Type:** Function + +Identifies ContractFunctionInteractionCallIntents + +**Signature:** + +```typescript +export isContractFunctionInteractionCallIntent(messageHashOrIntent: Fr | IntentInnerHash | CallIntent | ContractFunctionInteractionCallIntent): messageHashOrIntent is ContractFunctionInteractionCallIntent +``` + +**Parameters:** + +- `messageHashOrIntent`: `Fr | IntentInnerHash | CallIntent | ContractFunctionInteractionCallIntent` + +**Returns:** + +`messageHashOrIntent is ContractFunctionInteractionCallIntent` + +#### computeAuthWitMessageHash + +**Type:** Constant + +Compute an authentication witness message hash from an intent and metadata If using the `IntentInnerHash`, the consumer is the address that can "consume" the authwit, for token approvals it is the token contract itself. The `innerHash` itself will be the message that a contract is allowed to execute. At the point of "approval checking", the validating contract (account for private and registry for public) will be computing the message hash (`H(consumer, chainid, version, inner_hash)`) where the all but the `inner_hash` is injected from the context (consumer = msg_sender), and use it for the authentication check. Therefore, any allowed `innerHash` will therefore also have information around where it can be spent (version, chainId) and who can spend it (consumer). If using the `CallIntent`, the caller is the address that is making the call, for a token approval from Alice to Bob, this would be Bob. The action is then used along with the `caller` to compute the `innerHash` and the consumer. + +**Value Type:** `any` + + +#### getMessageHashFromIntent + +**Type:** Function + +Compute an authentication witness message hash from an intent and metadata. This is just a wrapper around computeAuthwitMessageHash that allows receiving an already computed messageHash as input + +**Signature:** + +```typescript +export async getMessageHashFromIntent( + messageHashOrIntent: Fr | IntentInnerHash | CallIntent | ContractFunctionInteractionCallIntent, + chainInfo: ChainInfo +) +``` + +**Parameters:** + +- `messageHashOrIntent`: `Fr | IntentInnerHash | CallIntent | ContractFunctionInteractionCallIntent` + - The precomputed messageHash or intent to approve (consumer and innerHash or caller and call/action) +- `chainInfo`: `ChainInfo` + +**Returns:** + +`Promise<Fr>` - The message hash for the intent + +#### computeInnerAuthWitHashFromAction + +**Type:** Constant + +Computes the inner authwitness hash for either a function call or an action, for it to later be combined with the metadata required for the outer hash and eventually the full AuthWitness. + +**Value Type:** `any` + + +#### lookupValidity + +**Type:** Function + +Lookup the validity of an authwit in private and public contexts. Uses the chain id and version of the wallet. + +**Signature:** + +```typescript +export async lookupValidity( + wallet: Wallet, + onBehalfOf: AztecAddress, + intent: IntentInnerHash | CallIntent | ContractFunctionInteractionCallIntent, + witness: AuthWitness +): Promise<{ + isValidInPrivate: boolean; + isValidInPublic: boolean; + }> +``` + +**Parameters:** + +- `wallet`: `Wallet` + - The wallet use to simulate and read the public data +- `onBehalfOf`: `AztecAddress` + - The address of the "approver" +- `intent`: `IntentInnerHash | CallIntent | ContractFunctionInteractionCallIntent` + - The consumer and inner hash or the caller and action to lookup +- `witness`: `AuthWitness` + - The computed authentication witness to check + +**Returns:** + +```typescript +Promise<{ + /** boolean flag indicating if the authwit is valid in private context */ + isValidInPrivate: boolean; + /** boolean flag indicating if the authwit is valid in public context */ + isValidInPublic: boolean; +}> +``` + +A struct containing the validity of the authwit in private and public contexts. + +#### SetPublicAuthwitContractInteraction + +**Type:** Class + +Convenience class designed to wrap the very common interaction of setting a public authwit in the AuthRegistry contract + +**Extends:** `ContractFunctionInteraction` + + +#### Constructor + +**Signature:** + +```typescript +private constructor( + wallet: Wallet, + private from: AztecAddress, + messageHash: Fr, + authorized: boolean +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `from`: `AztecAddress` +- `messageHash`: `Fr` +- `authorized`: `boolean` + +#### Methods + + +##### create + +**Signature:** + +```typescript +static async create( + wallet: Wallet, + from: AztecAddress, + messageHashOrIntent: Fr | IntentInnerHash | CallIntent | ContractFunctionInteractionCallIntent, + authorized: boolean +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `from`: `AztecAddress` +- `messageHashOrIntent`: `Fr | IntentInnerHash | CallIntent | ContractFunctionInteractionCallIntent` +- `authorized`: `boolean` + +**Returns:** + +`Promise<SetPublicAuthwitContractInteraction>` + +##### simulate + +Overrides the simulate method, adding the sender of the authwit (authorizer) as from and preventing misuse + +**Signature:** + +```typescript +public override simulate(options: Omit<SimulateInteractionOptions, 'from'> = {} as Omit<SimulateInteractionOptions, 'from'>): Promise<SimulationResult> +``` + +**Parameters:** + +- `options` (optional): `Omit<SimulateInteractionOptions, 'from'>` + - An optional object containing additional configuration for the transaction. + +**Returns:** + +`Promise<SimulationResult>` - The result of the transaction as returned by the contract function. + +##### profile + +Overrides the profile method, adding the sender of the authwit (authorizer) as from and preventing misuse + +**Signature:** + +```typescript +public override profile(options: Omit<ProfileInteractionOptions, 'from'> = { profileMode: 'gates' }): Promise<TxProfileResult> +``` + +**Parameters:** + +- `options` (optional): `Omit<ProfileInteractionOptions, 'from'>` + - Same options as `simulate`, plus profiling method + +**Returns:** + +`Promise<TxProfileResult>` - An object containing the function return value and profile result. + +##### send + +Overrides the send method, adding the sender of the authwit (authorizer) as from and preventing misuse + +**Signature:** + +```typescript +public override send(options?: Omit<SendInteractionOptionsWithoutWait, 'from'>): Promise<TxSendResultMined> +``` + +**Parameters:** + +- `options` (optional): `Omit<SendInteractionOptionsWithoutWait, 'from'>` + - An optional object containing 'fee' options information + +**Returns:** + +`Promise<TxSendResultMined>` - A TxReceipt (if wait is true/undefined) or TxHash (if wait is false) + +##### send + +**Signature:** + +```typescript +public override send<W extends InteractionWaitOptions>(options?: Omit<SendInteractionOptions<W>, 'from'>): Promise<SendReturn<W>> +``` + +**Parameters:** + +- `options` (optional): `Omit<SendInteractionOptions<W>, 'from'>` + +**Returns:** + +`Promise<SendReturn<W>>` + +##### send + +**Signature:** + +```typescript +public override send(options?: Omit<SendInteractionOptions<InteractionWaitOptions>, 'from'>): Promise<SendReturn<InteractionWaitOptions>> +``` + +**Parameters:** + +- `options` (optional): `Omit<SendInteractionOptions<InteractionWaitOptions>, 'from'>` + +**Returns:** + +`Promise<SendReturn<InteractionWaitOptions>>` + +--- + +### `utils/cross_chain.ts` + + +#### waitForL1ToL2MessageReady + +**Type:** Function + +Waits for the L1 to L2 message to be ready to be consumed, that is, for a block that already exists to have inserted it into the message tree at the given tip. This is a check on the chain as it stands, not a promise about a future block: a node simulating public calls may optimistically include messages that are still only in the Inbox, and the block that eventually inserts them can stop short of the one being waited for. + +**Signature:** + +```typescript +export waitForL1ToL2MessageReady( + node: Pick<AztecNode, 'getBlockData' | 'getL1ToL2MessageIndex'>, + l1ToL2MessageHash: Fr, + opts: { + timeoutSeconds: number; + chainTip?: BlockTag; +} +): Promise<boolean> +``` + +**Parameters:** + +- `node`: `Pick<AztecNode, 'getBlockData' | 'getL1ToL2MessageIndex'>` + - Aztec node instance used to obtain the information about the message +- `l1ToL2MessageHash`: `Fr` + - Hash of the L1 to L2 message +- `opts`: + - Options + + ```typescript + { + /** Timeout for the operation in seconds */ timeoutSeconds: number; + /** + * Chain tip to evaluate readiness against. Defaults to `'latest'`. Set this to the tip the consuming PXE syncs to + * (e.g. `'proven'`) so readiness answers whether the message is present at the same block the transaction + * simulation will anchor to, not at a newer tip. + */ + chainTip?: BlockTag; + } + ``` + + +**Returns:** + +`Promise<boolean>` + +#### isL1ToL2MessageReady + +**Type:** Function + +Returns whether the L1 to L2 message is ready to be consumed: whether a block at `chainTip` has already inserted it into the message tree. A message that is in the Inbox but not yet in any block is not ready, even though a node may already simulate public calls against it; use this rather than a successful simulation when a caller needs to know the message is really there. + +**Signature:** + +```typescript +export async isL1ToL2MessageReady( + node: Pick<AztecNode, 'getBlockData' | 'getL1ToL2MessageIndex'>, + l1ToL2MessageHash: Fr, + chainTip: BlockTag = 'latest' +): Promise<boolean> +``` + +**Parameters:** + +- `node`: `Pick<AztecNode, 'getBlockData' | 'getL1ToL2MessageIndex'>` + - Aztec node instance used to obtain the information about the message +- `l1ToL2MessageHash`: `Fr` + - Hash of the L1 to L2 message +- `chainTip` (optional): `BlockTag` + - Chain tip to evaluate readiness against. Defaults to `'latest'`. Pass the tip the consuming PXE syncs to (e.g. `'proven'`) so readiness is checked at the block the transaction simulation will anchor to. + +**Returns:** + +`Promise<boolean>` - True if the message is ready to be consumed, false otherwise + +--- + +### `utils/fee_juice.ts` + + +#### getFeeJuiceBalance + +**Type:** Function + +Returns the owner's fee juice balance. Note: This is used only e2e_local_network_example test. TODO: Consider nuking. + +**Signature:** + +```typescript +export async getFeeJuiceBalance( + owner: AztecAddress, + node: AztecNode +): Promise<bigint> +``` + +**Parameters:** + +- `owner`: `AztecAddress` +- `node`: `AztecNode` + +**Returns:** + +`Promise<bigint>` + +--- + +### `utils/field_compressed_string.ts` + + +#### readFieldCompressedString + +**Type:** Constant + +This turns + +**Value Type:** `any` + + +--- + +### `utils/node.ts` + + +#### waitForNode + +**Type:** Constant + +Waits for an Aztec node to become reachable, polling AztecNode.getNodeInfo until it succeeds. + +**Value Type:** `any` + + +#### waitForTx + +**Type:** Function + +Waits for a transaction to be mined and returns its receipt. + +**Signature:** + +```typescript +export async waitForTx( + node: AztecNode, + txHash: TxHash, + opts?: WaitOpts +): Promise<TxReceipt> +``` + +**Parameters:** + +- `node`: `AztecNode` + - The Aztec node to query for transaction status +- `txHash`: `TxHash` + - The hash of the transaction to wait for +- `opts` (optional): `WaitOpts` + - Optional configuration for waiting behavior + +**Returns:** + +`Promise<TxReceipt>` - The transaction receipt + +#### createAztecNodeClient + +**Type:** Constant + +This is re-exported from `@aztec-labs/stdlib/interfaces/client`. See the source module for full documentation. + +**Value Type:** `Re-export` + + +#### AztecNode + +**Type:** Type Alias + +This is a type re-exported from `@aztec-labs/stdlib/interfaces/client`. See the source module for full type definition and documentation. + +**Signature:** + +```typescript +export type { AztecNode } from '@aztec-labs/stdlib/interfaces/client' +``` + +#### AztecNodeClientOptions + +**Type:** Type Alias + +This is a type re-exported from `@aztec-labs/stdlib/interfaces/client`. See the source module for full type definition and documentation. + +**Signature:** + +```typescript +export type { AztecNodeClientOptions } from '@aztec-labs/stdlib/interfaces/client' +``` + +--- + +### `utils/pub_key.ts` + + +#### generatePublicKey + +**Type:** Function + +Method for generating a public grumpkin key from a private key. + +**Signature:** + +```typescript +export generatePublicKey(privateKey: GrumpkinScalar): Promise<PublicKey> +``` + +**Parameters:** + +- `privateKey`: `GrumpkinScalar` + - The private key. + +**Returns:** + +`Promise<PublicKey>` - The generated public key. + +## Wallet + + +--- + +### `wallet/account_entrypoint_meta_payment_method.ts` + + +#### AccountEntrypointMetaPaymentMethod + +**Type:** Class + +Fee payment method that allows an account contract to pay for its own deployment It works by rerouting the provided fee payment method through the account's entrypoint, which sets itself as fee payer. If no payment method is provided, it is assumed the account will pay with its own fee juice balance. Usually, in order to pay fees it is necessary to obtain an ExecutionPayload that encodes the necessary information that is sent to the user's account entrypoint, that has plumbing to handle it. If there's no account contract yet (it's being deployed) a MultiCallContract is used, which doesn't have a concept of fees or how to handle this payload. HOWEVER, the account contract's entrypoint does, so this method reshapes that fee payload into a call to the account contract entrypoint being deployed with the original fee payload. This class can be seen in action in DeployAccountMethod.ts#getSelfPaymentMethod + +**Implements:** `FeePaymentMethod` + + +#### Constructor + +**Signature:** + +```typescript +constructor( + private account: Account, + private chainInfo: ChainInfo, + private paymentMethod?: FeePaymentMethod, + private feeEntrypointOptions?: any +) +``` + +**Parameters:** + +- `account`: `Account` +- `chainInfo`: `ChainInfo` +- `paymentMethod` (optional): `FeePaymentMethod` +- `feeEntrypointOptions` (optional): `any` + +#### Methods + + +##### getAsset + +**Signature:** + +```typescript +getAsset(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getExecutionPayload + +**Signature:** + +```typescript +async getExecutionPayload(): Promise<ExecutionPayload> +``` + +**Returns:** + +`Promise<ExecutionPayload>` + +##### getFeePayer + +**Signature:** + +```typescript +getFeePayer(): Promise<AztecAddress> +``` + +**Returns:** + +`Promise<AztecAddress>` + +##### getGasSettings + +**Signature:** + +```typescript +getGasSettings(): GasSettings | undefined +``` + +**Returns:** + +`GasSettings | undefined` + +--- + +### `wallet/account_manager.ts` + + +#### AccountManagerCreateOptions + +**Type:** Interface + +Optional overrides passed to AccountManager.create. + + +#### Properties + + +##### salt + +Contract instantiation salt. Defaults to a random `Fr`. + +**Type:** `Salt` + + +##### immutablesHash + +Commitment to the contract's immutable storage values. Folded into the salted initialization hash, so a non-zero value affects the derived address. Defaults to `Fr.ZERO`. + +**Type:** `Fr` + + +##### deployer + +Address recorded as the instance deployer. Defaults to `AztecAddress.ZERO`. + +**Type:** `AztecAddress` + + +#### AccountManager + +**Type:** Class + +Manages a user account. Provides methods for calculating the account's address and other related data, plus a helper to return a preconfigured deploy method. + + +#### Constructor + +**Signature:** + +```typescript +private constructor( + private wallet: Wallet, + private secretKey: Fr, + private accountContract: AccountContract, + private instance: ContractInstanceWithAddress +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `secretKey`: `Fr` +- `accountContract`: `AccountContract` +- `instance`: `ContractInstanceWithAddress` + +#### Methods + + +##### create + +**Signature:** + +```typescript +static async create( + wallet: Wallet, + secretKey: Fr, + accountContract: AccountContract, + opts?: AccountManagerCreateOptions +) +``` + +**Parameters:** + +- `wallet`: `Wallet` +- `secretKey`: `Fr` +- `accountContract`: `AccountContract` +- `opts` (optional): `AccountManagerCreateOptions` + +**Returns:** + +`Promise<AccountManager>` + +##### getPublicKeys + +**Signature:** + +```typescript +protected getPublicKeys(): PublicKeys +``` + +**Returns:** + +`PublicKeys` + +##### getPublicKeysHash + +**Signature:** + +```typescript +protected getPublicKeysHash(): Promise<Fr> +``` + +**Returns:** + +`Promise<Fr>` + +##### getCompleteAddress + +Gets the calculated complete address associated with this account. Does not require the account to have been published for public execution. + +**Signature:** + +```typescript +public getCompleteAddress(): Promise<CompleteAddress> +``` + +**Returns:** + +`Promise<CompleteAddress>` - The address, partial address, and encryption public key. + +##### getSecretKey + +Returns the secret key used to derive the rest of the privacy keys for this contract + +**Signature:** + +```typescript +public getSecretKey() +``` + +**Returns:** + +`Fr` + +##### getInstance + +Returns the contract instance definition associated with this account. Does not require the account to have been published for public execution. + +**Signature:** + +```typescript +public getInstance(): ContractInstanceWithAddress +``` + +**Returns:** + +`ContractInstanceWithAddress` - ContractInstance instance. + +##### getAccount + +Returns the account (the transaction signer) backed by this account contract. Use it to build and authorize transactions from this account. + +**Signature:** + +```typescript +public async getAccount(): Promise<Account> +``` + +**Returns:** + +`Promise<Account>` + +##### getAccountContract + +Returns the account contract that backs this account. + +**Signature:** + +```typescript +getAccountContract(): AccountContract +``` + +**Returns:** + +`AccountContract` - The account contract + +##### getDeployMethod + +Returns a preconfigured deploy method that contains all the necessary function calls to deploy the account contract. + +**Signature:** + +```typescript +public async getDeployMethod(): Promise<DeployAccountMethod> +``` + +**Returns:** + +`Promise<DeployAccountMethod>` + +##### hasInitializer + +Returns whether this account contract has an initializer function. + +**Signature:** + +```typescript +public async hasInitializer() +``` + +**Returns:** + +`Promise<boolean>` + +#### Getters + + +##### address (getter) + +**Signature:** + +```typescript +get address(): AztecAddress { +``` + +**Returns:** + +`AztecAddress` + +--- + +### `wallet/capabilities.ts` + + +#### CAPABILITY_VERSION + +**Type:** Constant + +Current capability manifest version. + +**Value Type:** `any` + + +#### ContractFunctionPattern + +**Type:** Interface + +Pattern for matching contract functions with wildcards. Used in simulation and transaction capabilities to specify which contract functions are allowed. + + +#### Properties + + +##### contract + +Contract address or '*' for any contract + +**Type:** `AztecAddress | '*'` + + +##### function + +Function name or '*' for any function + +**Type:** `string` + + +##### additionalScopes + +Additional addresses whose private state and keys are accessible when calling this function, beyond the sender's. - undefined: No additional scopes allowed - AztecAddress[]: Only these specific addresses allowed as additional scopes - '*': All known address allowed as an additional scope + +**Type:** `AztecAddress[] | '*'` + + +#### AccountsCapability + +**Type:** Interface + +Account access capability - grants access to user accounts. Maps to wallet methods: - getAccounts (when canGet: true) - createAuthWit (when canCreateAuthWit: true) The wallet decides which accounts to reveal to the app. Apps don't specify which accounts they want - they just request the capability and the wallet shows them the available accounts. + + +#### Properties + + +##### type + +Discriminator for capability type + +**Type:** `'accounts'` + + +##### canGet + +Can get accounts from wallet. Maps to: getAccounts + +**Type:** `boolean` + + +##### canCreateAuthWit + +Can create auth witnesses for accounts. Maps to: createAuthWit + +**Type:** `boolean` + + +#### GrantedAccountsCapability + +**Type:** Interface + +Granted account access capability. Extends the request with specific accounts that were granted by the wallet. + +**Extends:** `AccountsCapability` + + +#### Properties + + +##### accounts + +Specific accounts granted by the wallet with their aliases. The wallet adds this when granting the capability. + +**Type:** `Aliased<AztecAddress>[]` + + +#### ContractsCapability + +**Type:** Interface + +Contract interaction capability - for registering and querying contracts. Maps to wallet methods: - registerContract (when canRegister: true) - getContractMetadata (when canGetMetadata: true) Matching is done by contract address, not class ID. This allows updating existing contracts with new artifacts (e.g., when contract is upgraded to a new contractClassId onchain). Note: For querying contract class metadata, use ContractClassesCapability instead. + + +#### Properties + + +##### type + +Discriminator for capability type + +**Type:** `'contracts'` + + +##### contracts + +Which contracts this applies to: - '*': Any contract address - AztecAddress[]: Specific contract addresses + +**Type:** `'*' | AztecAddress[]` + + +##### canRegister + +Can register contracts and update existing registrations. Maps to: registerContract When true, allows: - Registering new contract instances at specified addresses - Re-registering existing contracts with updated artifacts (e.g., after upgrade) + +**Type:** `boolean` + + +##### canGetMetadata + +Can query contract metadata. Maps to: getContractMetadata + +**Type:** `boolean` + + +#### GrantedContractsCapability + +**Type:** Interface + +Granted contract interaction capability. The wallet may reduce the scope (e.g., from '*' to specific addresses). + +**Extends:** `ContractsCapability` + + +#### ContractClassesCapability + +**Type:** Interface + +Contract class capability - for querying contract class metadata and registering contract classes. Maps to wallet methods: - getContractClassMetadata (when canGetMetadata: true) - registerContractClass (when canRegister: true) Contract classes are identified by their class ID (Fr), not by contract address. Multiple contract instances can share the same class. This capability grants permission to query metadata for, and register, specific contract classes. Apps typically acquire this permission automatically when registering a contract with an artifact (the wallet auto-grants permission for that contract's class ID). + + +#### Properties + + +##### type + +Discriminator for capability type + +**Type:** `'contractClasses'` + + +##### classes + +Which contract classes this applies to: - '*': Any contract class ID - Fr[]: Specific contract class IDs + +**Type:** `'*' | Fr[]` + + +##### canRegister + +Can register a contract class artifact in the local PXE. Maps to: registerContractClass + +**Type:** `boolean` + + +##### canGetMetadata + +Can query contract class metadata. Maps to: getContractClassMetadata + +**Type:** `boolean` + + +#### GrantedContractClassesCapability + +**Type:** Interface + +Granted contract class capability. The wallet may reduce the scope (e.g., from '*' to specific class IDs). + +**Extends:** `ContractClassesCapability` + + +#### SimulationCapability + +**Type:** Interface + +Transaction simulation capability - for simulating transactions and executing utilities. Maps to wallet methods: - simulateTx (when transactions scope specified) - executeUtility (when utilities scope specified) - profileTx (when transactions scope specified) + + +#### Properties + + +##### type + +Discriminator for capability type + +**Type:** `'simulation'` + + +##### transactions + +Transaction simulation scope. Maps to: simulateTx, profileTx + +**Type:** + +```typescript +{ + /** + * Which contracts/functions to allow: + * - '*': Any transaction + * - ContractFunctionPattern[]: Specific contract functions + */ + scope: '*' | ContractFunctionPattern[]; + } +``` + + +##### utilities + +Utility execution scope (unconstrained calls). Maps to: executeUtility + +**Type:** + +```typescript +{ + /** + * Which contracts/functions to allow: + * - '*': Any utility call + * - ContractFunctionPattern[]: Specific contract functions + */ + scope: '*' | ContractFunctionPattern[]; + } +``` + + +#### GrantedSimulationCapability + +**Type:** Interface + +Granted transaction simulation capability. The wallet may reduce the scope (e.g., from '*' to specific patterns). + +**Extends:** `SimulationCapability` + + +#### TransactionCapability + +**Type:** Interface + +Transaction execution capability - for sending transactions. Maps to wallet methods: - sendTx Policy enforcement (rate limits, spending limits) should be handled at the contract level in Aztec, not at the wallet level. + + +#### Properties + + +##### type + +Discriminator for capability type + +**Type:** `'transaction'` + + +##### scope + +Which contracts/functions to allow: - '*': Any transaction - ContractFunctionPattern[]: Specific patterns + +**Type:** `'*' | ContractFunctionPattern[]` + + +#### GrantedTransactionCapability + +**Type:** Interface + +Granted transaction execution capability. The wallet may reduce the scope (e.g., from '*' to specific patterns). + +**Extends:** `TransactionCapability` + + +#### DataCapability + +**Type:** Interface + +Data access capability - for querying private data. Maps to wallet methods: - getAddressBook (when addressBook: true) - getPrivateEvents (when privateEvents specified) + + +#### Properties + + +##### type + +Discriminator for capability type + +**Type:** `'data'` + + +##### addressBook + +Access to address book. Maps to: getAddressBook + +**Type:** `boolean` + + +##### privateEvents + +Access to private events. Maps to: getPrivateEvents + +**Type:** + +```typescript +{ + /** + * Which contracts to allow event queries from: + * - '*': Any contract + * - AztecAddress[]: Specific contracts + */ + contracts: '*' | AztecAddress[]; + } +``` + + +#### GrantedDataCapability + +**Type:** Interface + +Granted data access capability. The wallet may reduce the scope (e.g., from '*' to specific contracts). + +**Extends:** `DataCapability` + + +#### Capability + +**Type:** Type Alias + +Union type of all capability scopes (app request). Capabilities group wallet operations by their security sensitivity and functional cohesion, making permission requests understandable to users. + +**Signature:** + +```typescript +export type Capability = + | AccountsCapability + | ContractsCapability + | ContractClassesCapability + | SimulationCapability + | TransactionCapability + | DataCapability; +``` + +#### GrantedCapability + +**Type:** Type Alias + +Union type of all granted capabilities (wallet response). The wallet may augment capabilities with additional information: - AccountsCapability: adds specific accounts granted - Other capabilities: may reduce scope (e.g., '*' to specific addresses) + +**Signature:** + +```typescript +export type GrantedCapability = + | GrantedAccountsCapability + | GrantedContractsCapability + | GrantedContractClassesCapability + | GrantedSimulationCapability + | GrantedTransactionCapability + | GrantedDataCapability; +``` + +#### AppCapabilities + +**Type:** Interface + +Application capability manifest. Sent by dApp to declare all operations it needs. This reduces authorization friction from multiple dialogs to a single comprehensive permission request. + + +#### Properties + + +##### version + +Manifest version for forward compatibility. Currently only '1.0' is supported. + +**Type:** `typeof CAPABILITY_VERSION` + + +##### metadata + +Application metadata for display in authorization dialogs. + +**Type:** + +```typescript +{ + /** Human-readable app name */ + name: string; + + /** App version */ + version: string; + + /** Optional description of what the app does */ + description?: string; + + /** Optional website URL */ + url?: string; + + /** Optional icon URL or data URI */ + icon?: string; + } +``` + + +##### capabilities + +Requested capabilities grouped by scope. + +**Type:** `Capability[]` + + +#### WalletCapabilities + +**Type:** Interface + +Wallet capability response. Returned by wallet after user reviews and approves/denies the capability request. The wallet can modify requested capabilities: - Reduce scope (e.g., restrict to specific contracts instead of '*') - Add information (e.g., specify which accounts are granted) - Deny capabilities (by omitting them from the `granted` array) + + +#### Properties + + +##### version + +Response version for forward compatibility. + +**Type:** `typeof CAPABILITY_VERSION` + + +##### granted + +Capabilities granted by the wallet. Capabilities not in this array were implicitly denied. Empty array means the user denied all capabilities. + +**Type:** `GrantedCapability[]` + + +##### wallet + +Wallet implementation details. + +**Type:** + +```typescript +{ + /** Wallet name/implementation */ + name: string; + + /** Wallet version */ + version: string; + } +``` + + +--- + +### `wallet/deploy_account_method.ts` + + +#### DeployAccountFeePaymentMethodOption + +**Type:** Type Alias + +Extended fee payment method option for account deployments that includes entrypoint wrapping options + +**Signature:** + +```typescript +export type DeployAccountFeePaymentMethodOption = FeePaymentMethodOption & { + feeEntrypointOptions?: unknown; +}; +``` + +**Type Members:** + +##### feeEntrypointOptions + +Optional entrypoint-specific options for wrapping execution payloads + +**Type:** `unknown` + + +#### RequestDeployAccountOptions + +**Type:** Type Alias + +The configuration options for the request method. + +**Signature:** + +```typescript +export type RequestDeployAccountOptions = Omit<RequestDeployOptions, 'fee'> & { + fee?: DeployAccountFeePaymentMethodOption; + from?: AztecAddress | NoFrom; +}; +``` + +**Type Members:** + +##### fee + +Fee options specific to account deployment + +**Type:** `DeployAccountFeePaymentMethodOption` + +##### from + +Sender of the request. When NO_FROM, the to-be-deployed account pays for its own deployment (self-paid deploy) and the payload is wrapped through the multicall entrypoint. + +**Type:** `AztecAddress | NoFrom` + + +#### DeployAccountOptions + +**Type:** Type Alias + +The configuration options for the send/prove methods. + +**Signature:** + +```typescript +export type DeployAccountOptions<W extends InteractionWaitOptions = undefined> = DeployOptionsWithoutWait & { + wait?: W; +}; +``` + +**Type Members:** + +##### wait + +Whether to wait for the transaction to be mined. - undefined (default): wait with default options and return TxReceipt - WaitOpts object: wait with custom options and return TxReceipt - false: return txHash immediately without waiting + +**Type:** `W` + + +#### SimulateDeployAccountOptions + +**Type:** Type Alias + +The configuration options for the simulate method. + +**Signature:** + +```typescript +export type SimulateDeployAccountOptions = SimulateDeployOptions; +``` + +#### DeployAccountMethod + +**Type:** Class + +Modified version of the DeployMethod used to deploy account contracts. Supports deploying contracts that can pay for their own fee, plus some preconfigured options to avoid errors. + +**Extends:** `UniversalDeployMethod` + + +#### Constructor + +**Signature:** + +```typescript +constructor( + publicKeys: PublicKeys, + wallet: Wallet, + artifact: ContractArtifact, + postDeployCtor: (instance: ContractInstanceWithAddress, wallet: Wallet) => TContract, + salt: Fr, + immutablesHash: Fr, + private account: Account, + args: any[] = [], + constructorNameOrArtifact?: string | FunctionArtifact, + authWitnesses: AuthWitness[] = [], + capsules: Capsule[] = [], + extraHashedArgs: HashedValues[] = [] +) +``` + +**Parameters:** + +- `publicKeys`: `PublicKeys` +- `wallet`: `Wallet` +- `artifact`: `ContractArtifact` +- `postDeployCtor`: `(instance: ContractInstanceWithAddress, wallet: Wallet) => TContract` +- `salt`: `Fr` +- `immutablesHash`: `Fr` +- `account`: `Account` +- `args` (optional): `any[]` +- `constructorNameOrArtifact` (optional): `string | FunctionArtifact` +- `authWitnesses` (optional): `AuthWitness[]` +- `capsules` (optional): `Capsule[]` +- `extraHashedArgs` (optional): `HashedValues[]` + +#### Methods + + +##### request + +Returns the execution payload that allows this operation to happen on chain. For self-deployments (from === NO_FROM), the payload is wrapped through the multicall entrypoint on the app side so the wallet can execute it directly. + +**Signature:** + +```typescript +public override async request(opts?: RequestDeployAccountOptions): Promise<ExecutionPayload> +``` + +**Parameters:** + +- `opts` (optional): `RequestDeployAccountOptions` + - Configuration options. + +**Returns:** + +`Promise<ExecutionPayload>` - The execution payload for this operation + +##### convertDeployOptionsToSendOptions + +**Signature:** + +```typescript +protected override convertDeployOptionsToSendOptions<W extends InteractionWaitOptions>(options: DeployOptions<W>): SendOptions<W> +``` + +**Parameters:** + +- `options`: `DeployOptions<W>` + +**Returns:** + +`SendOptions<W>` + +##### convertDeployOptionsToSimulateOptions + +**Signature:** + +```typescript +protected override convertDeployOptionsToSimulateOptions(options: SimulateDeployOptions): SimulateOptions +``` + +**Parameters:** + +- `options`: `SimulateDeployOptions` + +**Returns:** + +`SimulateOptions` + +##### convertDeployOptionsToProfileOptions + +**Signature:** + +```typescript +protected override convertDeployOptionsToProfileOptions(options: DeployOptionsWithoutWait & ProfileInteractionOptions): ProfileOptions +``` + +**Parameters:** + +- `options`: `DeployOptionsWithoutWait & ProfileInteractionOptions` + +**Returns:** + +`ProfileOptions` + +##### with + +Augments this DeployAccountMethod with additional metadata, such as authWitnesses and capsules. + +**Signature:** + +```typescript +public override with({ authWitnesses = [], capsules = [], extraHashedArgs = [], }: { + authWitnesses?: AuthWitness[]; + capsules?: Capsule[]; + extraHashedArgs?: HashedValues[]; +}): DeployAccountMethod<TContract> +``` + +**Parameters:** + +- `{ authWitnesses = [], capsules = [], extraHashedArgs = [], }`: + + ```typescript + { + /** The authWitnesses to add to the deployment */ + authWitnesses?: AuthWitness[]; + /** The capsules to add to the deployment */ + capsules?: Capsule[]; + /** The extra hashed args to add to the deployment */ + extraHashedArgs?: HashedValues[]; + } + ``` + + +**Returns:** + +`DeployAccountMethod<TContract>` - A new DeployAccountMethod with the added metadata + +--- + +### `wallet/tx_simulation_result_with_app_offset.ts` + + +#### TxSimulationResultWithAppOffset + +**Type:** Class + +Extends TxSimulationResult with the app call offset, which tracks where the app's calls begin in the flattened array of calls. Tracking of app call offset is a wallet-level concern: the wallet may wrap the app payload in an entrypoint or may prepend calls (this is typically done for fee payments). + +**Extends:** `TxSimulationResult` + + +#### Constructor + +**Signature:** + +```typescript +constructor( + privateExecutionResult: PrivateExecutionResult, + publicInputs: PrivateKernelTailCircuitPublicInputs, + publicOutput?: PublicSimulationOutput, + stats?: SimulationStats, + public readonly appCallOffset: number | undefined = undefined +) +``` + +**Parameters:** + +- `privateExecutionResult`: `PrivateExecutionResult` +- `publicInputs`: `PrivateKernelTailCircuitPublicInputs` +- `publicOutput` (optional): `PublicSimulationOutput` +- `stats` (optional): `SimulationStats` +- `appCallOffset` (optional): `number | undefined` + - Index of the app's first call in a flattened array of calls. 0 = app call is the root execution itself (DefaultEntrypoint / NO_FROM). 1..N = wallet prepended calls before the app call. undefined = wallet did not send the field; use heuristic fallback. + +#### Methods + + +##### getPrivateReturnValuesOfAppCall + +Returns the private return values that correspond to the provided app call. + +**Signature:** + +```typescript +getPrivateReturnValuesOfAppCall(appCallIndex: number = 0): NestedProcessReturnValues | undefined +``` + +**Parameters:** + +- `appCallIndex` (optional): `number` + - Index of the app call within the app calls. + +**Returns:** + +`NestedProcessReturnValues | undefined` + +##### fromResultAndOffset + +Creates a TxSimulationResultWithAppOffset from an existing TxSimulationResult, attaching the app call offset computed by the wallet (i.e. how many calls precede the first app call in the flattened execution tree). + +**Signature:** + +```typescript +static fromResultAndOffset( + result: TxSimulationResult, + appCallOffset: number +): TxSimulationResultWithAppOffset +``` + +**Parameters:** + +- `result`: `TxSimulationResult` + - The simulation result to wrap. +- `appCallOffset`: `number` + - The index of the app's first call in the flattened execution tree. + +**Returns:** + +`TxSimulationResultWithAppOffset` + +##### random + +**Signature:** + +```typescript +static override async random() +``` + +**Returns:** + +`Promise<TxSimulationResultWithAppOffset>` + +#### Getters + + +##### schema (getter) + +**Signature:** + +```typescript +static override get schema(): ZodFor<TxSimulationResultWithAppOffset> { +``` + +**Returns:** + +`ZodFor<TxSimulationResultWithAppOffset>` + +--- + +### `wallet/wallet.ts` + + +#### Aliased + +**Type:** Type Alias + +A wrapper type that allows any item to be associated with an alias. + +**Signature:** + +```typescript +export type Aliased<T> = { + alias: string; + item: T; +}; +``` + +**Type Members:** + +##### alias + +The alias + +**Type:** `string` + +##### item + +The item being aliased. + +**Type:** `T` + + +#### SimulateOptions + +**Type:** Type Alias + +Options for simulating interactions with the wallet. Overrides the fee settings of an interaction with a simplified version that only hints at the wallet whether the interaction contains a fee payment method or not + +**Signature:** + +```typescript +export type SimulateOptions = Omit<SimulateInteractionOptions, 'fee'> & { + fee?: GasSettingsOption; +}; +``` + +**Type Members:** + +##### fee + +The fee options + +**Type:** `GasSettingsOption` + + +#### ProfileOptions + +**Type:** Type Alias + +Options for profiling interactions with the wallet. Overrides the fee settings of an interaction with a simplified version that only hints at the wallet whether the interaction contains a fee payment method or not + +**Signature:** + +```typescript +export type ProfileOptions = Omit<ProfileInteractionOptions, 'fee'> & { + fee?: GasSettingsOption; +}; +``` + +**Type Members:** + +##### fee + +The fee options + +**Type:** `GasSettingsOption` + + +#### SendOptions + +**Type:** Type Alias + +Options for sending/proving interactions with the wallet. Overrides the fee settings of an interaction with a simplified version that only hints at the wallet whether the interaction contains a fee payment method or not + +**Signature:** + +```typescript +export type SendOptions<W extends InteractionWaitOptions = undefined> = Omit< + SendInteractionOptionsWithoutWait, + 'fee' +> & { + fee?: GasSettingsOption; + wait?: W; +}; +``` + +**Type Members:** + +##### fee + +The fee options + +**Type:** `GasSettingsOption` + +##### wait + +Whether to wait for the transaction to be mined + +**Type:** `W` + + +#### BatchableMethods + +**Type:** Type Alias + +Helper type that represents all methods that can be batched (all methods except batch itself). + +**Signature:** + +```typescript +export type BatchableMethods = Omit<Wallet, 'batch'>; +``` + +#### BatchedMethod + +**Type:** Type Alias + +Union of all possible batched method calls. This ensures type safety: the `args` must match the specific `name`. + +**Signature:** + +```typescript +export type BatchedMethod = { + [K in keyof BatchableMethods]: BatchedMethodInternal<K>; +}[keyof BatchableMethods]; +``` + +#### BatchedMethodResult + +**Type:** Type Alias + +Helper type to extract the return type of a batched method + +**Signature:** + +```typescript +export type BatchedMethodResult<T> = + T extends BatchedMethodInternal<infer K> ? Awaited<ReturnType<BatchableMethods[K]>> : never; +``` + +#### BatchedMethodResultWrapper + +**Type:** Type Alias + +Wrapper type for batch results that includes the method name for discriminated union deserialization. Each result is wrapped as \{ name: 'methodName', result: ActualResult \} to allow proper deserialization when AztecAddress and TxHash would otherwise be ambiguous (both are hex strings). + +**Signature:** + +```typescript +export type BatchedMethodResultWrapper<T extends BatchedMethod> = { + name: T['name']; + result: BatchedMethodResult<T>; +}; +``` + +**Type Members:** + +##### name + +The method name + +**Type:** `T['name']` + +##### result + +The method result + +**Type:** `BatchedMethodResult<T>` + + +#### BatchResults + +**Type:** Type Alias + +Maps a tuple of BatchedMethod to a tuple of their wrapped return types + +**Signature:** + +```typescript +export type BatchResults<T extends readonly BatchedMethod[]> = { + [K in keyof T]: BatchedMethodResultWrapper<T[K]>; +}; +``` + +**Type Members:** + +##### [K in keyof T] + +**Signature:** `[K in keyof T]: BatchedMethodResultWrapper<T[K]>` + +**Key Type:** `keyof T` + +**Value Type:** `BatchedMethodResultWrapper<T[K]>` + + +#### EventFilterBase + +**Type:** Type Alias + +Base filter options for event queries. + +**Signature:** + +```typescript +export type EventFilterBase = { + txHash?: TxHash; + fromBlock?: BlockNumber; + toBlock?: BlockNumber; +}; +``` + +**Type Members:** + +##### txHash + +Transaction in which the events were emitted. + +**Type:** `TxHash` + +##### fromBlock + +The block number from which to start fetching events (inclusive). Optional. If provided, it must be greater or equal than 1. Defaults to the initial L2 block number (INITIAL_L2_BLOCK_NUM). + +**Type:** `BlockNumber` + +##### toBlock + +The block number until which to fetch logs (not inclusive). Optional. If provided, it must be greater than fromBlock. + +**Type:** `BlockNumber` + + +#### PrivateEventFilter + +**Type:** Type Alias + +Filter options when querying private events. + +**Signature:** + +```typescript +export type PrivateEventFilter = EventFilterBase & { + contractAddress: AztecAddress; + scopes: AztecAddress[]; +}; +``` + +**Type Members:** + +##### contractAddress + +The address of the contract that emitted the events. + +**Type:** `AztecAddress` + +##### scopes + +Addresses of accounts that are in scope for this filter. + +**Type:** `AztecAddress[]` + + +#### PublicEventFilter + +**Type:** Type Alias + +Filter options when querying public events. The contract address is required because the public log index is keyed on `(contract, tag)`; tag-only queries are not supported. + +**Signature:** + +```typescript +export type PublicEventFilter = EventFilterBase & { + contractAddress: AztecAddress; + afterEvent?: EventCursor; +}; +``` + +**Type Members:** + +##### contractAddress + +The address of the contract that emitted the events. Required. + +**Type:** `AztecAddress` + +##### afterEvent + +Cursor to resume strictly after, for pagination. Pass GetPublicEventsResult.nextCursor from a previous page here to fetch the next one. Omit to start from the beginning of the range. + +**Type:** `EventCursor` + + +#### Event + +**Type:** Type Alias + +An ABI decoded event with associated metadata. + +**Signature:** + +```typescript +export type Event<T, M extends object = object> = { + event: T; + metadata: InTx & M; +}; +``` + +**Type Members:** + +##### event + +The ABI decoded event + +**Type:** `T` + +##### metadata + +Metadata describing event context information such as tx and block + +**Type:** `InTx & M` + + +#### PrivateEvent + +**Type:** Type Alias + +An ABI decoded private event with associated metadata. + +**Signature:** + +```typescript +export type PrivateEvent<T> = Event<T>; +``` + +#### PublicEvent + +**Type:** Type Alias + +An ABI decoded public event with associated metadata (includes contract address). + +**Signature:** + +```typescript +export type PublicEvent<T> = Event< + T, + { + contractAddress: AztecAddress; + } +>; +``` + +#### ContractMetadata + +**Type:** Type Alias + +Contract metadata including deployment and registration status. + +**Signature:** + +```typescript +export type ContractMetadata = { + instance?: ContractInstancePreimageWithAddress; + initializationStatus: ContractInitializationStatus; + isContractPublished: boolean; + isContractUpdated: boolean; + updatedContractClassId?: Fr | undefined; +}; +``` + +**Type Members:** + +##### instance + +The contract instance preimage and address. + +**Type:** `ContractInstancePreimageWithAddress` + +##### initializationStatus + +Whether the contract has been initialized. + +**Type:** `ContractInitializationStatus` + +##### isContractPublished + +Whether the contract instance is publicly deployed onchain + +**Type:** `boolean` + +##### isContractUpdated + +Whether the contract has been updated to a different class + +**Type:** `boolean` + +##### updatedContractClassId + +The updated contract class ID if the contract has been updated + +**Type:** `Fr | undefined` + + +#### ContractClassMetadata + +**Type:** Type Alias + +Contract class metadata. + +**Signature:** + +```typescript +export type ContractClassMetadata = { + isArtifactRegistered: boolean; + isContractClassPubliclyRegistered: boolean; +}; +``` + +**Type Members:** + +##### isArtifactRegistered + +Whether the artifact is registered in the wallet + +**Type:** `boolean` + +##### isContractClassPubliclyRegistered + +Whether the contract class is publicly registered onchain + +**Type:** `boolean` + + +#### ExecuteUtilityOptions + +**Type:** Type Alias + +Options for executing a utility function call. + +**Signature:** + +```typescript +export type ExecuteUtilityOptions = { + scopes: AztecAddress[]; + authWitnesses?: AuthWitness[]; +}; +``` + +**Type Members:** + +##### scopes + +The scopes for the utility execution (determines which notes and keys are visible). + +**Type:** `AztecAddress[]` + +##### authWitnesses + +Optional auth witnesses to use during execution. + +**Type:** `AuthWitness[]` + + +#### Wallet + +**Type:** Type Alias + +The wallet interface. + +**Signature:** + +```typescript +export type Wallet = { + getPrivateEvents<T>( + eventMetadata: EventMetadataDefinition, + eventFilter: PrivateEventFilter, + ): Promise<PrivateEvent<T>[]>; + getChainInfo(): Promise<ChainInfo>; + getContractMetadata(address: AztecAddress): Promise<ContractMetadata>; + getContractClassMetadata(id: Fr): Promise<ContractClassMetadata>; + registerSender(address: AztecAddress, alias?: string): Promise<AztecAddress>; + getAddressBook(): Promise<Aliased<AztecAddress>[]>; + getAccounts(): Promise<Aliased<AztecAddress>[]>; + registerContract( + instance: ContractInstancePreimage, + artifact?: ContractArtifact, + secretKeyOrKeys?: Fr | MasterSecretKeys, + ): Promise<void>; + registerContractClass(artifact: ContractArtifact): Promise<void>; + simulateTx(exec: ExecutionPayload, opts: SimulateOptions): Promise<TxSimulationResultWithAppOffset>; + executeUtility(call: FunctionCall, opts: ExecuteUtilityOptions): Promise<UtilityExecutionResult>; + profileTx(exec: ExecutionPayload, opts: ProfileOptions): Promise<TxProfileResult>; + sendTx<W extends InteractionWaitOptions = undefined>( + exec: ExecutionPayload, + opts: SendOptions<W>, + ): Promise<SendReturn<W>>; + createAuthWit(from: AztecAddress, messageHashOrIntent: IntentInnerHash | CallIntent): Promise<AuthWitness>; + requestCapabilities(manifest: AppCapabilities): Promise<WalletCapabilities>; + batch<const T extends readonly BatchedMethod[]>(methods: T): Promise<BatchResults<T>>; +}; +``` + +**Type Members:** + +##### getPrivateEvents + +**Signature:** + +```typescript +getPrivateEvents<T>( + eventMetadata: EventMetadataDefinition, + eventFilter: PrivateEventFilter +): Promise<PrivateEvent<T>[]> +``` + +**Parameters:** + +- `eventMetadata`: `EventMetadataDefinition` +- `eventFilter`: `PrivateEventFilter` + +**Returns:** + +`Promise<PrivateEvent<T>[]>` +##### getChainInfo + +**Signature:** + +```typescript +getChainInfo(): Promise<ChainInfo> +``` + +**Returns:** + +`Promise<ChainInfo>` +##### getContractMetadata + +**Signature:** + +```typescript +getContractMetadata(address: AztecAddress): Promise<ContractMetadata> +``` + +**Parameters:** + +- `address`: `AztecAddress` + +**Returns:** + +`Promise<ContractMetadata>` +##### getContractClassMetadata + +**Signature:** + +```typescript +getContractClassMetadata(id: Fr): Promise<ContractClassMetadata> +``` + +**Parameters:** + +- `id`: `Fr` + +**Returns:** + +`Promise<ContractClassMetadata>` +##### registerSender + +**Signature:** + +```typescript +registerSender( + address: AztecAddress, + alias?: string +): Promise<AztecAddress> +``` + +**Parameters:** + +- `address`: `AztecAddress` +- `alias` (optional): `string` + +**Returns:** + +`Promise<AztecAddress>` +##### getAddressBook + +**Signature:** + +```typescript +getAddressBook(): Promise<Aliased<AztecAddress>[]> +``` + +**Returns:** + +`Promise<Aliased<AztecAddress>[]>` +##### getAccounts + +**Signature:** + +```typescript +getAccounts(): Promise<Aliased<AztecAddress>[]> +``` + +**Returns:** + +`Promise<Aliased<AztecAddress>[]>` +##### registerContract + +**Signature:** + +```typescript +registerContract( + instance: ContractInstancePreimage, + artifact?: ContractArtifact, + secretKeyOrKeys?: Fr | MasterSecretKeys +): Promise<void> +``` + +**Parameters:** + +- `instance`: `ContractInstancePreimage` +- `artifact` (optional): `ContractArtifact` +- `secretKeyOrKeys` (optional): `Fr | MasterSecretKeys` + +**Returns:** + +`Promise<void>` +##### registerContractClass + +Registers a contract class artifact in the local PXE without binding it to any instance. Useful for simulation flows that need the artifact available locally before any onchain upgrade has taken effect. No chain check. + +**Signature:** + +```typescript +registerContractClass(artifact: ContractArtifact): Promise<void> +``` + +**Parameters:** + +- `artifact`: `ContractArtifact` + +**Returns:** + +`Promise<void>` +##### simulateTx + +**Signature:** + +```typescript +simulateTx( + exec: ExecutionPayload, + opts: SimulateOptions +): Promise<TxSimulationResultWithAppOffset> +``` + +**Parameters:** + +- `exec`: `ExecutionPayload` +- `opts`: `SimulateOptions` + +**Returns:** + +`Promise<TxSimulationResultWithAppOffset>` +##### executeUtility + +**Signature:** + +```typescript +executeUtility( + call: FunctionCall, + opts: ExecuteUtilityOptions +): Promise<UtilityExecutionResult> +``` + +**Parameters:** + +- `call`: `FunctionCall` +- `opts`: `ExecuteUtilityOptions` + +**Returns:** + +`Promise<UtilityExecutionResult>` +##### profileTx + +**Signature:** + +```typescript +profileTx( + exec: ExecutionPayload, + opts: ProfileOptions +): Promise<TxProfileResult> +``` + +**Parameters:** + +- `exec`: `ExecutionPayload` +- `opts`: `ProfileOptions` + +**Returns:** + +`Promise<TxProfileResult>` +##### sendTx + +**Signature:** + +```typescript +sendTx<W extends InteractionWaitOptions = undefined>( + exec: ExecutionPayload, + opts: SendOptions<W> +): Promise<SendReturn<W>> +``` + +**Parameters:** + +- `exec`: `ExecutionPayload` +- `opts`: `SendOptions<W>` + +**Returns:** + +`Promise<SendReturn<W>>` +##### createAuthWit + +**Signature:** + +```typescript +createAuthWit( + from: AztecAddress, + messageHashOrIntent: IntentInnerHash | CallIntent +): Promise<AuthWitness> +``` + +**Parameters:** + +- `from`: `AztecAddress` +- `messageHashOrIntent`: `IntentInnerHash | CallIntent` + +**Returns:** + +`Promise<AuthWitness>` +##### requestCapabilities + +**Signature:** + +```typescript +requestCapabilities(manifest: AppCapabilities): Promise<WalletCapabilities> +``` + +**Parameters:** + +- `manifest`: `AppCapabilities` + +**Returns:** + +`Promise<WalletCapabilities>` +##### batch + +**Signature:** + +```typescript +batch<const T extends readonly BatchedMethod[]>(methods: T): Promise<BatchResults<T>> +``` + +**Parameters:** + +- `methods`: `T` + +**Returns:** + +`Promise<BatchResults<T>>` + +#### ExecutionPayloadSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### GasSettingsOptionSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### WaitOptsSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### SendOptionsSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### SimulateOptionsSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### ProfileOptionsSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### MessageHashOrIntentSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### EventMetadataDefinitionSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### PrivateEventFilterSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### PublicEventFilterSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### PrivateEventSchema + +**Type:** Constant + +**Value Type:** `z.ZodType<any>` + + +#### PublicEventSchema + +**Type:** Constant + +**Value Type:** `z.ZodType<PublicEvent<AbiDecoded>>` + + +#### ContractMetadataSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### ContractClassMetadataSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### ContractFunctionPatternSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### AccountsCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### GrantedAccountsCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### ContractsCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### GrantedContractsCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### ContractClassesCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### GrantedContractClassesCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### SimulationCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### GrantedSimulationCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### TransactionCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### GrantedTransactionCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### DataCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### GrantedDataCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### CapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### GrantedCapabilitySchema + +**Type:** Constant + +**Value Type:** `any` + + +#### AppCapabilitiesSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### WalletCapabilitiesSchema + +**Type:** Constant + +**Value Type:** `any` + + +#### BatchedMethodSchema + +**Type:** Constant + + +#### BatchedResultSchema + +**Type:** Constant + + +#### WalletSchema + +**Type:** Constant + +**Value Type:** `ApiSchemaFor<Wallet>` diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_connect_to_local_network.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_connect_to_local_network.md new file mode 100644 index 00000000000..4bee18dfe57 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_connect_to_local_network.md @@ -0,0 +1,61 @@ +--- +title: Connect to Local Network +tags: [local_network, connection, wallet] +sidebar_position: 1 +description: Connect your application to the Aztec local network and interact with accounts. +references: ["docs/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/wallets/src/embedded/*", "labs/yarn-project/accounts/src/testing/*"] +--- + +This guide shows you how to connect your application to the Aztec local network and interact with the network. + +## Prerequisites + +- Running Aztec local network (see [Quickstart](../../getting_started_on_local_network.md)) on port 8080 +- Node.js installed +- TypeScript project set up + +## Install dependencies + +```bash +yarn add @aztec-labs/aztec.js@#include_version_without_prefix @aztec-labs/accounts@#include_version_without_prefix @aztec-labs/wallets@#include_version_without_prefix +``` + +## Connect to the network + +Create a node client and EmbeddedWallet to interact with the local network: + +#include_code connect_to_network /docs/examples/ts/aztecjs_connection/index.ts typescript + +:::note About EmbeddedWallet +`EmbeddedWallet` is a simplified wallet for local development that implements the same `Wallet` interface used in production. It handles key management, transaction signing, and proof generation in-process without external dependencies. + +**Why use it for testing?** It starts instantly, requires no setup, and provides deterministic behavior—ideal for automated tests and rapid iteration. + +**Production wallets** (like browser extensions or mobile apps) implement the same interface but store keys securely, may require user confirmation for transactions, and typically run in a separate process. Code written against `EmbeddedWallet` works with any `Wallet` implementation, so your application logic transfers directly to production. +::: + +### Verify the connection + +Get node information to confirm your connection: + +#include_code verify_connection /docs/examples/ts/aztecjs_connection/index.ts typescript + +### Load pre-funded accounts + +The local network has accounts pre-funded with fee juice to pay for gas. Register them in your wallet: + +#include_code load_accounts /docs/examples/ts/aztecjs_connection/index.ts typescript + +These accounts are pre-funded with fee juice (the native gas token) at genesis, so you can immediately send transactions without needing to bridge funds from L1. + +### Check fee juice balance + +Verify that an account has fee juice for transactions: + +#include_code check_fee_juice /docs/examples/ts/aztecjs_connection/index.ts typescript + +## Next steps + +- [Create an account](./how_to_create_account.md) - Deploy new accounts on the network +- [Deploy a contract](./how_to_deploy_contract.md) - Deploy your smart contracts +- [Send transactions](./how_to_send_transaction.md) - Execute contract functions diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_create_account.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_create_account.md new file mode 100644 index 00000000000..b8aab5ee545 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_create_account.md @@ -0,0 +1,75 @@ +--- +title: Creating Accounts +tags: [accounts] +sidebar_position: 2 +description: Step-by-step guide to creating and deploying new user accounts in Aztec.js applications. +references: ["docs/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/accounts/src/schnorr/*", "labs/yarn-project/wallets/src/embedded/*", "labs/yarn-project/aztec.js/src/fee/fee_juice_payment_method_with_claim.ts"] +--- + +This guide shows you how to create and deploy a new account on Aztec. + +## Prerequisites + +- [Connected to a network](./how_to_connect_to_local_network.md) with a `EmbeddedWallet` instance +- Understanding of [account concepts](../foundational-topics/accounts/index.md) + +## Install dependencies + +```bash +yarn add @aztec-labs/aztec.js@#include_version_without_prefix @aztec-labs/wallets@#include_version_without_prefix @aztec-labs/noir-contracts.js@#include_version_without_prefix +``` + +## Create a new account + +Using the [`wallet` from the connection guide](./how_to_connect_to_local_network.md), call `createSchnorrAccount` to create a new account with a random secret, salt, and signing key: + +#include_code create_account /docs/examples/ts/aztecjs_connection/index.ts typescript + +The secret derives the account's encryption keys, the signing key authenticates its transactions, and the salt ensures address uniqueness. The signing key is provided independently and is not derived from the secret: it is an ownership key, so keep it separate from the encryption secret that your PXE holds. + +:::warning Store your secret, salt, and signing key +Save the `secret`, `salt`, and `signingKey` values securely. You need all three to recover access to your account. If you lose them, you will permanently lose access to the account and any assets it holds. +::: + +## Deploy the account + +New accounts must be deployed before they can send transactions. Deployment requires paying fees. + +### Using the Sponsored FPC + +If your account doesn't have Fee Juice, use the [Sponsored FPC](./how_to_pay_fees.md#sponsored-fpc): + +#include_code deploy_account_sponsored_fpc /docs/examples/ts/aztecjs_connection/index.ts typescript + +:::info +See the [guide on fees](./how_to_pay_fees.md#sponsored-fpc) for more details on the Sponsored FPC and what this snippet means. +::: + +### Using Fee Juice + +If your account has Fee Juice from a [bridge from L1](./how_to_pay_fees.md#bridge-fee-juice-from-l1), you can claim it and deploy in one step using `FeeJuicePaymentMethodWithClaim`. + +Create a new Schnorr account for this path: + +#include_code create_fee_juice_account /docs/examples/ts/aztecjs_connection/index.ts typescript + +Claim the bridged Fee Juice and deploy in one step: + +#include_code bridge_fee_juice_claim /docs/examples/ts/aztecjs_connection/index.ts typescript + +If the account already has Fee Juice on L2 (for example, from a faucet or a previously claimed bridge), no special payment method is needed — just call `send({ from: NO_FROM })` and Fee Juice is used automatically. + +The `from: NO_FROM` signals that this transaction should be executed without account contract mediation. The wallet will directly execute it via a default entrypoint with no authorization. + +## Verify deployment + +Confirm the account was deployed successfully. Substitute the account variable for whichever path you used above (`newAccount` for the Sponsored FPC path, `feeJuiceAccount` for the Fee Juice path): + +#include_code verify_account_deployment /docs/examples/ts/aztecjs_connection/index.ts typescript + +## Next steps + +- [Deploy contracts](./how_to_deploy_contract.md) with your new account +- [Send transactions](./how_to_send_transaction.md) from an account +- Learn about [account abstraction](../foundational-topics/accounts/index.md) +- Implement [authentication witnesses](./how_to_use_authwit.md) diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_deploy_contract.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_deploy_contract.md new file mode 100644 index 00000000000..9e8baf6c151 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_deploy_contract.md @@ -0,0 +1,225 @@ +--- +title: Deploying Contracts +tags: [contracts, deployment] +sidebar_position: 3 +description: Deploy smart contracts to Aztec using generated TypeScript classes. +references: ["docs/examples/ts/aztecjs_advanced/index.ts", "docs/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts", "labs/yarn-project/aztec.js/src/contract/deploy_method.ts", "labs/yarn-project/aztec.js/src/deployment/*"] +--- + +This guide shows you how to deploy compiled contracts to Aztec using the generated TypeScript interfaces. + +## Overview + +Deploying a contract to Aztec involves publishing the contract class (the bytecode) and creating a contract instance at a specific address. The generated TypeScript classes handle this process through an API: you call `deploy()` with constructor arguments and `send()` with transaction options to deploy and get the contract instance. The contract address is deterministically computed from the contract class, constructor arguments, salt, and deployer address. + +import { General } from '@site/src/components/Snippets/general_snippets'; + +## Prerequisites + +- Compiled contract artifacts (see [How to Compile](../aztec-nr/compiling_contracts.md)) +- <General.AztecJSPrerequisites /> +- TypeScript project set up + +## Generate TypeScript bindings + +### Compile and generate code + +```bash +# Compile the contract +aztec compile + +# Generate TypeScript interface +aztec codegen ./target/my_contract-MyContract.json -o src/artifacts +``` + +:::info +The codegen command creates a TypeScript class with typed methods for deployment and interaction. This provides type safety and autocompletion in your IDE. +::: + +## Deploy a contract + +### Step 1: Import and connect + +```typescript +import { MyContract } from "./artifacts/MyContract"; +``` + +:::note[About wallets and accounts] +In the examples below, `wallet` refers to a `Wallet` instance that manages keys and signs transactions. See [Creating Accounts](./how_to_create_account.md) for how to set up a wallet. The `from` option in `send()` specifies which account pays for the transaction. This account must be registered in the wallet and have sufficient fee juice. On a local network, test accounts are pre-funded; on testnet, you typically use sponsored fees. +::: + +### Step 2: Deploy the contract + +How you deploy depends on how you pay for it. When paying using an account's fee juice (like a test account on the local network): + +#include_code deploy_basic_local /docs/examples/ts/aztecjs_advanced/index.ts typescript + +On testnet, your account likely won't have Fee Juice. Instead, pay fees using the [Sponsored Fee Payment Contract method](./how_to_pay_fees.md): + +#include_code deploy_sponsored_fpc_contract /docs/examples/ts/aztecjs_advanced/index.ts typescript + +Here's a complete example from the test suite: + +#include_code deploy_basic labs/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts typescript + +## Use deployment options + +### Deploy with custom salt + +By default, the deployment's salt is random, but you can specify it (for example, if you want to get a deterministic address): + +#include_code deploy_custom_salt /docs/examples/ts/aztecjs_advanced/index.ts typescript + +### Deploy universally + +Deploy to the same address across networks by setting `universalDeploy: true`: + +#include_code deploy_universal labs/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts typescript + +:::info +Universal deployment excludes the sender from address computation, allowing the same address on any network with the same salt. +::: + +### Skip initialization + +Deploy without running the constructor: + +#include_code skip_initialization /docs/examples/ts/aztecjs_advanced/index.ts typescript + +### Deploy with a specific initializer + +Some contracts have multiple initializer functions (e.g., both a private `constructor` and a `public_constructor`). By default, the generated `deploy()` method uses the default initializer (typically named `constructor`). To deploy using a different initializer, use `deployWithOpts`: + +#include_code deploy_with_opts labs/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts typescript + +The `deployWithOpts` method accepts an options object as its first argument: + +- `wallet`: The wallet to use for deployment (required) +- `method`: The name of the initializer function to call (optional, defaults to `constructor`) +- `publicKeys`: Custom public keys for the contract instance (optional) + +The remaining arguments are the parameters for the chosen initializer function. + +:::tip +This is useful for contracts that support multiple initialization patterns, such as token standards that allow both private and public minting during deployment. +::: + +## Calculate deployment address + +### Get address before deployment + +#include_code calculate_address_before_deploy /docs/examples/ts/aztecjs_advanced/index.ts typescript + +:::warning +This is an advanced pattern. For most use cases, deploy the contract directly and get the address from the deployed instance. +::: + +## Monitor deployment progress + +### Track deployment transaction + +Use `NO_WAIT` to get the transaction hash immediately and track deployment: + +#include_code no_wait_deploy_imports /docs/examples/ts/aztecjs_advanced/index.ts typescript + +#include_code no_wait_deploy /docs/examples/ts/aztecjs_advanced/index.ts typescript + +For most use cases, simply await the deployment to get the contract directly: + +#include_code deploy_contract /docs/examples/ts/aztecjs_connection/index.ts typescript + +## Deploy multiple contracts + +### Deploy a token contract + +Here's an example deploying a `TokenContract` with constructor arguments for admin, name, symbol, and decimals: + +#include_code deploy_token labs/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts typescript + +### Deploy contracts with dependencies + +When one contract depends on another, deploy them sequentially and pass the first contract's address: + +#include_code deploy_with_dependencies /docs/examples/ts/aztecjs_advanced/index.ts typescript + +### Deploy contracts in parallel + +#include_code parallel_deploy /docs/examples/ts/aztecjs_advanced/index.ts typescript + +:::tip[Parallel deployment considerations] +Parallel deployment is faster, but transactions from the same account share a nonce sequence. The wallet handles nonce assignment automatically, but if one deployment fails, subsequent deployments may also fail due to nonce gaps. For reliable parallel deployments: + +- Use separate accounts for each deployment, or +- Handle failures gracefully and retry with fresh nonces +- Consider using `BatchCall` to bundle multiple operations into a single transaction (see below) + ::: + +### Deploy with BatchCall + +Use `BatchCall` to bundle a deployment with other calls into a single transaction. This is useful when you need to deploy a contract and immediately call methods on it: + +#include_code deploy_batch labs/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts typescript + +## Verify deployment + +### Check contract state + +Use `wallet.getContractMetadata()` to check your contract's current state: + +```typescript +const metadata = await wallet.getContractMetadata(contractAddress); + +// Check each state: +metadata.instance; // Contract registered in your wallet? +metadata.isContractClassPubliclyRegistered; // Class registered on the network? +metadata.isContractPublished; // Instance registered on the network? +metadata.initializationStatus; // Constructor has been called? +``` + +For a complete overview of what these states mean and when functions become callable, see [Contract Readiness States](../aztec-nr/contract_readiness_states.md). + +Here's a complete example: + +#include_code verify_deployment labs/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts typescript + +### What the PXE checks automatically + +When you simulate or send a transaction, the PXE automatically verifies: + +- Contract instance is registered in your wallet +- Contract artifact is available locally +- Contract class ID matches the network state + +The PXE does **not** automatically check: + +- Whether the contract is published on the network +- Whether the contract is initialized +- Whether the contract class is registered on the network + +If you call a public function on an unpublished contract, the transaction will fail at the network level, not during local simulation. Use `getContractMetadata()` to check these states before sending transactions if you want to provide better error messages to users. + +### Verify contract is callable + +#include_code verify_contract_callable /docs/examples/ts/aztecjs_advanced/index.ts typescript + +## Register deployed contracts + +### Add existing contract to wallet + +If a contract was deployed by another account: + +#include_code register_external_contract /docs/examples/ts/aztecjs_advanced/index.ts typescript + +:::warning +You need the exact deployment parameters (salt, initialization hash, etc.) to correctly register an externally deployed contract. If you don't have access to the contract instance, you can reconstruct it: + +#include_code reconstruct_contract_instance /docs/examples/ts/aztecjs_advanced/index.ts typescript + +::: + +## Next steps + +- [Contract Readiness States](../aztec-nr/contract_readiness_states.md) - Understand the different states a contract progresses through +- [Send transactions](./how_to_send_transaction.md) to interact with your contract +- [Read contract data](./how_to_read_data.md) including simulating functions and reading events +- [Use authentication witnesses](./how_to_use_authwit.md) for delegated calls diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_pay_fees.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_pay_fees.md new file mode 100644 index 00000000000..089e9e0f839 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_pay_fees.md @@ -0,0 +1,226 @@ +--- +title: Paying Fees +tags: [fees, transactions, accounts, mana, gas] +sidebar_position: 7 +description: Pay transaction fees on Aztec, understand mana costs, estimate gas, and retrieve fees from receipts. +references: ["docs/examples/ts/aztecjs_advanced/index.ts", "docs/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/end-to-end/src/single-node/fees/private_payments.parallel.test.ts", "labs/yarn-project/aztec.js/src/fee/*"] +--- + +import { General, Fees } from '@site/src/components/Snippets/general_snippets'; + +This guide walks you through paying transaction fees on Aztec using various payment methods. + +## Prerequisites + +- <General.AztecJSPrerequisites /> +- Understanding of [fee concepts](../foundational-topics/fees.md) + +:::info +<Fees.FeeAsset_NonTransferrable /> +::: + +## Payment methods overview + +| Method | Use Case | Privacy | Requirements | +| ------------------- | ---------------------------------------------- | --------------- | ---------------------------- | +| Fee Juice (default) | Account already has Fee Juice | Public | Funded account | +| Sponsored FPC | Testing, free transactions | Public | None | +| Private FPC | Privacy-preserving fees | Private | Bridged Fee Juice via FPC | +| Third-party FPC | Pay in other tokens on testnet/mainnet | Varies by FPC | FPC provider's SDK | +| Bridge + Claim | Bootstrap from L1 | Public | L1 ETH for gas | + +## Mana and Fee Juice + +Mana is Aztec's unit of computational effort (like gas on Ethereum), and Fee Juice is the native fee token used to pay for transactions. For a detailed explanation of these concepts, see [Fee Concepts](../foundational-topics/fees.md). + +## Estimate mana costs + +:::tip Automatic estimation with EmbeddedWallet +When using `EmbeddedWallet`, gas is estimated automatically on every `send()` call. You only need to manually estimate if you want to preview costs before sending, or if you're using a custom wallet implementation. +::: + +Before sending a transaction, you can read the mana it will consume by simulating with `includeMetadata: true` and reading `gasUsed` from the result: + +#include_code estimate_mana /docs/examples/ts/aztecjs_advanced/index.ts typescript + +The `gasUsed` object contains the raw gas the simulation consumed: + +- `totalGas.daGas` / `totalGas.l2Gas` - DA and L2 mana consumed across the whole transaction +- `teardownGas.daGas` / `teardownGas.l2Gas` - DA and L2 mana consumed in the teardown phase + +It is up to you to derive the gas limits you declare from this raw usage (typically by padding it, as shown above). If you don't declare any gas limits, the wallet fills in the network's per-tx admission limits for you. + +### Calculate expected fee from estimate + +To calculate the expected fee from the padded gas, use the `computeFee` method with current network fees: + +#include_code compute_fee_from_estimate /docs/examples/ts/aztecjs_advanced/index.ts typescript + +:::tip +Pad the raw `gasUsed` yourself to leave a safety margin. Multiplying by `1.1` adds 10%; use higher padding for transactions with variable gas costs. The wallet rejects any declared limit above the network's per-tx admission limit. +::: + +## Get transaction fee from receipt + +After a transaction is mined, you can retrieve the fee paid from the receipt: + +#include_code get_fee_from_receipt /docs/examples/ts/aztecjs_advanced/index.ts typescript + +The `transactionFee` field is a `bigint` representing the total fee paid in the fee token (Fee Juice). You can also check execution status: + +#include_code check_receipt_status /docs/examples/ts/aztecjs_advanced/index.ts typescript + +## Pay with Fee Juice + +Fee Juice is the native fee token on Aztec. + +If your account has Fee Juice (for example, from a faucet), is [deployed](./how_to_create_account.md), and is registered in your wallet, it will be used automatically to pay for the fee of the transaction: + +#include_code pay_with_fee_juice /docs/examples/ts/aztecjs_advanced/index.ts typescript + +## Use Fee Payment Contracts + +Fee Payment Contracts (FPCs) pay Fee Juice on your behalf. An FPC holds its own Fee Juice balance to pay the protocol and can accept other tokens from users in exchange. Some FPCs operate privately by design, routing fee payments through private notes rather than public function calls. + +:::note +The SDK includes `PrivateFeePaymentMethod` and `PublicFeePaymentMethod` classes for the built-in reference FPC, but these are **deprecated** and do not work on mainnet alpha. For custom-token fee payment, use a third-party FPC with its own SDK (see [below](#third-party-fpcs-on-testnet-and-mainnet)). +::: + +### Sponsored FPC + +#if(mainnet) +:::note +The Sponsored FPC is not available on mainnet. It is available on testnet, devnet, and local network. +::: +#endif + +The Sponsored FPC pays fees unconditionally, enabling free transactions. It is available on testnet, devnet, and local network. + +You can derive the Sponsored FPC address from its deployment parameters, register it with your wallet, and use it to pay for transactions: + +#include_code deploy_sponsored_fpc_contract /docs/examples/ts/aztecjs_advanced/index.ts typescript + +Here's a simpler example from the test suite: + +#include_code sponsored_fpc_simple labs/yarn-project/end-to-end/src/single-node/fees/private_payments.parallel.test.ts typescript + +### Private fee payment + +For transactions where the fee payment itself should be private, you can use a fully private FPC, one that holds Fee Juice claimed from L1 as an internal private balance, works on every network, and never needs an onchain deployment. See [Pay Fees Privately](./how_to_use_private_fee_juice.md) for how this pattern works and a walkthrough using a community-built example. + +:::tip Shared salt for privacy +When multiple apps derive the same private FPC address (using the same artifact and salt), every private fee payment joins a single, larger privacy set. See [recommended salt](./how_to_use_private_fee_juice.md#recommended-salt-0) for details. +::: + +### Third-party FPCs on testnet and mainnet + +On networks where the Sponsored FPC is unavailable, third-party FPCs deployed by ecosystem teams let you pay fees in tokens other than Fee Juice. Each FPC provider typically offers an SDK or API that handles payment method construction on the client side. This may include quote fetching and authwit creation, though the exact flow depends on the FPC design. For background on how FPCs work at the protocol level, see [how FPCs work](../foundational-topics/fees.md#how-fpcs-work). + +#### Example: Nethermind Private Multi Asset FPC + +To illustrate how a third-party FPC integration works, the following walkthrough uses Nethermind's [Private Multi Asset FPC](https://github.com/NethermindEth/aztec-fpc) as a reference. This is one implementation, other FPCs may differ in design and API. + +This FPC is quote-based and operates privately: + +- A single deployment accepts many tokens. The asset is selected per quote rather than hard-coded at deploy time. +- Fee payments are transferred as private notes, so fee activity is not visible onchain. +- An operator-run attestation service signs per-user quotes binding the FPC address, accepted asset, amounts, expiry, and user. +- A cold-start entrypoint allows a brand-new account to bridge tokens from L1, claim on L2, and pay the fee in a single transaction. Note that the cold-start path calls `Token::mint_to_private`, which enqueues a public call to update the token's total supply, so the minted amount is visible onchain even though the user's identity and balances remain private. + +:::warning Third-party software +This FPC is developed and maintained by Nethermind, not by Aztec Labs. The SDK (`@nethermindeth/aztec-fpc-sdk`) may not yet be published to npm; check the [repository README](https://github.com/NethermindEth/aztec-fpc/blob/main/sdk/README.md) for current install instructions. Review the [protocol spec](https://github.com/NethermindEth/aztec-fpc/blob/main/docs/spec/protocol-spec.md) and evaluate independently before integrating. +::: + +The SDK wraps the quote-and-pay flow into a single call. The snippet below shows the general shape of the integration (illustrative; verify against the current SDK API before using): + +```ts +import { FpcClient } from "@nethermindeth/aztec-fpc-sdk"; + +// Point the client at the FPC's attestation service +const fpcClient = new FpcClient({ + fpcAddress, // the deployed FPC contract address + operator, // operator's Aztec address + node, // PXE or node connection + attestationBaseUrl: "https://...", // attestation service URL from the FPC provider +}); + +// Estimate gas, fetch a signed quote, and build the payment method +const payment = await fpcClient.createPaymentMethod({ + wallet, + user: aliceAddress, // the account paying the fee + tokenAddress, // the token you want to pay in + estimatedGas, // gas limits derived from a prior simulation's gasUsed +}); + +// Use it like any other payment method +const tx = await myContract.methods.myMethod(args).send({ fee: payment.fee }); +await tx.wait(); +``` + +For the cold-start flow, deployment addresses, and the full API, see the [`aztec-fpc` repository](https://github.com/NethermindEth/aztec-fpc). + +## Bridge Fee Juice from L1 + +Fee Juice is non-transferable on L2, but you can bridge it from L1, claim it on L2, and use it. This involves a few components that are part of a running network's infrastructure: + +- An L1 fee juice contract +- An L1 fee juice portal +- An L2 fee juice portal +- An L2 fee juice contract + +`aztec.js` provides helpers to simplify the process: + +#include_code bridge_fee_juice_setup /docs/examples/ts/aztecjs_connection/index.ts typescript + +Under the hood, `L1FeeJuicePortalManager` gets the L1 addresses from the node `aztec_getNodeInfo` endpoint. It then exposes an easy method `bridgeTokensPublic` which mints fee juice on L1 and sends it to an L2 address via the L1 portal: + +#include_code bridge_fee_juice_execute /docs/examples/ts/aztecjs_connection/index.ts typescript + +After this transaction is minted on L1 and a few blocks pass, you can claim the message on L2 and use it directly to pay for fees: + +#include_code bridge_fee_juice_claim /docs/examples/ts/aztecjs_connection/index.ts typescript + +## Configure gas settings + +### Understanding gas dimensions + +Gas settings specify limits and fees for both DA and L2 dimensions: + +- **gasLimits**: Maximum mana the transaction may consume in total, across all phases including teardown +- **teardownGasLimits**: Portion of `gasLimits` reserved for the teardown phase (used by FPCs for refunds) +- **maxFeesPerGas**: Maximum price you're willing to pay per mana unit +- **maxPriorityFeesPerGas**: Priority fee for faster inclusion + +Teardown gas is not added on top of `gasLimits`: it is a reservation carved out of the total, so the mana available to the rest of the transaction is `gasLimits - teardownGasLimits`. A transaction with a teardown call is billed the full `teardownGasLimits`, regardless of how much teardown actually consumes. + +The fee limit is calculated as `gasLimits × maxFeesPerGas` for each dimension. + +### Set custom gas limits + +Set custom gas limits by importing from `stdlib`: + +#include_code custom_gas_settings /docs/examples/ts/aztecjs_advanced/index.ts typescript + +Then pass the settings when sending: + +#include_code send_with_gas_settings /docs/examples/ts/aztecjs_advanced/index.ts typescript + +Note that `gasLimits` and `teardownGasLimits` use `daGas`/`l2Gas` field names, while `maxFeesPerGas` and `maxPriorityFeesPerGas` use `feePerDaGas`/`feePerL2Gas`. + +### Use automatic gas estimation + +:::note +When using `EmbeddedWallet`, gas estimation happens automatically on every `send()`; you don't need to declare gas limits at all. Reading `gasUsed` from a `simulate({ includeMetadata: true })` result is useful when you want to preview costs before sending, or to set explicit limits with a custom wallet implementation. +::: + +#include_code auto_gas_estimation /docs/examples/ts/aztecjs_advanced/index.ts typescript + +:::tip +Gas estimation runs a simulation first to determine actual gas usage, then adds padding for safety. This works with all payment methods, including FPCs. +::: + +## Next steps + +- Learn about [fee concepts](../foundational-topics/fees.md) in detail +- Explore [authentication witnesses](./how_to_use_authwit.md) for delegated payments +- See [testing guide](./how_to_test.md) for fee testing strategies diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_read_data.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_read_data.md new file mode 100644 index 00000000000..2215fc0bad8 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_read_data.md @@ -0,0 +1,145 @@ +--- +title: Reading Contract Data +tags: [functions, simulation, events, logs] +sidebar_position: 5 +description: How to read data from contracts including simulating functions, reading logs, and retrieving events. +references: ["docs/examples/ts/aztecjs_connection/index.ts", "docs/examples/ts/aztecjs_advanced/index.ts", "labs/yarn-project/end-to-end/src/automine/effects/event_logs.test.ts", "labs/yarn-project/aztec.js/src/contract/contract_function_interaction.ts"] +--- + +This guide shows you how to read data from Aztec contracts in TypeScript, including simulating function calls, reading raw logs, and retrieving typed events. + +import { General } from '@site/src/components/Snippets/general_snippets'; + +## Prerequisites + +- <General.AztecJSPrerequisites /> +- A deployed contract instance (see [How to Deploy a Contract](./how_to_deploy_contract.md)) + +## Simulating functions + +The `simulate` method executes a contract function locally and returns its result. It works with private, public, and utility functions. No transaction is created and no gas is spent. + +#include_code simulate_function /docs/examples/ts/aztecjs_connection/index.ts typescript + +The `from` option specifies which account context to use for the simulation. This is required for all simulations. For private functions, it determines which account's private state is accessed. For public functions, it sets the `msg_sender` context. + +### Handling return values + +For functions returning multiple values, destructure the result: + +```typescript +// contract and callerAddress are from the example above +const { result: [value1, value2] } = await contract.methods + .get_multiple_values() + .simulate({ from: callerAddress }); +``` + +### Including metadata + +Set `includeMetadata: true` to get additional information about the simulation: + +#include_code simulate_with_metadata /docs/examples/ts/aztecjs_advanced/index.ts typescript + +The result includes `result` (the function return value), `stats` (execution statistics), `offchainEffects`, and `gasUsed` (the raw gas the simulation consumed, with `totalGas` and `teardownGas`). Derive your own gas limits from `gasUsed` if you want to declare them explicitly; otherwise the wallet fills in the network's per-tx admission limits. + +### Private function considerations + +When simulating private functions, the caller must have access to any private state being read. The PXE only has visibility into notes belonging to registered accounts. + +#include_code simulate_private_access /docs/examples/ts/aztecjs_advanced/index.ts typescript + +If the caller doesn't have access to another address's notes, the simulation will fail with an error. + +:::tip +If `.simulate()` is prompting the user to sign every call, or failing with `min_revertible_side_effect_counter must not be 0` when you pass `from: AztecAddress.ZERO`, see [Simulate without signing prompts](./how_to_simulate_without_signing.md). +::: + +:::warning +Simulation runs locally without generating proofs. No correctness guarantees are provided on the result. See [Call Types](../foundational-topics/call_types.md#simulate) for more details. +::: + +## Reading logs vs events + +Contracts emit data in two forms you can read: + +| Aspect | Logs | Events | +| ------------------ | --------------------------- | -------------------------------------------------- | +| **What** | Raw field arrays (untyped) | Decoded domain objects with type info | +| **Storage** | Archiver (node-level) | PXE (client-level) for private events | +| **API** | `aztecNode.getBlock()` tx effects | `wallet.getPrivateEvents()` or `getPublicEvents()` | +| **Type awareness** | None - raw `Fr[]` data | Requires ABI metadata to decode | + +**Logs** are the low-level transport layer, while **events** are the semantic application layer decoded using ABI metadata from your contract. + +## Reading raw public logs + +Raw public logs are carried on each block's transaction effects. Fetch a block with `includeTransactions: true` and read `body.txEffects[*].publicLogs`: + +#include_code read_public_logs /docs/examples/ts/aztecjs_advanced/index.ts typescript + +You can scope this to a single transaction (by locating its block and matching its tx hash) or to a block range (by reading each block's tx effects): + +#include_code read_logs_by_filter /docs/examples/ts/aztecjs_advanced/index.ts typescript + +## Reading events + +Events provide typed access to contract emissions. The event metadata from your contract artifact (`Contract.events.EventName`) contains the ABI type information needed for decoding. + +### Reading public events + +Use the `getPublicEvents` helper to retrieve typed public events: + +#include_code import_get_public_events /docs/examples/ts/aztecjs_advanced/index.ts typescript + +#include_code get_public_events labs/yarn-project/end-to-end/src/automine/effects/event_logs.test.ts typescript + +The function parameters are: + +- `aztecNode` - The node to query +- `Contract.events.EventName` - Event metadata from the contract artifact (contains the event selector) +- `filter` - An object with optional fields: + - `fromBlock` - Starting block number (inclusive) + - `toBlock` - Ending block number (exclusive) + - `contractAddress` - Filter to a specific contract + - `txHash` - Filter to a specific transaction + +Each returned event includes both the decoded `event` data and `metadata` (block number, block hash, tx hash, contract address). + +### Reading private events + +Private events are stored in the PXE with privacy scoping. Use `wallet.getPrivateEvents()` to retrieve them: + +#include_code import_private_event_types /docs/examples/ts/aztecjs_advanced/index.ts typescript + +The `BlockNumber` type is a branded type that wraps raw numbers for type safety. Use it when setting `fromBlock` and `toBlock` in filters. + +#include_code get_private_events labs/yarn-project/end-to-end/src/automine/effects/event_logs.test.ts typescript + +The `PrivateEventFilter` includes: + +- `contractAddress` - The contract that emitted the events +- `fromBlock` / `toBlock` - Block range to search +- `scopes` - Array of account addresses whose private state is being queried +- `txHash` (optional) - Filter to a specific transaction + +Private events return objects with an `event` property containing the decoded data: + +```typescript +collectedEvents.forEach((ev) => { + console.log(ev.event.value0); // Access event fields via .event +}); +``` + +## Polling for events + +To continuously monitor for new events, poll at regular intervals while tracking the last processed block: + +#include_code poll_for_events /docs/examples/ts/aztecjs_advanced/index.ts typescript + +For private events, use the same pattern with `wallet.getPrivateEvents()` and update the `fromBlock` in your filter accordingly. + +## Next steps + +- [Send transactions](./how_to_send_transaction.md) to modify contract state +- Learn about [call types](../foundational-topics/call_types.md) and when to use simulation vs transactions +- Explore [testing patterns](./how_to_test.md) that use simulation diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_send_transaction.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_send_transaction.md new file mode 100644 index 00000000000..d02a680f97e --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_send_transaction.md @@ -0,0 +1,108 @@ +--- +title: Sending Transactions +sidebar_position: 4 +description: Send transactions to Aztec contracts using Aztec.js with various options and error handling +tags: [transactions, contracts, aztec.js] +references: ["docs/examples/ts/aztecjs_advanced/index.ts", "labs/yarn-project/aztec.js/src/contract/*"] +--- + +This guide shows you how to send transactions to smart contracts on Aztec. + +## Overview + +Transactions on Aztec execute contract functions that modify state. Unlike simple reads, transactions go through private execution on your device, proving, and then submission to the network for inclusion in a block. You can send single transactions, batch multiple calls atomically, and query transaction status after submission. + +import { General } from '@site/src/components/Snippets/general_snippets'; + +## Prerequisites + +- <General.AztecJSPrerequisites /> +- Deployed contract with its address and ABI (see [How to Deploy](./how_to_deploy_contract.md)) +- Understanding of [contract interactions](../aztec-nr/framework-description/calling_contracts.md) + +## Send a transaction + +After connecting to a contract: + +#include_code connect_to_contract /docs/examples/ts/aztecjs_advanced/index.ts typescript + +Call a function and wait for it to be mined: + +#include_code basic_send_transaction /docs/examples/ts/aztecjs_advanced/index.ts typescript + +The `from` field specifies which account sends the transaction. If that account has Fee Juice, it pays for the transaction automatically. For other fee payment options, see [paying fees](./how_to_pay_fees.md). + +### What happens behind the scenes + +When using `EmbeddedWallet`, calling `send()` triggers a **simulation** step before the transaction is actually sent. This simulation: + +1. **Estimates gas limits** based on actual execution, with a configurable padding (default 10%) to avoid reverts. If you provide explicit gas limits via `fee.gasSettings`, they take precedence. +2. **Generates private authwits automatically**. If the contract you're calling requires a private [authentication witness](./how_to_use_authwit.md) (e.g., a token transfer on behalf of the sender), the wallet detects this during simulation and creates the authwit on the fly — no manual setup needed. +3. **Delays the first receipt poll.** After the transaction is sent, `.wait()` holds off one poll interval before its first `getTxReceipt` call, since the transaction cannot have been mined yet. Environments where receipts are available immediately (for example automine) can pass `initialDelay: 0` to `.wait()`. + +This means a simple `.send()` is all most apps need. You can adjust the gas padding if desired: + +#include_code set_gas_padding /docs/examples/ts/aztecjs_advanced/index.ts typescript + +:::note +Public authwits still need to be set explicitly before the transaction, as they require a separate onchain transaction. See [Using Authentication Witnesses](./how_to_use_authwit.md) for details. +::: + +### Send without waiting + +Use the `NO_WAIT` option to get the transaction hash immediately without waiting for inclusion: + +#include_code no_wait_transaction /docs/examples/ts/aztecjs_advanced/index.ts typescript + +## Send batch transactions + +Execute multiple calls atomically using `BatchCall`: + +#include_code batch_call /docs/examples/ts/aztecjs_advanced/index.ts typescript + +:::warning +All calls in a batch must succeed or the entire batch reverts. Use batch transactions when you need atomic execution of multiple operations. +::: + +## Query transaction status + +After sending a transaction without waiting, you can query its receipt using the node: + +#include_code query_tx_status /docs/examples/ts/aztecjs_advanced/index.ts typescript + +`getTxReceipt` always resolves to one of three lifecycle variants of the `TxReceipt` union, depending on where the transaction is in its lifecycle: + +- `PendingTxReceipt` - still in the mempool. Exposes `status` (`pending`) and, when requested, the pending `tx`. +- `DroppedTxReceipt` - dropped by the node. Exposes `status` (`dropped`) and an optional `error` message. +- `MinedTxReceipt` - included in a block. Exposes `status` (`proposed`, `checkpointed`, `proven`, or `finalized`), `blockNumber`, `blockHash`, `txIndexInBlock`, `transactionFee`, and the execution result. + +The `status`, `blockNumber`, and `transactionFee` fields are readable on the bare union, but block and fee details are only populated once the transaction is mined. Use the `isMined()`, `isPending()`, and `isDropped()` type guards to narrow to a specific variant before reading its fields: + +```typescript +const receipt = await node.getTxReceipt(txHash); +if (receipt.isMined()) { + console.log(`Mined in block ${receipt.blockNumber}, fee ${receipt.transactionFee}`); +} +``` + +You can pass a second `options` argument to attach extra data to the receipt: + +- `includeTxEffect` - attaches the full `TxEffect` (note hashes, nullifiers, logs, and messages) to a mined receipt, available as `receipt.txEffect`. +- `includePendingTx` - attaches the pending `Tx` to a pending receipt, available as `receipt.tx`. +- `includeProof` - keeps the proof on that attached pending tx (only meaningful together with `includePendingTx`; the proof is stripped by default to avoid shipping large payloads over RPC). + +For example, to read a mined transaction's effects, request them with `includeTxEffect` and read `receipt.txEffect` after narrowing with `isMined()`: + +```typescript +const receipt = await node.getTxReceipt(txHash, { includeTxEffect: true }); +if (receipt.isMined() && receipt.txEffect) { + console.log(`Nullifiers: ${receipt.txEffect.nullifiers.length}`); +} +``` + +## Next steps + +- Learn to [read contract data](./how_to_read_data.md) including simulating functions before sending +- Understand [authentication witnesses](./how_to_use_authwit.md) for delegated transactions +- Configure [gas and fees](./how_to_pay_fees.md) for transaction costs +- Set up [transaction testing](./how_to_test.md) in your development workflow diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md new file mode 100644 index 00000000000..41b8d1c3dec --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md @@ -0,0 +1,113 @@ +--- +title: Simulate without signing prompts +tags: [simulation, authwit, wallet] +sidebar_position: 6 +description: How to call .simulate() on a view function or estimate gas without prompting the user to sign authentication witnesses. +references: ["docs/examples/ts/aztecjs_kernelless_simulation/index.ts", "labs/yarn-project/aztec.js/src/contract/interaction_options.ts", "labs/yarn-project/aztec.js/src/authorization/call_authorization_request.ts", "labs/yarn-project/wallets/src/embedded/embedded_wallet.ts"] +--- + +You want to call `.simulate()` from an app and not have the user's wallet pop up a signing prompt. This page covers the symptoms that lead to that prompt, why the obvious workarounds do not work, and the right fix. + +For the conceptual model of what kernelless simulation is, see [Kernelless simulations](../foundational-topics/pxe/kernelless_simulations.md). + +## Symptoms + +You are probably here because of one of these: + +- The wallet prompts the user for a signature on every `.simulate()` call, including reads of view-style functions. +- `.simulate()` fails with one of: + - `Account "0x0000…0000" does not exist on this wallet.` (from `EmbeddedWallet`) + - `Account not found in wallet for address: 0x0000…0000` (from other wallets built on `BaseWallet`) + - `Circuit execution failed: min_revertible_side_effect_counter must not be 0 for tail_to_public` (from a custom wallet that does not intercept the zero address, where the call reaches the kernel) + +All three are the same root cause: passing `from: AztecAddress.ZERO`. + +- A custom fee payment method breaks during simulation because `from` is `AztecAddress.ZERO`. +- Simulations take long enough that you want to skip the kernel circuits entirely. + +## The wrong fix + +Do not pass `from: AztecAddress.ZERO`. That value was the old way to express "no account context," and it is no longer a supported input for `.simulate()`. The replacement is `NO_FROM` (from `@aztec-labs/aztec.js/account`), which tells the wallet to execute the payload through the default entrypoint with no account contract mediation. `NO_FROM` is still only appropriate for calls that genuinely have no sender; use a real account address for everything else. + +## The right fix + +A simulation uses a **stub account contract override**: the wallet provides a `SimulationOverrides` payload whose `contracts` map swaps the caller's account contract for a stub whose `is_valid` always returns true, and the PXE applies that override during the kernelless simulation. With the stub in place, authwit validity checks pass without a signature, and the wallet collects any `CallAuthorizationRequest` offchain effects emitted during the run to turn them into real authentication witnesses for the eventual `.send()`. + +`EmbeddedWallet` installs this override automatically on every `.simulate()` call, so most apps do not need to construct overrides themselves. The two ways to wire this up below correspond to "use the default" and "implement it in your own wallet." + +### Calling .simulate() from an app + +For a normal `.simulate()` you do not need to pass overrides yourself. The default simulation path is already kernelless, and wallets such as `EmbeddedWallet` install the stub-account override internally for you. Three things to remember: + +- Pass a real account address as `from`, or `NO_FROM` if the call genuinely has no sender. Do not use `AztecAddress.ZERO`. +- For simple reads, you can omit the `fee` block. +- For a transaction whose real fee path uses a fee payment contract (FPC) with private side effects (the FPC emits notes during fee payment), include that FPC in the simulation's fee options so gas estimation accounts for the FPC's side effects. Kernelless still applies; the gas number stays accurate. +- If you have a stale call site that uses `from: AztecAddress.ZERO` plus a no-op fee payment method as a workaround, replace it with a real `from` (or `NO_FROM`) and drop the no-op fee contract. + +#include_code simulate-view-without-signing /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript + +If you genuinely need to construct your own `SimulationOverrides` (for example, to combine a contract-instance swap with a `fastForwardContractUpdate` for upgrade testing), you can pass them through `.simulate()`: + +```typescript +import { SimulationOverrides } from "@aztec-labs/aztec.js/wallet"; + +const { result } = await contract.methods + .transfer_in_private(sender, recipient, amount, nonce) + .simulate({ + from: sender, + overrides: new SimulationOverrides({ + /* contracts and/or publicStorage */ + }), + }); +``` + +The override map itself has to be built by code that knows the contract class id and live contract instance. That is normally the wallet, not the app. Note that `overrides` does not apply to [utility functions](../foundational-topics/pxe/kernelless_simulations.md#where-kernelless-does-not-apply), those are simulated through `wallet.executeUtility`, which rejects `SimulationOverrides`. If your wallet does not handle the override path for you and you are tempted to reimplement it in app code, read the next section instead. + +### Implementing the override in a custom wallet + +`EmbeddedWallet` (`yarn-project/wallets/src/embedded/embedded_wallet.ts`, in `@aztec-labs/wallets`) is the canonical implementation of the override pattern and the reference any custom wallet should follow. The three pieces it wires up are: + +1. **Register the stub contract class with the PXE at wallet startup.** Inside `initStubClasses`, `EmbeddedWallet` calls `pxe.registerContractClass` for each supported account type's stub artifact and caches the resulting class id by account type. +2. **Build an override map for every account in scope.** Inside `buildAccountOverrides`, it fetches the live contract instance for each scoped address and returns a `ContractOverrides` map that copies the instance with `currentContractClassId` rewritten to the stub class id. The map covers every account in scope, not only `from`. +3. **Use the stub entrypoint and pass the override to `pxe.simulateTx`.** Inside the overridden `simulateViaEntrypoint`, it constructs the `TxExecutionRequest` through the stub account's `DefaultAccountEntrypoint` (so the request is signed by the stub's empty-signature provider) and calls `pxe.simulateTx` with the resulting `SimulationOverrides`. + +The key constraints on this path: + +- `skipKernels` must be `true` to use `contracts` overrides. The PXE rejects the combination otherwise. `pxe.simulateTx` already defaults `skipKernels` to `true`. +- The stub contract class must be registered with the PXE before you reference it in an override. +- The override map must cover every scoped account, not only `from`. + +## Collecting authwit requests + +A simulation with the stub override active will reach `#[authorize_once]` call sites in app and token contracts without prompting for signatures. Each such site emits a `CallAuthorizationRequest` as an offchain effect, which the wallet can collect and turn into a real authentication witness for the eventual `.send()`. + +The example below uses the canonical contract-mediated pattern: Alice calls `Crowdfunding.donate(amount)`, which internally calls `transfer_in_private(alice, crowdfunding, amount, 0)` with `msg_sender = crowdfunding`. The token's `#[authorize_once]` macro requires an authwit from Alice authorizing the crowdfunding contract. Because Alice is the transaction sender, her PXE already has her notes and nullifier key, so the simulation can run end-to-end against her own state without any cross-wallet state sharing. + +Run the simulation and filter the offchain effects by the `CallAuthorizationRequest` selector: + +#include_code simulate-and-collect-effects /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript + +Decode each effect into a `CallAuthorizationRequest`. The `innerHash` field is the piece the authorizing account needs to sign: + +#include_code decode-call-authorization /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript + +Build a real authentication witness from each inner hash and send the transaction with the collected witnesses attached: + +#include_code build-authwits-and-send /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript + +The app does not need to know which calls require authwits ahead of time. The simulation discovers them; the wallet signs them at send time. + +`EmbeddedWallet.sendTx` runs this same simulate-then-collect flow internally before delegating to `BaseWallet.sendTx`, so an app that uses `EmbeddedWallet` does not need to call `.simulate()` manually and pass `authWitnesses` to `.send()`. The explicit pattern above is the one a wallet that does not auto-collect must implement, either inside its `sendTx` (as `EmbeddedWallet` does) or inside the app call site. + +## Things to watch out for + +- **`AztecAddress.ZERO` is not "no sender".** Use `NO_FROM` (from `@aztec-labs/aztec.js/account`) for calls that genuinely have no account context, and a real account address otherwise. +- **A private FPC needs to be included in fee options for accurate gas.** Kernelless simulation matches full simulation on gas, but only if the simulation sees the same fee path as the real transaction. If the user will pay through a fee payment contract (FPC) that emits private notes, pass that FPC in the simulation's fee options so its side effects are accounted for. Kernelless plus a private FPC is the supported path; you do not need a full simulation to get accurate gas. +- **`profile()` is not kernelless.** If you call `.profile()` to count circuit gates, the kernels run regardless. Use `.simulate()` if you only need return values, offchain effects, or gas estimates. +- **Utility functions reject overrides.** `FunctionType.UTILITY` calls go through `wallet.executeUtility`, and `ContractFunctionInteraction.simulate` throws `overrides are not supported for utility function simulation` if you pass non-empty `overrides.publicStorage` or `overrides.contracts`. Utility functions do not need an override anyway, since they do not run through an account contract. + +## Related + +- [Kernelless simulations](../foundational-topics/pxe/kernelless_simulations.md) for the conceptual model. +- [Reading contract data](./how_to_read_data.md) for the basic `.simulate()` API. +- [Authentication witnesses](../foundational-topics/advanced/authwit.md) for what `CallAuthorizationRequest` represents. diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_test.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_test.md new file mode 100644 index 00000000000..5f90a5240af --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_test.md @@ -0,0 +1,111 @@ +--- +title: Testing Smart Contracts +tags: [contracts, tests] +sidebar_position: 8 +description: Learn how to write and run tests for your Aztec smart contracts using Aztec.js and a local network. +references: ["docs/examples/ts/aztecjs_testing/index.ts", "docs/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/wallets/src/embedded/*", "labs/yarn-project/accounts/src/testing/*"] +--- + +This guide covers how to test Aztec smart contracts by connecting to a local network, deploying contracts, and verifying their behavior. + +## Prerequisites + +- A running [local Aztec network](../../getting_started_on_local_network.md) +- A compiled contract artifact (see [How to compile a contract](../aztec-nr/compiling_contracts.md)) +- Node.js test framework (Jest, Vitest, or similar) + +## Setting up the test environment + +Connect to your local Aztec network and create an embedded wallet: + +#include_code connect_to_network /docs/examples/ts/aztecjs_connection/index.ts typescript + +The `EmbeddedWallet` manages accounts, tracks deployed contracts, and handles transaction proving. It connects to the Aztec node which provides access to both the Private eXecution Environment (PXE) and the network. + +## Loading test accounts + +The local network comes with pre-funded accounts. Load them into your wallet: + +#include_code load_test_accounts /docs/examples/ts/aztecjs_testing/index.ts typescript + +## Deploying contracts in tests + +Deploy contracts using the generated contract class: + +#include_code deploy_test_contract /docs/examples/ts/aztecjs_testing/index.ts typescript + +## Verifying contract state + +Use `.simulate()` to read contract state without creating a transaction: + +#include_code simulate_function /docs/examples/ts/aztecjs_connection/index.ts typescript + +Simulations are free (no gas cost) and return the function's result directly. Use them for: + +- Checking balances and state before/after transactions +- Validating expected outcomes in assertions +- Debugging contract behavior + +## Sending test transactions + +Send transactions and wait for confirmation: + +#include_code send_transaction /docs/examples/ts/aztecjs_connection/index.ts typescript + +The `send()` method returns when the transaction is included in a block. + +## Example test structure + +Here's a complete test example showing the typical structure with setup, test cases, and assertions: + +#include_code complete_test_example /docs/examples/ts/aztecjs_testing/index.ts typescript + +## Testing failure cases + +Test that invalid operations revert as expected: + +#include_code test_revert_case /docs/examples/ts/aztecjs_testing/index.ts typescript + +Use `.simulate()` to test reverts without spending gas. The simulation will throw if the transaction would fail onchain. + +## Simulating with overrides + +`.simulate()` accepts an `overrides` option that injects values into the simulator's (ephemeral) world-state fork and contract DB before the call runs. The override is scoped to that single simulation and thrown away afterwards. + +Override a public-storage slot: + +```typescript +const result = await contract.methods.read_balance(account).simulate({ + overrides: { + publicStorage: [{ contract: contract.address, slot: BALANCE_SLOT, value: new Fr(1_000_000n) }], + }, +}); +``` + +Use this to set up state preconditions, reproduce production bugs against pinned storage, or exercise rare value branches without orchestrating the contract calls that produce them. + +### Fast-forwarding a contract update + +`fastForwardContractUpdate` returns a `SimulationOverrides` object that simulates a deployed instance as if it had already been upgraded to a new contract class. The new class must already be registered on chain. The cheat mirrors a real onchain upgrade followed by waiting out the upgrade delay: the override instance's `currentContractClassId` is bumped, and the `ContractInstanceRegistry`'s delayed-public-mutable storage is rewritten to look like the upgrade was scheduled in the past. + +```typescript +import { fastForwardContractUpdate } from '@aztec-labs/aztec.js'; + +const overrides = await fastForwardContractUpdate({ + instanceAddress: contract.address, + newClassId: upgradedClass.id, + node, +}); + +const result = await contract.methods.upgraded_method().simulate({ overrides }); +``` + +Use this to test code paths that only execute after an upgrade, without orchestrating the full delayed-mutable upgrade flow. + +## Further reading + +- [How to read contract data](./how_to_read_data.md) +- [How to send transactions](./how_to_send_transaction.md) +- [How to deploy a contract](./how_to_deploy_contract.md) +- [How to create an account](./how_to_create_account.md) +- [How to compile a contract](../aztec-nr/compiling_contracts.md) diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_authwit.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_authwit.md new file mode 100644 index 00000000000..480b16304c8 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_authwit.md @@ -0,0 +1,83 @@ +--- +title: Using Authentication Witnesses +tags: [accounts, authwit] +sidebar_position: 6 +description: Step-by-step guide to implementing authentication witnesses in Aztec.js for delegated transactions. +references: ["docs/examples/ts/aztecjs_authwit/index.ts", "labs/yarn-project/aztec.js/src/utils/authwit.ts", "labs/yarn-project/aztec.js/src/wallet/*"] +--- + +This guide shows you how to create and use authentication witnesses (authwits) to authorize other accounts to perform actions on your behalf. + +:::tip Automatic private authwits with EmbeddedWallet + +When using `EmbeddedWallet`, **private authwits are created automatically**. The wallet simulates your transaction before sending and detects which private authwits are needed, then generates them on the fly. You don't need to create them manually. + +Public authwits still need to be set explicitly, as they require a separate onchain transaction before use. The manual approach described below is also relevant if you're building a custom wallet implementation. + +::: + +:::warning aztec-nr + +Using AuthWitnesses is always a two-part process. This guide shows how to generate and use them, but you still need to set up your contract to accept and authenticate them. + +Therefore it is recommended to read the `aztec-nr` [guide on authwitnesses](../aztec-nr/framework-description/authentication_witnesses.md) before this one. + +::: + +import { General } from '@site/src/components/Snippets/general_snippets'; + +## Prerequisites + +- <General.AztecJSPrerequisites /> +- Contract with authwit validation (see [smart contract authwits](../aztec-nr/framework-description/authentication_witnesses.md)) +- Understanding of [authwit concepts](../foundational-topics/advanced/authwit.md) + +## Intent types + +The authwit system supports different intent types depending on your use case: + +- **`CallIntent`**: Use when authorizing a specific contract function call. Contains `{ caller, call }` where `call` is a `FunctionCall`, typically obtained with `await interaction.getFunctionCall()`. +- **`ContractFunctionInteractionCallIntent`**: Convenience form that takes the interaction directly. Contains `{ caller, action }` where `action` is a `ContractFunctionInteraction`; internally resolved to a `FunctionCall` before signing. +- **`IntentInnerHash`**: Use when authorizing arbitrary data. Contains `{ consumer, innerHash }` where `consumer` is the contract that will verify the authwit. + +## Create private authwits + +:::note +If you're using `EmbeddedWallet`, this section is handled for you automatically. See the tip above. +::: + +Private authwits authorize actions in the private domain. The authorization is included directly in the transaction that uses it. + +Let's say Alice wants to allow Bob to transfer tokens from her account. Alice is the **authorizer** (she owns the tokens) and Bob is the **caller** (he will execute the transfer): + +#include_code private_authwit /docs/examples/ts/aztecjs_authwit/index.ts typescript + +:::tip +The nonce prevents replay attacks. When `from` and `msg_sender` are the same (self-transfer), set the nonce to `0`. +::: + +## Create public authwits + +Public authwits require a transaction to store the authorization in the `AuthRegistry` contract before the authorized action can be executed: + +#include_code public_authwit /docs/examples/ts/aztecjs_authwit/index.ts typescript + +## Create arbitrary message authwits + +Use this when authorizing arbitrary data rather than a specific contract function call: + +#include_code arbitrary_authwit /docs/examples/ts/aztecjs_authwit/index.ts typescript + +The `consumer` is the contract address that will verify this authwit. + +## Revoke public authwits + +Public authwits can be revoked by setting `authorized` to `false`: + +#include_code revoke_authwit /docs/examples/ts/aztecjs_authwit/index.ts typescript + +## Next steps + +- Learn about [authwits in smart contracts](../aztec-nr/framework-description/authentication_witnesses.md) +- Understand [authwit concepts](../foundational-topics/advanced/authwit.md) +- Explore [account abstraction](../foundational-topics/accounts/index.md) diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_private_fee_juice.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_private_fee_juice.md new file mode 100644 index 00000000000..2d3076517a9 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_private_fee_juice.md @@ -0,0 +1,114 @@ +--- +title: Pay Fees Privately +tags: [fees, privacy, fpc] +sidebar_position: 8 +description: Learn how private fee payment works on Aztec and walk through an example using a community-built fully private Fee Payment Contract. +references: ["labs/yarn-project/aztec.js/src/fee/private_fee_payment_method.ts", "labs/noir-projects/noir-contracts/contracts/fees/fpc_contract/*"] +--- + +import { General, Fees } from '@site/src/components/Snippets/general_snippets'; + +This guide explains how private fee payment works on Aztec and walks through a concrete example. A fully private FPC can pay transaction fees without revealing the payer: it has no public functions, no owner, and no offchain agent. Because the contract is fully private, **no onchain deployment transaction is required**. Every app just derives the address deterministically from the class hash and a shared salt, and users interact with it privately. + +To illustrate the pattern, this guide uses [`PrivateFPC`](https://github.com/alejoamiras/ecosystem-tooling/tree/main/packages/private-fee-juice), the community-built implementation from the `private-fee-juice` package (published on npm as `@alejoamiras/private-fee-juice`). You could write your own private FPC following the same design principles. + +## Prerequisites + +- <General.AztecJSPrerequisites /> +- Familiarity with [fee concepts](../foundational-topics/fees.md) and [Paying Fees](./how_to_pay_fees.md) + +:::info +<Fees.FeeAsset_NonTransferrable /> +::: + +## Why a fully private FPC? + +On Aztec, the transaction's setup phase is non-revertible, and a protocol-level allowlist controls which public function calls are permitted during it. Public token functions (like `transfer_in_public` and `_increase_public_balance`) have been removed from the default allowlist; custom FPCs may only call protocol-contract setup functions like those on `AuthRegistry` and `FeeJuice`. The reference [`FPC` contract](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/fees/fpc_contract/src/main.nr) collects user payment during setup by calling those token functions, so its flow is now rejected on public networks. The `PrivateFeePaymentMethod` shipped in `@aztec-labs/aztec.js/fee` (which targets that contract) is therefore deprecated. See the [migration note](../resources/migration_notes.md#custom-token-fpcs-removed-from-default-public-setup-allowlist) for details. + +A fully private FPC side-steps the allowlist entirely. Instead of collecting payment from the user during setup, it holds Fee Juice as its own internal private balance (funded earlier by the user through the Fee Juice portal). When a transaction runs, the FPC just verifies that a **Fee Juice claim nullifier** exists in the nullifier tree (proof that the L1 deposit was consumed on L2) and deducts from its private note-based balance. No public cross-contract token calls happen during setup, so the allowlist never blocks anything. + +## How a private FPC works + +This section describes the design pattern using the `private-fee-juice` package's `PrivateFPC` as an example. The contract stores an internal, note-based `BalanceSet` of Fee Juice per user. There is no constructor, no admin, and no public surface. + +### Two salts, not one + +Two different salt values show up in this flow; it's worth naming them up front so they don't get confused: + +- **Deployment salt.** Used to derive the FPC's contract address. Once a community agrees on the bytecode and this salt, everyone can derive the same address locally without an onchain deployment tx. The convention for the `private-fee-juice` `PrivateFPC` is `Fr.ZERO` (see [Recommended salt](#recommended-salt-0)). +- **Bridge salt.** A random value the user chooses per L1 deposit. Combined with the user's Aztec address, it derives the _bridge secret_ (`secret = poseidon2([salt, claimer], DOM_SEP__FPC_BRIDGE_SECRET)`), whose hash is passed as the `secretHash` on the L1 deposit. Only the user knows the preimage, so only the user can later produce the `secret` that `FeeJuice.claim` requires to consume the L1-to-L2 message. + +`PrivateFPC.mint(amount, salt, leaf_index)` and `PrivateFPC.mint_and_pay_fee(amount, salt, leaf_index)` take the **bridge** salt (along with the leaf index and the user's claimer address, which is `msg_sender`) to reconstruct the Fee Juice claim nullifier and verify the bridge was consumed. + +### Two flows + +1. **Bridge + mint + pay** (run once to seed the user's private Fee Juice balance inside the FPC, and run again each time that balance runs low and the user wants to add more by bridging another deposit from L1): + 1. **L1 deposit.** Call `FeeJuicePortal.depositToAztecPublic(_to = fpcAddress, _amount = amount, _secretHash = computeSecretHash(bridgeSecret))` where `bridgeSecret = poseidon2([bridgeSalt, claimer], DOM_SEP__FPC_BRIDGE_SECRET)`. The FPC is the _recipient_ of the deposit, the user is the _claimer_. + 2. **L2 claim.** In a normal L2 transaction, call `FeeJuice.claim(fpcAddress, amount, bridgeSecret, leafIndex)` directly. This consumes the L1-to-L2 message, credits Fee Juice to the FPC's **public** Fee Juice balance, and emits the claim nullifier. The fee for this transaction is paid by whatever mechanism the user normally uses (their own Fee Juice, `FeeJuicePaymentMethodWithClaim` on a _separate_ bridge they control, the Sponsored FPC on devnet/testnet, and so on). The `PrivateFPC` does _not_ sponsor this call, because at this point the user has no balance with it yet. + 3. **Mint.** In a follow-up L2 transaction, call `PrivateFPC.mint(amount, bridgeSalt, leafIndex)` (again paid by whatever mechanism the user normally uses). `mint` does **not** call `FeeJuice.claim` again, because the claim already happened in step 1.2. The contract recomputes the same nullifier value that the earlier `claim` emitted (possible because the user supplies the `bridgeSalt` that originally produced it), asserts that nullifier exists in the nullifier tree as proof the L1 deposit was consumed, emits its own FPC-scoped nullifier to prevent double-minting the same bridge credit, and credits `amount` to the claimer's private balance inside the FPC. + 4. **Pay.** From that point on, the user can pass `new FPCFeePaymentMethod(fpcAddress)` as the payment method on any transaction. Under the hood, the method calls `PrivateFPC.pay_fee()` in setup, which deducts `max_gas_cost` from the user's private balance and makes the FPC the fee payer. +2. **Cold-start** (single-transaction equivalent of steps 2–4 above, for first-time users who have only done the L1 deposit): + 1. **L1 deposit.** Same as step 1.1 above. + 2. **Single L2 transaction.** Pass `new PrivateMintAndPayFeePaymentMethod(fpcAddress, amount, bridgeSecret, bridgeSalt, leafIndex)` as the payment method on the user's first real transaction. The SDK bundles two calls into the setup phase of that single transaction: + - `FeeJuice.claim(fpcAddress, amount, bridgeSecret, leafIndex)`: consumes the L1-to-L2 message, crediting Fee Juice to the FPC and emitting the claim nullifier (pending within the same tx). + - `PrivateFPC.mint_and_pay_fee(amount, bridgeSalt, leafIndex)`: asserts the (pending) claim nullifier, credits `amount - max_gas_cost` to the user's private balance in the FPC, and marks the FPC as fee payer. + + The bridged amount itself funds this transaction's fee, so the user doesn't need prior Fee Juice or a sponsor to bootstrap. Any remaining credit (`amount - max_gas_cost`) is available for subsequent transactions via `FPCFeePaymentMethod`. + +Cold-start exists for users who have no other way to pay fees: the bridged amount itself funds that very first transaction, but `max_gas_cost` of it is consumed in the process. For top-ups (when the user already has another fee mechanism), the three-step `claim → mint → pay` path is preferable because it credits the full `amount` rather than `amount - max_gas_cost`, and it decouples the L1 bridge from the first app transaction (useful for privacy). For protocol details and the full API surface, see the [SDK README](https://github.com/alejoamiras/ecosystem-tooling/blob/main/packages/private-fee-juice/src/ts/README.md) and [PRD](https://github.com/alejoamiras/ecosystem-tooling/blob/main/packages/private-fee-juice/docs/private-product-requirements.md). + +Because neither `pay_fee` nor `mint_and_pay_fee` makes public cross-contract token calls in setup (they only deduct from the FPC's internal private balance and invoke `set_as_fee_payer`), the [setup-phase allowlist](../foundational-topics/transactions.md#setup-phase-non-revertible) never blocks these flows. + +:::note No refund +`PrivateFPC.pay_fee()` deducts the full `max_gas_cost` and does not refund unused gas. Read `gasUsed` from a simulation (see [Estimate mana costs](./how_to_pay_fees.md#estimate-mana-costs)) to right-size your limits. +::: + +## Share one FPC address across the ecosystem + +Privacy on Aztec comes from indistinguishability. Private calldata and user identities are hidden. What an observer sees of a private call is its onchain *footprint*: the number of nullifiers and note commitments it emits, any logs, and its public gas usage. They do not learn which contract or which function produced those. Any two transactions whose footprints match are indistinguishable, even if they originated from entirely different contracts or functions, so an anonymity set at the private layer can span many unrelated contract–function pairs. + +Fee payments add one extra observable: the transaction's fee payer address, set via `set_as_fee_payer()`, is recorded onchain by the protocol. Every fee paid through a given FPC address is therefore publicly tagged with that address. If your app uses its own copy of the private FPC at a unique address, that tag distinguishes your users' fee payments from everyone else's. If every app derives the *same* FPC address and routes fees through it, every private fee payment in the ecosystem shares the same public fee-payer tag and joins a single, much larger shared set. + +This is the whole point of a fully private FPC. Because you don't have to deploy it on L2, there is no race to "be the deployer": the only thing that matters is that everyone agrees on the address. + +## Recommended salt: `0` + +Two parties derive the same contract address if and only if they use the same compiled artifact and the same deployment salt. For any fully private FPC, using a common salt maximizes the shared privacy set. The community convention for the `private-fee-juice` `PrivateFPC` is `Fr.ZERO`. + +This is a convention, not a protocol-enforced default. It is up to each developer to pass the salt when registering the contract with their PXE, just as they choose any other deployment parameter. Following the convention means your users' private fee payments join the same privacy set as every other app that follows it. + +:::danger Version-specific addresses +The `PrivateFPC` address depends on the compiled contract bytecode. A different Aztec version produces different bytecode and therefore a **different address**. Sending Fee Juice to the wrong address means **unrecoverable loss**. Before using a derived address on a given network, verify the network runs the same Aztec version as the `private-fee-juice` SDK version you have installed. +::: + +## Example: pay fees with `PrivateFPC` + +The SDK exports two payment methods plus a `registerPrivateContract` helper that registers the FPC with your PXE using the shared deployment salt, with no deployment transaction needed: + +- `new FPCFeePaymentMethod(fpcAddress)`: for users who already have a private balance in the FPC. Wraps `PrivateFPC.pay_fee()`. +- `new PrivateMintAndPayFeePaymentMethod(fpcAddress, amount, bridgeSecret, bridgeSalt, leafIndex)`: for cold-start. Bundles `FeeJuice.claim` and `PrivateFPC.mint_and_pay_fee` into the setup phase of a single transaction. + +For installation, the complete bridge-claim-mint-pay flow, required `send()` options (including `additionalScopes` and `gasSettings`), and a runnable end-to-end example, see the [SDK README](https://github.com/alejoamiras/ecosystem-tooling/blob/main/packages/private-fee-juice/src/ts/README.md) and the [integration test](https://github.com/alejoamiras/ecosystem-tooling/blob/main/packages/private-fee-juice/src/ts/test/private.test.ts). + +:::note Transaction behavior +| Scenario | Status | Execution result | Fee paid? | +| --- | --- | --- | --- | +| Private revert | `DROPPED` (not included in block) | N/A | No | +| Public revert | `PROPOSED` | `REVERTED` | Yes (FPC pays) | +| Success | `PROPOSED` | `SUCCESS` | Yes (FPC pays) | +::: + +## Reference implementation + +The `private-fee-juice` package ships detailed documentation for this design and its security properties: + +- [Private FPC Product Requirements](https://github.com/alejoamiras/ecosystem-tooling/blob/main/packages/private-fee-juice/docs/private-product-requirements.md): problem statement, requirements matrix, cryptographic design (secret derivation, nullifier reconstruction, double-spend prevention), and security properties +- [`PrivateFPC` Noir source](https://github.com/alejoamiras/ecosystem-tooling/blob/main/packages/private-fee-juice/src/nr/private_contract/src/main.nr): the contract itself, annotated with the full bridge-to-mint-to-pay flow +- [`src/ts/README.md`](https://github.com/alejoamiras/ecosystem-tooling/blob/main/packages/private-fee-juice/src/ts/README.md): SDK reference with every exported class and utility +- [Integration test `private.test.ts`](https://github.com/alejoamiras/ecosystem-tooling/blob/main/packages/private-fee-juice/src/ts/test/private.test.ts): canonical end-to-end example of the bridge, claim, mint, sponsor flow + +## Next steps + +- Learn about [fee concepts](../foundational-topics/fees.md) in detail +- Review the other [fee payment methods](./how_to_pay_fees.md) available in `aztec.js` +- Browse the [`private-fee-juice`](https://github.com/alejoamiras/ecosystem-tooling/tree/main/packages/private-fee-juice) package for the Noir source, TypeScript SDK, and integration examples diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/index.md b/docs/lines/v6/docs-developers/docs/aztec-js/index.md new file mode 100644 index 00000000000..71ca0bac64b --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/index.md @@ -0,0 +1,53 @@ +--- +title: Aztec.js +sidebar_position: 0 +tags: [aztec.js, javascript, typescript] +description: Complete guide to Aztec.js library for managing accounts and interacting with contracts on the Aztec network, including installation, importing, and core workflow functions. +references: ["labs/yarn-project/aztec.js/src/*", "labs/yarn-project/wallets/src/*", "labs/yarn-project/accounts/src/*"] +--- + +import DocCardList from "@theme/DocCardList"; + +Aztec.js is a library that provides APIs for managing accounts and interacting with contracts on the Aztec network. It communicates with the [Private eXecution Environment (PXE)](../foundational-topics/pxe/index.md) through a `PXE` implementation, allowing developers to easily register new accounts, deploy contracts, view functions, and send transactions. + +## Installing + +```bash +npm install @aztec-labs/aztec.js@#include_version_without_prefix +``` + +## Common Dependencies + +Most applications will need additional packages alongside `@aztec-labs/aztec.js`, e.g.: + +```bash +npm install @aztec-labs/aztec.js@#include_version_without_prefix \ + @aztec-labs/accounts@#include_version_without_prefix \ + @aztec-labs/wallets@#include_version_without_prefix \ + @aztec-labs/noir-contracts.js@#include_version_without_prefix +``` + +| Package | Description | +| -------------------------- | ------------------------------------------------------------- | +| `@aztec-labs/aztec.js` | Core SDK for contracts, transactions, and network interaction | +| `@aztec-labs/accounts` | Account contract implementations (Schnorr, ECDSA) | +| `@aztec-labs/wallets` | Simplified wallets for local development and scripting | +| `@aztec-labs/noir-contracts.js` | Pre-compiled contract interfaces (Token, NFT, etc.) | + +## Package Structure + +`@aztec-labs/aztec.js` uses subpath exports. You must import from specific subpaths rather than the package root: + +```typescript +import { createAztecNodeClient, waitForNode } from "@aztec-labs/aztec.js/node"; +import { Fr } from "@aztec-labs/aztec.js/fields"; +import { AztecAddress } from "@aztec-labs/aztec.js/addresses"; +``` + +## AI-Friendly Reference + +The [TypeScript API reference](./typescript_api_reference.mdx) links to markdown interface files for common packages for easy use with AI coding assistants. Copy relevant sections to give your AI tool accurate context about Aztec.js APIs. + +## Guides + +<DocCardList /> diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/typescript_api_reference.mdx b/docs/lines/v6/docs-developers/docs/aztec-js/typescript_api_reference.mdx new file mode 100644 index 00000000000..b41066662f2 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-js/typescript_api_reference.mdx @@ -0,0 +1,137 @@ +--- +title: TypeScript API Reference +description: API reference documentation for Aztec TypeScript packages including aztec.js, accounts, PXE, and core libraries. +references: ["labs/yarn-project/aztec.js/src/*", "labs/yarn-project/accounts/src/*", "labs/yarn-project/pxe/src/*", "labs/yarn-project/wallets/src/*", "labs/yarn-project/wallet-sdk/src/*", "labs/yarn-project/entrypoints/src/*"] +--- + +import { useState } from "react"; + +export const CopyIcon = ({ path }) => { + const [status, setStatus] = useState("idle"); + + const handleCopy = async (e) => { + e.preventDefault(); + setStatus("loading"); + try { + const response = await fetch(path); + if (!response.ok) throw new Error("Failed to fetch"); + const text = await response.text(); + await navigator.clipboard.writeText(text); + setStatus("copied"); + setTimeout(() => setStatus("idle"), 2000); + } catch (error) { + setStatus("error"); + setTimeout(() => setStatus("idle"), 2000); + } + }; + + const buttonStyle = { + padding: "2px", + marginLeft: "6px", + cursor: status === "loading" ? "wait" : "pointer", + border: "none", + borderRadius: "3px", + background: "transparent", + color: status === "copied" ? "var(--ifm-color-success)" : + status === "error" ? "var(--ifm-color-danger)" : + "var(--ifm-color-emphasis-600)", + transition: "color 0.2s ease", + verticalAlign: "middle", + display: "inline-flex", + alignItems: "center", + }; + + const iconStyle = { + width: "14px", + height: "14px", + }; + + const copyIcon = ( + <svg style={iconStyle} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> + </svg> + ); + + const checkIcon = ( + <svg style={iconStyle} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> + <polyline points="20 6 9 17 4 12"></polyline> + </svg> + ); + + const errorIcon = ( + <svg style={iconStyle} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> + <line x1="18" y1="6" x2="6" y2="18"></line> + <line x1="6" y1="6" x2="18" y2="18"></line> + </svg> + ); + + const icon = status === "copied" ? checkIcon : status === "error" ? errorIcon : copyIcon; + const title = status === "copied" ? "Copied!" : status === "error" ? "Failed to copy" : "Copy contents"; + + return ( + <button onClick={handleCopy} style={buttonStyle} disabled={status === "loading"} title={title} aria-label={title}> + {status === "loading" ? "..." : icon} + </button> + ); +}; + +This section provides API reference documentation for the Aztec TypeScript packages. These packages enable developers to build applications on Aztec, from simple contract interactions to complex privacy-preserving protocols. + +## Package Categories + +### Client SDKs + +Packages for building Aztec applications: + +| Package | Description | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| **@aztec-labs/aztec.js** | Main SDK for building Aztec applications. Provides contract deployment, transaction creation, and account management. | +| **@aztec-labs/accounts** | Sample account contract implementations including ECDSA and Schnorr accounts. | +| **@aztec-labs/pxe** | Private eXecution Environment client library for orchestrating private transaction execution and proving. | +| **@aztec-labs/wallet-sdk** | Wallet SDK for browser and extension integrations. | +| **@aztec-labs/wallets** | Embedded wallet for browser and Node.js environments. | +| **@aztec-labs/entrypoints** | Transaction entrypoint implementations for account abstraction. | + +### Core Libraries + +Foundational types and utilities used across the Aztec stack: + +| Package | Description | +| -------------------- | -------------------------------------------------------------------------------------- | +| **@aztec-labs/stdlib** | Protocol-level types including transactions, blocks, proofs, and kernel circuit types. | +| **@aztec-labs/foundation** | Low-level utilities including crypto primitives, serialization, and async helpers. | +| **@aztec-labs/constants** | Protocol constants shared between TypeScript and Noir circuits. | + +:::note +Common types like `Fr`, `AztecAddress`, and `EthAddress` are re-exported through `@aztec-labs/aztec.js` subpaths (e.g., `@aztec-labs/aztec.js/fields`, `@aztec-labs/aztec.js/addresses`). Most developers won't need to import from `@aztec-labs/stdlib` directly. +::: + +## LLM-Optimized Documentation + +For LLM consumption, we provide machine-readable documentation in multiple formats: + +- **[llms.txt](pathname:///llms.txt)** <CopyIcon path="/llms.txt" /> - Full documentation optimized for LLM context +- [**LLM Summary**](pathname:///typescript-api/#api_ref_version/llm-summary.txt) <CopyIcon path="/typescript-api/#api_ref_version/llm-summary.txt" /> - Human-readable API summary + +### Markdown API Files + +The following markdown files are available for LLM context inclusion at `/typescript-api/#api_ref_version/`: + +| File | Description | +| --------------------------------------- | ----------------------------------------------- | +| [`llm-summary.txt`](pathname:///typescript-api/#api_ref_version/llm-summary.txt) <CopyIcon path="/typescript-api/#api_ref_version/llm-summary.txt" /> | Human-readable summary with package overview | +| [`aztec.js.md`](pathname:///typescript-api/#api_ref_version/aztec.js.md) <CopyIcon path="/typescript-api/#api_ref_version/aztec.js.md" /> | Main SDK - contracts, transactions, accounts | +| [`accounts.md`](pathname:///typescript-api/#api_ref_version/accounts.md) <CopyIcon path="/typescript-api/#api_ref_version/accounts.md" /> | Account implementations (ECDSA, Schnorr) | +| [`pxe.md`](pathname:///typescript-api/#api_ref_version/pxe.md) <CopyIcon path="/typescript-api/#api_ref_version/pxe.md" /> | Private execution environment client | +| [`wallet-sdk.md`](pathname:///typescript-api/#api_ref_version/wallet-sdk.md) <CopyIcon path="/typescript-api/#api_ref_version/wallet-sdk.md" /> | Browser/extension wallet integration | +| [`wallets.md`](pathname:///typescript-api/#api_ref_version/wallets.md) <CopyIcon path="/typescript-api/#api_ref_version/wallets.md" /> | Embedded wallet for browser and Node.js | +| [`entrypoints.md`](pathname:///typescript-api/#api_ref_version/entrypoints.md) <CopyIcon path="/typescript-api/#api_ref_version/entrypoints.md" /> | Transaction entrypoints for account abstraction | +| [`stdlib.md`](pathname:///typescript-api/#api_ref_version/stdlib.md) <CopyIcon path="/typescript-api/#api_ref_version/stdlib.md" /> | Protocol types (transactions, blocks, proofs) | +| [`foundation.md`](pathname:///typescript-api/#api_ref_version/foundation.md) <CopyIcon path="/typescript-api/#api_ref_version/foundation.md" /> | Low-level utilities (crypto, serialization) | +| [`constants.md`](pathname:///typescript-api/#api_ref_version/constants.md) <CopyIcon path="/typescript-api/#api_ref_version/constants.md" /> | Protocol constants for circuits | + +## Related Resources + +- [Aztec.js Getting Started](../tutorials/js_tutorials/aztecjs-getting-started.md) +- [GitHub: aztec-packages](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/yarn-project) diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/_category_.json b/docs/lines/v6/docs-developers/docs/aztec-nr/_category_.json new file mode 100644 index 00000000000..aa2589a597d --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 2, + "collapsible": true, + "collapsed": true, + "label": "Aztec.nr" +} diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/api.mdx b/docs/lines/v6/docs-developers/docs/aztec-nr/api.mdx new file mode 100644 index 00000000000..d31bd9e0b0f --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/api.mdx @@ -0,0 +1,81 @@ +--- +title: Aztec.nr API Reference +sidebar_position: 100 +description: Auto-generated API reference documentation for the Aztec.nr smart contract framework. +aztec_nr_api_version: next +--- + +import Link from "@docusaurus/Link"; +import { useActiveVersion } from "@docusaurus/plugin-content-docs/client"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import { aztecNrApiFolder } from "@site/src/utils/aztecNrApi.mjs"; + +export const useApiVersion = () => { + const { siteConfig } = useDocusaurusContext(); + return aztecNrApiFolder(useActiveVersion("developer"), siteConfig.customFields.aztecNrApiFolders); +}; + +export const ModuleLink = ({ path, children }) => { + const apiVersion = useApiVersion(); + const href = `/aztec-nr-api/${apiVersion}/${path}/index.html`; + return ( + <a href={href} target="_blank" rel="noopener noreferrer"> + {children} + </a> + ); +}; + +# Aztec.nr API Reference + +The Aztec.nr API reference documentation is auto-generated from the source code using `nargo doc`. + +## View the API Documentation + +<ModuleLink path="noir_aztec">**Aztec.nr**</ModuleLink> + +The API reference includes documentation for all public modules, functions, structs, and types in the aztec-nr workspace: + +### Core Crates + +- <ModuleLink path="noir_aztec">**noir_aztec**</ModuleLink> - Core Aztec contract framework including: + - <ModuleLink path="noir_aztec/context">`context`</ModuleLink> - Private and public execution contexts + - <ModuleLink path="noir_aztec/state_vars">`state_vars`</ModuleLink> - State variable types (PrivateMutable, PublicMutable, Map, etc.) + - <ModuleLink path="noir_aztec/note">`note`</ModuleLink> - Note interfaces and utilities + - <ModuleLink path="noir_aztec/authwit">`authwit`</ModuleLink> - Authentication witness support + - <ModuleLink path="noir_aztec/history">`history`</ModuleLink> - Historical state proofs + - <ModuleLink path="noir_aztec/messages">`messages`</ModuleLink> - Cross-chain messaging + - <ModuleLink path="noir_aztec/oracle">`oracle`</ModuleLink> - Oracle interfaces + - <ModuleLink path="noir_aztec/macros">`macros`</ModuleLink> - Contract macros and attributes + - <ModuleLink path="noir_aztec/hash">`hash`</ModuleLink> - Hash functions and utilities + - <ModuleLink path="noir_aztec/keys">`keys`</ModuleLink> - Key management utilities + - <ModuleLink path="noir_aztec/event">`event`</ModuleLink> - Event emission and interfaces + - <ModuleLink path="noir_aztec/test">`test`</ModuleLink> - Testing utilities + - <ModuleLink path="noir_aztec/utils">`utils`</ModuleLink> - General utilities + +### Note Types + +- <ModuleLink path="address_note">**address_note**</ModuleLink> - Note type for storing + Aztec addresses +- <ModuleLink path="field_note">**field_note**</ModuleLink> - Note type for storing + a single Field value +- <ModuleLink path="uint_note">**uint_note**</ModuleLink> - Note type for storing + unsigned integers +- <ModuleLink path="ecdsa_public_key_note">**ecdsa_public_key_note**</ModuleLink> - Note type for + storing an ECDSA public key + +### State Variables + +- <ModuleLink path="balance_set">**balance_set**</ModuleLink> - State variable for + managing private balances + +### Utilities + +- <ModuleLink path="compressed_string">**compressed_string**</ModuleLink> - Compressed + string utilities for efficient storage + +### Protocol Contract Interfaces + +- <ModuleLink path="contract_instance_registry_interface">**contract_instance_registry_interface**</ModuleLink> - + Interface for calling the `ContractInstanceRegistry` protocol contract +- <ModuleLink path="fee_juice_interface">**fee_juice_interface**</ModuleLink> - Interface for calling + the `FeeJuice` protocol contract diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/compiling_contracts.md b/docs/lines/v6/docs-developers/docs/aztec-nr/compiling_contracts.md new file mode 100644 index 00000000000..a7f420d8b11 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/compiling_contracts.md @@ -0,0 +1,76 @@ +--- +title: Compiling Contracts +tags: [contracts] +sidebar_position: 2 +description: Compile your Aztec smart contracts into deployable artifacts using aztec command. +references: ["labs/yarn-project/aztec/src/cli/cmds/compile.ts", "labs/yarn-project/aztec/scripts/aztec.sh", "labs/noir-projects/aztec-nr/aztec/src/macros/calls_generation/*"] +--- + +This guide shows you how to compile your Aztec contracts into artifacts ready for deployment and interaction. + +## Prerequisites + +- An Aztec contract written in Aztec.nr +- `aztec` installed +- Contract project with proper `Nargo.toml` configuration + +## Compile your contract + +Compile your Noir contracts to generate JSON artifacts: + +```bash +aztec compile +``` + +This outputs contract artifacts to the `target` folder. + +## Use generated interfaces + +The compiler automatically generates type-safe interfaces for contract interaction. + +### Import and use contract interfaces + +Use generated interfaces instead of manual function calls: + +```rust +contract MyContract { + use token::Token; + + #[external("private")] + fn transfer_tokens(token_address: AztecAddress, recipient: AztecAddress, amount: u128) { + // Use the generated Token interface to call another contract + self.call(Token::at(token_address).transfer(recipient, amount)); + } + + #[external("private")] + fn transfer_then_mint(token_address: AztecAddress, recipient: AztecAddress, amount: u128) { + // Private call executed immediately + self.call(Token::at(token_address).transfer(recipient, amount)); + + // Public call enqueued for later execution + self.enqueue(Token::at(token_address).mint_to_public(recipient, amount)); + } +} +``` + +:::warning +Do not import generated interfaces from the same project as the source contract to avoid circular references. +::: + +## Test your contract + +Run your contract's tests with the `aztec` CLI, not `nargo test`: + +```bash +aztec test +``` + +`aztec test` compiles the workspace first (a no-op when nothing changed), starts the TXE test environment that `TestEnvironment` connects to, and runs the workspace's `#[test]` functions through `nargo test`. To write tests, see [Testing contracts](./testing_contracts.md). + +## Next steps + +After compilation, use the generated artifacts to: + +- Deploy contracts with the `Contract` class from `aztec.js` +- Interact with deployed contracts using type-safe interfaces +- Import contracts in other Aztec.nr projects diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/contract_readiness_states.md b/docs/lines/v6/docs-developers/docs/aztec-nr/contract_readiness_states.md new file mode 100644 index 00000000000..59d43502914 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/contract_readiness_states.md @@ -0,0 +1,85 @@ +--- +title: Contract Deployment Reference +sidebar_position: 3 +tags: [contracts, deployment, initialization] +description: A practical guide to determine which deployment steps your Aztec contract needs and when functions become callable. +references: ["labs/yarn-project/aztec.js/src/deployment/*", "labs/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr"] +--- + +This guide helps you quickly determine which deployment steps your contract needs. For conceptual background on how contract deployment works, see [Contract Deployment](../foundational-topics/contract_creation.md). + +## What Do I Need to Do? + +Use this decision tree to determine which steps your contract needs. + +```mermaid +%%{init: {'themeVariables': {'fontSize': '10px'}}}%% +flowchart TD + Start([I want to call a function on my contract]) --> HasPublic{Does your contract<br/>have public functions?} + + HasPublic -->|Yes| WantPublic{Do you want to call<br/>a public function?} + HasPublic -->|No| PrivateOnly[No class registration or<br/>public deployment needed!] + + WantPublic -->|Yes| InstanceDeployed{Is the instance<br/>publicly deployed?} + WantPublic -->|No| WantPrivate{Do you want to call<br/>a private function?} + + InstanceDeployed -->|Yes| CheckInit{Is the contract<br/>initialized?} + InstanceDeployed -->|No| ClassRegistered{Is the class<br/>registered?} + + ClassRegistered -->|Yes| NeedInstance[Register the instance via<br/>ContractInstanceRegistry] + ClassRegistered -->|No| NeedClass[Register the class via<br/>ContractClassRegistry first] + NeedClass --> NeedInstance + NeedInstance --> CheckInit + + WantPrivate -->|Yes| CheckInit + PrivateOnly --> CheckInit + + CheckInit -->|Yes| Ready([Ready to call your function]) + CheckInit -->|No| HasInitializer{"Does your function have<br/>noinitcheck?"} + + HasInitializer -->|Yes| ReadyNoInit([Call it! No init check needed]) + HasInitializer -->|No| MustInit[Initialize the contract first] + MustInit --> Ready +``` + +:::tip No initializer? +If your contract has no `#[initializer]` function and was deployed with `without_initializer()`, it's considered initialized immediately. Skip the initialization checks above. +::: + +## Checking Contract State Programmatically + +Use `wallet.getContractMetadata(contractAddress)` to check whether a contract is registered, published, and initialized. See [Verify deployment](../aztec-js/how_to_deploy_contract.md#verify-deployment) for usage examples and details on what the PXE checks automatically versus what you need to verify manually. + +## When Can You Skip States? + +| Contract Type | Class Registration | Instance Creation | Initialization | Public Deployment | +| ------------------------- | ------------------ | ----------------- | -------------- | ----------------- | +| Private-only | Optional | Required | Depends | Skip | +| Public-only | Required | Required | Depends | Required | +| Hybrid (private + public) | Required | Required | Depends | Required | +| Stateless helper | Optional | Required | Skip | Depends | + +"Depends" means it depends on whether your contract has a constructor marked with `#[initializer]`. + +## When Functions Become Callable + +| State | Private Functions | Public Functions | +| ----------------------------------- | --------------------- | ---------------- | +| Address computed only | With `#[noinitcheck]` | No | +| Class registered | With `#[noinitcheck]` | No | +| Instance deployed (not initialized) | With `#[noinitcheck]` | No | +| Initialized | Yes | No | +| Publicly deployed | Yes | Yes | + +Private functions marked with `#[noinitcheck]` can be called as soon as you know the address, even before initialization. This enables patterns like pre-funded accounts. + +:::note Contracts without initializers +If your contract has no initializer and is deployed with `without_initializer()`, it's considered initialized immediately. Private functions are callable right after instance creation without needing `#[noinitcheck]`. Public functions still require public deployment. +::: + +## Further Reading + +- [Contract Deployment](../foundational-topics/contract_creation.md) - Conceptual foundation of classes, instances, and lifecycle states +- [Deploying Contracts](../aztec-js/how_to_deploy_contract.md) - TypeScript deployment guide +- [Defining Initializer Functions](./framework-description/functions/how_to_define_functions.md#define-initializer-functions) - How to use `#[initializer]` and `#[noinitcheck]` +- [Communicating Cross-Chain](./framework-description/ethereum_aztec_messaging.md) - Portal contracts and L1/L2 messaging diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/debugging.md b/docs/lines/v6/docs-developers/docs/aztec-nr/debugging.md new file mode 100644 index 00000000000..39e6e388664 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/debugging.md @@ -0,0 +1,196 @@ +--- +title: Debugging Aztec Code +sidebar_position: 5 +tags: [debugging, errors, local_network, aztec.nr] +description: This guide shows you how to debug issues in your Aztec contracts. +references: ["labs/noir-projects/aztec-nr/aztec/src/logging.nr"] +--- + +This guide shows you how to debug issues in your Aztec development environment. + +## Prerequisites + +- Running Aztec local network +- Aztec.nr contract or aztec.js application +- Basic understanding of Aztec architecture + +## Enable logging + +For adding log statements to your contracts, controlling log verbosity, and understanding the `LOG_LEVEL` syntax, see the [Logging from Contracts](./logging.md) guide. + +To enable verbose system-level logging on a local network: + +```bash +LOG_LEVEL=verbose aztec start --local-network +``` + +## Debugging common errors + +### Contract Errors + +| Error | Solution | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Aztec dependency not found` | Add to Nargo.toml: `aztec = { git="https://github.com/aztec-labs-eng/aztec-node/", tag="#include_aztec_version", directory="noir-projects/aztec-nr/aztec" }` | +| `Public state writes only supported in public functions` | Move state writes to public functions | +| `Unknown contract 0x0` | Call `wallet.registerContract(...)` to register contract | +| `No public key registered for address` | Call `wallet.registerSender(...)` | +| `Direct invocation of ... functions is not supported` | Use `self.call()`, `self.view()`, or `self.enqueue()` to [call contract functions](framework-description/calling_contracts.md) | +| `Failed to solve brillig function` | Check function parameters and note validity | +| `Cross-contract utility call denied` | Configure an `authorizeUtilityCall` [execution hook](#cross-contract-utility-call-denied) on your PXE | + +#### Cross-contract utility call denied + +Utility functions execute on the user's device and have access to private state. A cross-contract utility call made by +a malicious or compromised contract could leak private information to an untrusted contract. PXE therefore denies cross- +contract utility calls by default and requires explicit authorization via an execution hook. Calls to standard contracts +(such as the HandshakeRegistry, which is queried during every contract's sync) are always automatically authorized. + +When a contract executes a utility function that calls into a different contract, PXE asks the wallet through an [execution hook](../foundational-topics/pxe/execution_hooks.md) whether the call should be allowed. If no hook is configured, or the wallet denies the request, you will see: + +``` +Cross-contract utility call denied: <reason>. <caller> attempted to call <target>:<selector> (<name>). +``` + +See [execution hooks](../foundational-topics/pxe/execution_hooks.md#authorizeutilitycall) for how to authorize calls, both in production and in Noir tests. + +### Circuit Errors + +| Error Code | Meaning | Fix | +| ----------- | ---------------------------- | -------------------------------------------------- | +| `2002` | Invalid contract address | Ensure contract is deployed and address is correct | +| `2005/2006` | Static call violations | Remove state modifications from static calls | +| `2017` | User intent mismatch | Verify transaction parameters match function call | +| `3001` | Unsupported operation | Check if operation is supported in current context | +| `3005` | Non-empty private call stack | Ensure private functions complete before public | +| `4007/4008` | Chain ID/version mismatch | Verify L1 chain ID and Aztec version | +| `7008` | Membership check failed | Ensure using valid historical state | +| `7009` | Array overflow | Reduce number of operations in transaction | + +### Quick Fixes for Common Issues + +```bash +# Archiver sync issues - force progress with dummy transactions. +# Assumes you have imported the local network test accounts +# (aztec-wallet import-test-accounts) and have a deployed token +# aliased as `testtoken`. +aztec-wallet send transfer --from test0 --contract-address testtoken --args accounts:test0 0 +aztec-wallet send transfer --from test0 --contract-address testtoken --args accounts:test0 0 + +# L1 to L2 message pending - wait for inclusion +# Messages need 2 blocks to be processed +``` + +## Debugging WASM errors + +### Enable debug WASM + +```javascript +// In vite.config.ts or similar +export default { + define: { + "process.env.BB_WASM_PATH": JSON.stringify("https://debug.wasm.url"), + }, +}; +``` + +### Profile transactions + +```javascript +import { serializePrivateExecutionSteps } from "@aztec-labs/stdlib"; + +// Profile the transaction +const profileTx = await contract.methods + .myMethod(param1, param2) + .profile({ profileMode: "execution-steps" }); + +// Serialize for debugging +const ivcMessagePack = serializePrivateExecutionSteps(profileTx.executionSteps); + +// Download debug file +const blob = new Blob([ivcMessagePack]); +const url = URL.createObjectURL(blob); +const link = document.createElement("a"); +link.href = url; +link.download = "debug-steps.msgpack"; +link.click(); +``` + +⚠️ **Warning:** Debug files may contain private data. Use only in development. + +## Interpret error messages + +### Circuit and protocol errors + +- **Private kernel errors (2xxx)**: Issues with private function execution +- **Public kernel errors (3xxx)**: Issues with public function execution +- **Rollup errors (4xxx)**: Block production issues +- **Generic errors (7xxx)**: Resource limits or state validation + +### Transaction limits + +Current limits that trigger `7009 - ARRAY_OVERFLOW`: + +- Max new notes per tx: Check `MAX_NOTE_HASHES_PER_TX` +- Max nullifiers per tx: Check `MAX_NULLIFIERS_PER_TX` +- Max function calls: Check call stack size limits +- Max L2→L1 messages: Check message limits + +## Debugging sequencer issues + +### Common sequencer errors + +| Error | Cause | Solution | +| ------------------------------------ | --------------------- | ------------------------------------------ | +| `tree root mismatch` | State inconsistency | Restart local network or check state transitions | +| `next available leaf index mismatch` | Tree corruption | Verify tree updates are sequential | +| `Public call stack size exceeded` | Too many public calls | Reduce public function calls | +| `Failed to publish block` | L1 submission failed | Check L1 connection and gas | + +## Reporting issues + +When debugging fails: + +1. Collect error messages and codes +2. Generate transaction profile (if applicable) +3. Note your environment setup +4. Create issue at [aztec-packages](https://github.com/AztecProtocol/aztec-packages/issues/new) + +## Quick reference + +### Enable verbose logging + +```bash +LOG_LEVEL=verbose aztec start --local-network +``` + +### Contract logging + +See the full [Logging from Contracts](./logging.md) guide for all available log functions and `LOG_LEVEL` configuration. + +```rust +use aztec::oracle::logging::{debug_log, debug_log_format}; +``` + +### Check contract registration + +```javascript +await wallet.getContractMetadata(myContractInstance.address); +``` + +### Decode L1 errors + +Check hex errors against [Errors.sol](https://github.com/AztecProtocol/aztec-packages/blob/#include_foundation_version/l1-contracts/src/core/libraries/Errors.sol) + +## Tips + +- Always check logs before diving into circuit errors +- State-related errors often indicate timing issues +- Array overflow errors mean you hit transaction limits +- Use debug WASM for detailed stack traces +- Profile transactions when errors are unclear + +## Next steps + +- [Circuit Architecture](../foundational-topics/advanced/circuits/index.md) +- [Call Types](../foundational-topics/call_types.md) +- [Aztec.nr Dependencies](./framework-description/dependencies.md) diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/_category_.json b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/_category_.json new file mode 100644 index 00000000000..3eac7b6e0a9 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 4, + "collapsible": true, + "collapsed": true, + "label": "Framework Description" +} diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/_category_.json b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/_category_.json new file mode 100644 index 00000000000..ac511433849 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 15, + "collapsible": true, + "collapsed": true, + "label": "Advanced Topics" +} diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_profile_transactions.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_profile_transactions.md new file mode 100644 index 00000000000..008aab77ad6 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_profile_transactions.md @@ -0,0 +1,218 @@ +--- +title: Profiling Transactions +sidebar_position: 2 +tags: [contracts, profiling] +description: How to profile Aztec transactions and identify performance bottlenecks using aztec profile, aztec-wallet, and aztec.js. +references: ["labs/yarn-project/aztec/src/cli/cmds/profile_gates.ts", "labs/yarn-project/aztec/src/cli/cmds/profile_flamegraph.ts", "labs/yarn-project/cli-wallet/src/cmds/profile.ts", "labs/yarn-project/aztec.js/src/contract/contract_function_interaction.ts"] +--- + +This guide shows you how to profile Aztec transactions to understand gate counts and identify optimization opportunities. + +## Prerequisites + +- `aztec` command installed ([see installation](../../../../getting_started_on_local_network.md)) +- Aztec contract compiled (`aztec compile`) +- Basic understanding of proving and gate counts + +## Choosing a profiling tool + +Aztec provides three ways to profile. Each serves a different purpose: + +| Tool | What it measures | Needs deployment? | When to use | +| ---- | ---------------- | ----------------- | ----------- | +| `aztec profile gates` | Per-function gate counts | No | Quick check of individual function costs after compiling | +| `aztec profile flamegraph` | Per-function flamegraph SVG | No | Deep-dive into where gates come from inside a function | +| `aztec-wallet profile` / `.profile()` in aztec.js | Full transaction gate count including all kernel circuits | Yes* | Understanding the true cost of a transaction end-to-end | + +\* `aztec-wallet profile` and `ContractFunctionInteraction.profile()` require a deployed contract. However, `DeployMethod.profile()` in aztec.js can profile deployment transactions before the contract exists. + +In most cases, start with `aztec profile gates` for a quick overview, then use the full transaction profiling tools when you need to understand kernel overhead. + +## Quick profiling with `aztec profile` + +These commands work on compiled artifacts directly — no deployment or running network required. + +### Gate counts + +```bash +# Compile your contract +aztec compile + +# Get gate counts for all functions +aztec profile gates ./target +``` + +Example output: + +```text +Gate counts: +──────────────────────────────────────────────────────────────────── +my_contract-MyContract::constructor 5,200 +my_contract-MyContract::my_function 14,832 +my_contract-MyContract::transfer 31,559 +──────────────────────────────────────────────────────────────────── +Total: 3 circuit(s) +``` + +These are the gate counts for your contract functions alone, **without** kernel circuit overhead. See [Understanding kernel overhead](#understanding-kernel-overhead) for how this translates to total transaction cost. + +:::note BB binary +`aztec profile` needs the Barretenberg (`bb`) backend binary. It is auto-detected from the `@aztec-foundation/bb.js` package. If auto-detection fails, set the `BB` environment variable: + +```bash +BB=/path/to/bb aztec profile gates ./target +``` +::: + +:::tip Machine-readable output +For build automation, use `--json` to emit gate counts as a JSON array. Each entry has `name`, `type` (`contract-function` or `program`), and `gates`: + +```bash +aztec profile gates --json ./target +``` +::: + +### Flamegraphs + +To generate an interactive flamegraph SVG for a specific function: + +```bash +aztec profile flamegraph ./target/my_contract-MyContract.json my_function +``` + +This outputs a file like `my_contract-MyContract-my_function-flamegraph.svg` in the same directory. Open it in a browser for an interactive view where: + +- **Width** represents gate count +- **Height** represents call stack depth +- **Wide sections** indicate optimization targets + +:::tip +If `noir-profiler` is not on your PATH, set the `PROFILER_PATH` environment variable: + +```bash +PROFILER_PATH=/path/to/noir-profiler aztec profile flamegraph ./target/my_contract-MyContract.json my_function +``` +::: + +## Full transaction profiling + +The tools above measure individual function gate counts. To understand the **total** proving cost of a transaction — including account entrypoints and kernel circuits — use `aztec-wallet profile` or `.profile()` in aztec.js. These require a running network and, in most cases, a deployed contract. The exception is `DeployMethod.profile()` in aztec.js, which can profile deployment transactions before the contract exists. + +### Profile with aztec-wallet + +Use the `profile` command instead of `send` to get detailed gate counts: + +```bash +# Import test accounts +aztec-wallet import-test-accounts + +# Deploy your contract +aztec-wallet deploy MyContractArtifact \ + --from accounts:test0 \ + --args [CONSTRUCTOR_ARGS] \ + -a mycontract + +# Profile a function call +aztec-wallet profile my_function \ + -ca mycontract \ + --args [FUNCTION_ARGS] \ + -f accounts:test0 +``` + +#### Reading the output + +The profile command outputs a per-circuit breakdown: + +```text +Per circuit breakdown: + + Function name Time Gates Subtotal +-------------------------------------------------------------------------------- + - SchnorrAccount:entrypoint 12.34ms 21,724 21,724 + - private_kernel_init 23.45ms 45,351 67,075 + - MyContract:my_function 15.67ms 31,559 98,634 + - private_kernel_inner 34.56ms 78,452 177,086 + +Total gates: 177,086 (Biggest circuit: private_kernel_inner -> 78,452) +``` + +Key metrics: + +- **Gates**: Circuit complexity for each step +- **Subtotal**: Accumulated gate count +- **Time**: Execution time per circuit + +Notice that the kernel circuits (`private_kernel_init`, `private_kernel_inner`) appear alongside your contract functions. These are protocol overhead — see [Understanding kernel overhead](#understanding-kernel-overhead). + +### Profile with aztec.js + +```typescript +const result = await contract.methods.my_function(args).profile({ + from: walletAddress, + profileMode: "full", + skipProofGeneration: true, +}); + +// Access gate counts from execution steps +for (const step of result.executionSteps) { + console.log(`${step.functionName}: ${step.gateCount} gates`); +} + +// Access timing information +console.log("Total time:", result.stats.timings.total, "ms"); +``` + +#### Profile modes + +- `gates`: Gate counts per circuit +- `execution-steps`: Detailed execution trace with bytecode and witnesses +- `full`: Complete profiling information (gates + execution steps) + +Set `skipProofGeneration: true` for faster iteration when you only need gate counts. + +## Generate flamegraphs with noir-profiler + +For deeper analysis of individual contract functions beyond what `aztec profile flamegraph` provides, you can use the Noir profiler directly. The profiler is installed automatically with Nargo (starting noirup v0.1.4). + +```bash +# Compile your contract first +aztec compile + +# Generate a gates flamegraph (requires bb backend) +noir-profiler gates \ + --artifact-path ./target/my_contract-MyContract.json \ + --backend-path bb \ + --output ./target + +# Generate an ACIR opcodes flamegraph +noir-profiler opcodes \ + --artifact-path ./target/my_contract-MyContract.json \ + --output ./target +``` + +For detailed usage, see the [Noir profiler documentation](https://noir-lang.org/docs/tooling/profiler). + +## Understanding kernel overhead + +When you profile a full transaction, you'll see kernel circuits alongside your contract functions. These are protocol overhead — the private kernel runs once per private function call in the transaction. Even a typical transaction calling a single contract function involves two private calls (the account entrypoint + your function), totaling ~427k gates of which only ~14k are your function. + +For a detailed breakdown of kernel phases and their gate costs, see [Private Kernel Circuit - Performance Impact](../../../foundational-topics/advanced/circuits/private_kernel.md#performance-impact). + +## Gate count guidelines + +These are rough guidelines for a **single contract function's** gate count (i.e. what `aztec profile gates` reports). A typical transaction (e.g. a token transfer) totals ~500,000 gates across all circuits including kernel overhead, so use that as a reference point. + +| Gate Count | Assessment | +| ----------------- | ---------------------------- | +| < 50,000 | Excellent | +| 50,000 - 200,000 | Good | +| 200,000 - 500,000 | Consider optimizing | +| > 500,000 | Worth optimizing if possible | + +Note that a high gate count does **not** prevent transaction inclusion — it only affects client-side proving time. See [Private Kernel Circuit - Performance Impact](../../../foundational-topics/advanced/circuits/private_kernel.md#performance-impact) for details. + +## Next steps + +- [Writing efficient contracts](./writing_efficient_contracts.md) - optimization strategies and examples +- [Transaction lifecycle](../../../foundational-topics/transactions.md) +- [Testing contracts](../../testing_contracts.md) diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md new file mode 100644 index 00000000000..f9561124273 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md @@ -0,0 +1,154 @@ +--- +title: Proving historic state +sidebar_position: 15 +tags: [contracts] +description: Prove historical notes, nullifiers, contract deployment, and public storage in your Aztec smart contracts. +references: ["labs/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr"] +--- + +This guide shows you how to prove facts about Aztec's historical state from inside a private function: that a note existed, that a nullifier was or wasn't present, that a contract was deployed, or what a public storage slot held at a past block. + +Each proof is a Merkle membership (or non-membership) proof against a state tree root committed in a past block header: the note hash tree, the nullifier tree, or the public data tree. (The Archive tree, by contrast, holds a hash of each block header and is what proves a header is canonical.) Private functions always read from a past block header rather than the live chain tip, because the chain advances while a proof is generated on your device. The proofs therefore tell you what was true once every transaction in the chosen block had executed. + +## Prerequisites + +- An Aztec contract project set up +- Understanding of Aztec's note and nullifier system + +## What you can prove + +You can create proofs for these elements at any past block height: + +- **Note inclusion** - prove a note existed in the note hash tree +- **Note validity** - prove a note existed and wasn't nullified at a specific block +- **Nullifier inclusion/non-inclusion** - prove a nullifier was or wasn't in the nullifier tree +- **Contract deployment** - prove a contract's bytecode was published or initialized +- **Public storage** - read the value a public storage slot held at a past block + +Common use cases: +- Verify ownership of an asset from another contract without revealing which specific note +- Prove eligibility based on historical state (e.g., "owned tokens at block X") +- Claim rewards based on past contributions (see the [claim contract](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr) for a complete example) + +## Choosing a block header + +Every function below takes a `BlockHeader`. Two getters on the private context provide one: + +- `self.context.get_anchor_block_header()` returns the transaction's anchor block header. The protocol's circuits verify it once per transaction, so using it adds no extra constraints to your function. The protocol requires a transaction to expire within 24 hours of its anchor block, so the anchor is always a block from the last 24 hours. +- `self.context.get_block_header_at(block_number)` returns the header of any block at or before the anchor block, letting you prove against older state at the cost of extra constraints (see [Prove at a specific historical block](#prove-at-a-specific-historical-block)). + +:::warning Data availability +Producing these proofs requires the historical state trees (note hashes, nullifiers, public storage) for the chosen block. Many nodes prune this data after a few hours, so proving against older blocks requires a node that retains it, such as an archive node. +::: + +## Prove note inclusion + +Import the function: + +#include_code history_import labs/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr rust + +Prove a note exists in the note hash tree: + +#include_code prove_note_inclusion labs/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr rust + +## Prove note validity + +To prove a note was valid (existed AND wasn't nullified) at a historical block: + +```rust +use aztec::history::note::assert_local_note_was_valid_by; + +let header = self.context.get_anchor_block_header(); +assert_local_note_was_valid_by(header, hinted_note, &mut self.context); +``` + +This verifies both: +1. The note was included in the note hash tree +2. The note's nullifier was not in the nullifier tree + +The `assert_local_note_was_*` helpers only work for the executing contract's own notes: this is because it is not possible for a contract to retrieve the app-siloed nullifier hiding secret key that corresponds to another contract. To prove facts about another contract's notes, use `assert_note_existed_by`, which supports notes of any contract (see [Prove note inclusion](#prove-note-inclusion)). + +## Prove at a specific historical block + +To prove against state at a specific past block (not just the anchor block): + +```rust +use aztec::history::note::assert_note_existed_by; + +let historical_header = self.context.get_block_header_at(block_number); +assert_note_existed_by(historical_header, hinted_note); +``` + +:::warning +Using `get_block_header_at` adds ~3k constraints to prove Archive tree membership. The anchor block header is effectively free since it's verified once per transaction. +::: + +## Prove a note was nullified + +To prove a note has been nullified: + +```rust +use aztec::history::note::assert_local_note_was_nullified_by; + +let header = self.context.get_anchor_block_header(); +assert_local_note_was_nullified_by(header, confirmed_note, &mut self.context); +``` + +## Prove contract bytecode was published + +To prove a contract's bytecode was published at a historical block: + +```rust +use aztec::history::deployment::assert_contract_bytecode_was_published_by; + +let header = self.context.get_anchor_block_header(); +assert_contract_bytecode_was_published_by(header, contract_address); +``` + +You can also prove a contract was initialized (constructor was called): + +```rust +use aztec::history::deployment::assert_contract_was_initialized_by; +use aztec::oracle::get_contract_instance::get_contract_instance; + +let header = self.context.get_anchor_block_header(); +let instance = get_contract_instance(contract_address); +assert_contract_was_initialized_by(header, contract_address, instance.initialization_hash); +``` + +## Read historical public storage + +To read the value a public storage slot held at a past block in a private function, use `public_storage_historical_read`. It returns the stored value and constrains it against the public data tree root in the given block header: + +```rust +use aztec::history::storage::public_storage_historical_read; + +let header = self.context.get_anchor_block_header(); +let value = public_storage_historical_read(header, storage_slot, contract_address); +``` + +An uninitialized slot reads as `0`. + +Because this proves the value against a past block header rather than reading the live chain tip, a private function can read public state this way **without enqueuing a public call**. Enqueuing a public call is only needed to read the _current_ value or to write to public storage. + +Higher-level state variables build on this. Reading a `PublicImmutable` or `DelayedPublicMutable` from a private function performs a historical public storage read internally (through `WithHash`), so you rarely need to call `public_storage_historical_read` directly. Both types are designed so that a historical read is also a correct read of the current value: a `PublicImmutable` can only be initialized once, so its value never changes, while a `DelayedPublicMutable` delays every write and sets the transaction's expiration timestamp so the transaction is only valid for as long as the value it read still holds. + +## Available history functions + +The `aztec::history` module provides these functions: + +| Function | Module | Purpose | +|----------|--------|---------| +| `assert_note_existed_by` | `history::note` | Prove note exists in note hash tree (any contract) | +| `assert_local_note_was_valid_by` | `history::note` | Prove own note exists and is not nullified | +| `assert_local_note_was_nullified_by` | `history::note` | Prove own note's nullifier is in nullifier tree | +| `assert_local_note_was_not_nullified_by` | `history::note` | Prove own note's nullifier is not in nullifier tree | +| `assert_nullifier_existed_by` | `history::nullifier` | Prove a siloed nullifier exists | +| `assert_nullifier_did_not_exist_by` | `history::nullifier` | Prove a siloed nullifier does not exist | +| `assert_contract_bytecode_was_published_by` | `history::deployment` | Prove a contract's bytecode was published | +| `assert_contract_bytecode_was_not_published_by` | `history::deployment` | Prove a contract's bytecode was not published | +| `assert_contract_was_initialized_by` | `history::deployment` | Prove a contract was initialized | +| `assert_contract_was_not_initialized_by` | `history::deployment` | Prove a contract was not initialized | +| `public_storage_historical_read` | `history::storage` | Read a public storage value at a historical block | + +The nullifier functions take a _siloed_ nullifier: the nullifier hashed together with the contract address, which is the value actually stored in the global nullifier tree. Use `compute_siloed_nullifier` to convert an inner nullifier (the value passed to `push_nullifier_unsafe`) into its siloed form. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_retrieve_filter_notes.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_retrieve_filter_notes.md new file mode 100644 index 00000000000..3319e8d815a --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_retrieve_filter_notes.md @@ -0,0 +1,166 @@ +--- +title: Retrieving and Filtering Notes +sidebar_position: 0 +tags: [private-state, smart-contracts, notes] +description: Step-by-step guide to retrieving, filtering, and sorting notes from private storage in Aztec contracts. +references: ["labs/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr", "labs/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr"] +--- + +This guide shows you how to retrieve and filter notes from private storage using `NoteGetterOptions`. + +## Prerequisites + +- Aztec contract with note storage +- Understanding of note structure and properties + +## Required imports + +```rust +use aztec::note::note_getter_options::{NoteGetterOptions, NoteStatus, SortOrder}; +use aztec::utils::comparison::Comparator; +``` + +## Set up basic note retrieval + +### Step 1: Create default options + +```rust +let mut options = NoteGetterOptions::new(); +``` + +This returns up to `MAX_NOTE_HASH_READ_REQUESTS_PER_CALL` notes without filtering. + +### Step 2: Retrieve notes from storage + +```rust +// Returns BoundedVec<ConfirmedNote<MyNote>, ...> +let confirmed_notes = storage.my_notes.at(owner).get_notes(options); +``` + +:::tip get_notes vs pop_notes + +- `get_notes`: Retrieves notes without nullifying. Note data is not guaranteed to be current or non-nullified—use when you only need to read note data without consuming it. +- `pop_notes`: Retrieves AND nullifies notes in one operation. Use when consuming notes (e.g., spending tokens). More efficient than calling `get_notes` followed by manual nullification. + +::: + +Here's an example of `pop_notes` with filtering from the NFT contract: + +#include_code pop_notes labs/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr rust + +## Filter notes by properties + +### Step 1: Select notes with specific field values + +```rust +// Assuming MyNote has an 'owner' field +let mut options = NoteGetterOptions::new(); +options = options.select( + MyNote::properties().owner, + Comparator.EQ, + owner +); +``` + +### Step 2: Apply multiple selection criteria + +```rust +let mut options = NoteGetterOptions::new(); +options = options + .select(MyNote::properties().value, Comparator.EQ, value) + .select(MyNote::properties().owner, Comparator.EQ, owner); +``` + +:::tip +Chain multiple `select` calls to filter by multiple fields. Remember to call `get_notes(options)` after applying all your selection criteria to retrieve the filtered notes. +::: + +## Sort retrieved notes + +### Sort and paginate results + +```rust +let mut options = NoteGetterOptions::new(); +options = options + .select(MyNote::properties().owner, Comparator.EQ, owner) + .sort(MyNote::properties().value, SortOrder.DESC) + .set_limit(10) // Max 10 notes + .set_offset(20); // Skip first 20 +``` + +## Apply custom filters + +:::tip Filter Performance +Database `select` is more efficient than custom filters. Use custom filters only for complex logic. +::: + +### Create and use a custom filter + +#include_code custom_filter labs/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr rust + +Then use it with `NoteGetterOptions`: + +```rust +let options = NoteGetterOptions::with_filter(filter_notes_min_sum, min_value); +``` + +:::warning Note Limits +Maximum notes per call: `MAX_NOTE_HASH_READ_REQUESTS_PER_CALL` (currently 16) +::: + +:::info Available Comparators + +- `Comparator.EQ`: Equal to +- `Comparator.NEQ`: Not equal to +- `Comparator.LT`: Less than +- `Comparator.LTE`: Less than or equal +- `Comparator.GT`: Greater than +- `Comparator.GTE`: Greater than or equal + +::: + +## Call from TypeScript + +You can pass comparator values from TypeScript to your contract functions: + +```typescript +import { Comparator } from '@aztec-labs/aztec.js/note'; + +// Pass comparator to a contract function that accepts it as a parameter +await contract.methods.read_notes(Comparator.GTE, 5).simulate({ from: senderAddress }); +``` + +## View notes without constraints + +Use `NoteViewerOptions` in unconstrained utility functions to query notes without generating proofs: + +#include_code view_notes labs/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr rust + +:::tip Viewer vs Getter + +- `NoteGetterOptions`: For constrained private functions with proof generation (max 16 notes) +- `NoteViewerOptions`: For unconstrained utility functions, no proofs (max 10 notes per page via `MAX_NOTES_PER_PAGE`) + +::: + +## Query notes with different status + +### Set status to include nullified notes + +```rust +let mut options = NoteGetterOptions::new(); +options = options.set_status(NoteStatus.ACTIVE_OR_NULLIFIED); +``` + +:::info Note Status Options + +- `NoteStatus.ACTIVE`: Only active (non-nullified) notes (default) +- `NoteStatus.ACTIVE_OR_NULLIFIED`: Both active and nullified notes + +::: + +## Next steps + +- Learn about [custom note implementations](../custom_notes.md) +- Explore [note discovery mechanisms](../../../foundational-topics/advanced/storage/note_discovery.md) +- Understand [partial notes](./partial_notes.md) diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules.md new file mode 100644 index 00000000000..ded35a99074 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules.md @@ -0,0 +1,76 @@ +--- +title: Using Capsules +sidebar_position: 17 +tags: [functions, oracles] +description: Learn how to use capsules for per-contract non-volatile storage in the PXE. +references: ["labs/noir-projects/aztec-nr/aztec/src/capsules/*", "labs/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr"] +--- + +Capsules provide per-contract non-volatile storage in the PXE. Data is stored locally (not onchain), scoped per contract address, and persists until explicitly deleted. + +## Basic usage + +```rust +use aztec::oracle::capsules; + +// Capsule operations are unconstrained, so these values are typically +// passed in as parameters from the calling context. +let contract_address: AztecAddress = /* self.address */; +let slot: Field = 1; +// scope is an AztecAddress used for capsule isolation, allowing multiple +// independent namespaces within the same contract. +let scope: AztecAddress = /* e.g. the account address */; + +// Store data at a slot (overwrites existing data) +capsules::store(contract_address, slot, value, scope); + +// Load data (returns Option<T>) +let result: Option<MyStruct> = capsules::load(contract_address, slot, scope); + +// Delete data at a slot +capsules::delete(contract_address, slot, scope); + +// Copy contiguous slots (supports overlapping regions) +// copy(contract_address, src_slot, dst_slot, num_entries: u32, scope) +capsules::copy(contract_address, src_slot, dst_slot, 3, scope); +``` + +Types must implement `Serialize` and `Deserialize` traits. + +:::warning +All capsule operations are `unconstrained`. Data loaded from capsules should be validated in constrained contexts. Contracts can only access their own capsules. +::: + +## CapsuleArray + +`CapsuleArray` provides dynamic array storage backed by capsules: + +```rust +use aztec::capsules::CapsuleArray; +use aztec::protocol::hash::sha256_to_field; + +// Use a hash for base_slot to avoid collisions with other storage +global BASE_SLOT: Field = sha256_to_field("MY_CONTRACT::MY_ARRAY".as_bytes()); + +let array: CapsuleArray<Field> = CapsuleArray::at(contract_address, BASE_SLOT, scope); + +array.push(value); // Append to end +let value = array.get(index); // Read at index (throws if out of bounds) +let length = array.len(); // Get current size (returns u32) +array.remove(index); // Delete & shift elements (index is u32) + +// Iterate and optionally remove elements +array.for_each(|index, value| { + if some_condition(value) { + array.remove(index); // Safe to remove current element only + } +}); +``` + +:::warning `for_each` Safety +It is safe to remove the current element during `for_each`, but **do not push new elements** during iteration. +::: + +:::info Storage Layout +CapsuleArray stores length at the base slot, with elements in consecutive slots (base+1 for index 0, base+2 for index 1, etc.). Ensure sufficient space between different array base slots. +::: diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes.md new file mode 100644 index 00000000000..ee1655f91e1 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes.md @@ -0,0 +1,137 @@ +--- +title: Partial notes +sidebar_position: 1 +tags: [Developers, Contracts, Notes] +description: How partial notes work, how they are completed, and how they enable use cases like AMM swaps and payment endpoints. +references: ["labs/noir-projects/aztec-nr/uint-note/src/uint_note.nr"] +--- + +import Image from "@theme/IdealImage"; + +:::note Token standard +Where this page refers to a concrete token, it assumes the [AIP-20 fungible token standard](../../standards/aip-20.md). The partial-note primitive (`UintNote` / `PartialUintNote`) is token-agnostic; AIP-20 is one standard built on top of it. +::: + +## What are partial notes? + +Partial notes are notes created with incomplete data, usually during private execution, which can be completed with additional information that becomes available later, usually during public execution. + +Let's say, for example, we have a `UintNote`: + +#include_code uint_note_def /labs/noir-projects/aztec-nr/uint-note/src/uint_note.nr rust + +The `UintNote` struct itself only contains the `value` field. Additional fields including `owner`, `randomness`, and `storage_slot` are passed as parameters during note hash computation. + +When creating the note locally during private execution, the `owner` and `storage_slot` are known, but the `value` potentially is not (e.g., it depends on some onchain dynamic variable). First, a **partial note** can be created during private execution that commits to the `owner` and `randomness`, and then the note is _"completed"_ to create a full note by later adding the `storage_slot` and `value` fields, usually during public execution. + +<Image img={require("@site/static/img/partial-notes.png")} /> + +## Use cases + +Partial notes are useful when part of the note struct is a value that depends on dynamic, public onchain data that isn't available during private execution, such as: + +- AMM swap prices +- Current gas prices +- Time-dependent interest accrual + +They are also useful as **payment endpoints**: a recipient can create a partial note ahead of time and share the commitment with prospective senders. Senders later complete the partial note to pay the recipient, with no action needed from the recipient at payment time. See [partial notes as payment endpoints](./partial_notes_as_payment_endpoints.md) for the full design. + +## The completer + +A partial note is finalized by a later completion step that supplies the public fields (`storage_slot` and `value`). At that point its private preimage (`owner` and `randomness`) is not re-derived or re-checked, and the partial note itself is just a `Field` that can be copied and shared freely. If anyone holding it could complete it, they could insert a note with an arbitrary, unbacked `value` into the note hash tree, or complete the note at the wrong time or with the wrong values. + +To prevent this, the creator designates a **completer** at creation time. During the constrained private execution that creates the partial note, the contract records a validity commitment `H(partial_commitment, completer)` in the nullifier tree. Completion recomputes this commitment and asserts it exists, using its presence as proof that a legitimate, constrained execution created the partial note and authorized this specific completer to supply the public values and finalize it. + +With AIP-20, the completer is chosen explicitly when calling `initialize_transfer_commitment`; completion (`transfer_private_to_commitment` / `transfer_public_to_commitment`) binds the completer to the caller's `msg_sender` and debits a separately authorized `from` account. + +For `UintNote`, the fields split cleanly across the two phases: + +| Field | Fixed at | How | +| -------------- | ------------------------------- | -------------------------------------------------------------------------------------------- | +| `owner` | Partial note creation (private) | Committed in `partial_commitment = H(owner, randomness)` | +| `randomness` | Partial note creation (private) | Same commitment; fresh per note, blinds the owner | +| `completer` | Partial note creation (private) | Bound in the validity commitment `H(partial_commitment, completer)`; not part of the note hash | +| `storage_slot` | Completion (public or private) | Hashed into `note_hash = H(storage_slot, partial_commitment, value)` | +| `value` | Completion (public or private) | Same hash; supplied by the completer's call | + +(`storage_slot` is typically known during private execution too; it is just not bound into the note hash until completion.) + +The creator fixes who gets paid (`owner`) and who may finalize (`completer`); the completer later fixes how much (`value`). Funds therefore flow from the completing side to the note's owner: in AIP-20, completion debits the authorized `from` account (the completer itself, or a payer who authorized it) and credits the `owner` chosen by the creator. + +## Single-use semantics + +Each partial note is intended to be completed exactly once. The protocol does not enforce this directly: completion checks that a validity commitment exists in the nullifier tree but does not consume it, so a partial note can technically be completed more than once. However, reuse is unsafe for two independent reasons: + +1. **Privacy.** The completion log is tagged by `H(partial_commitment)`. Two completions of the same partial note emit logs with the same tag, which publicly links those completions as paying the same recipient. +2. **Discovery.** The recipient's Private eXecution Environment (PXE) treats the partial note as pending until the first matching completion log is found. After the first match, the pending entry is removed. A second completion against the same commitment may not be discovered by the recipient's wallet, so the funds are effectively lost. + +This is why an AIP-20 commitment should be completed only once. A second `transfer_private_to_commitment` (or `transfer_public_to_commitment`) against the same commitment is not found by the recipient's log processing on the second pass, so the amount is most likely lost. + +The takeaway: treat each partial note as a one-shot object. To accept multiple payments, create multiple partial notes. + +## Completion in public and private contexts + +`PartialUintNote` supports completion in two contexts: + +- `complete` runs in a public function (AIP-20's `transfer_public_to_commitment`). The storage slot and value are emitted in a public log tagged by the partial note's commitment. Anyone observing the chain learns the amount. +- `complete_from_private` runs in a private function (AIP-20's `transfer_private_to_commitment`). The same storage slot and value are emitted in a private log with the same tag. The payload is plaintext, but it is only discoverable by a party that can derive the tag, and the tag derives from the partial note's commitment. + +For private→private completion, the privacy of the amount depends on whether the partial note's commitment itself is held secret. If the commitment is published publicly (e.g., in an onchain registry), anyone can derive the tag and read the amount from the private log payload. If the commitment is shared only with prospective senders, the amount stays hidden from outside observers. + +One additional protocol constraint: `complete_from_private` requires the validity commitment to be settled in a prior transaction. A partial note cannot be both created and completed in the same private transaction. The public completion path has no such restriction. + +## Implementation + +All notes in Aztec use the partial note format internally. This ensures that notes produce identical note hashes regardless of whether they were created as complete notes (with all fields known in private) or as partial notes (completed later in public). By having all notes follow the same two-phase hash commitment process, the protocol maintains consistency and allows notes created through different flows to behave identically. + +### Note structure example + +The `UintNote` struct contains only the `value` field: + +#include_code uint_note_def /labs/noir-projects/aztec-nr/uint-note/src/uint_note.nr rust + +### Two-phase commitment process + +**Phase 1: partial commitment (private execution)** + +The private fields (`owner` and `randomness`) are committed during local, private execution: + +#include_code compute_partial_commitment /labs/noir-projects/aztec-nr/uint-note/src/uint_note.nr rust + +This creates a partial note commitment: + +``` +partial_commitment = H(owner, randomness) +``` + +**Phase 2: note completion (public execution)** + +The note is completed by hashing the partial commitment with the public value: + +#include_code compute_complete_note_hash /labs/noir-projects/aztec-nr/uint-note/src/uint_note.nr rust + +The resulting structure is a nested commitment: + +``` +note_hash = H(storage_slot, H(owner, randomness), value) + = H(storage_slot, partial_commitment, value) +``` + +## Universal note format + +All notes in Aztec use the partial note format internally, even when all data is known during private execution. This ensures consistent note hash computation regardless of how the note was created. + +When a note is created with all fields known (including `owner`, `storage_slot`, `randomness`, and `value`): + +1. A partial commitment is computed from the private fields (`owner`, `randomness`) +2. The partial commitment is immediately completed with the `storage_slot` and `value` fields + +#include_code compute_note_hash /labs/noir-projects/aztec-nr/uint-note/src/uint_note.nr rust + +This two-step process ensures that notes with identical field values produce identical note hashes, regardless of whether they were created as partial notes or complete notes. + +## Partial notes in practice + +To understand how to use partial notes in practice, [this AMM contract](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/noir-projects/noir-contracts/contracts/app/amm_contract) uses partial notes to initiate and complete the swap of `token1` to `token2`. Since the exchange rate is onchain, it cannot be known ahead of time while executing in private so a full note cannot be created. Instead, a partial note is created for the `owner` swapping the tokens. This partial note is then completed during public execution once the exchange rate can be read. + +For a different application of the same primitive, where the partial note represents an offer to be paid rather than a deferred DeFi settlement, see [partial notes as payment endpoints](./partial_notes_as_payment_endpoints.md). diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes_as_payment_endpoints.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes_as_payment_endpoints.md new file mode 100644 index 00000000000..3c606b0c025 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes_as_payment_endpoints.md @@ -0,0 +1,159 @@ +--- +title: Partial notes as payment endpoints +sidebar_position: 2 +tags: [Developers, Contracts, Notes, Privacy] +description: Using partial notes as a recipient's offer to be paid, enabling naming-service style flows with no recipient action at payment time. +references: + [ + "labs/noir-projects/aztec-nr/uint-note/src/uint_note.nr", + "labs/noir-projects/aztec-nr/aztec/src/partial_notes/*", + ] +--- + +:::note Assumed token standard +This page assumes the [AIP-20 fungible token standard](../../standards/aip-20.md), which exposes commitment-based transfers directly: the completer is an explicit argument to `initialize_transfer_commitment`, and completion debits a separately authorized account. The example `token_contract` in aztec-packages also supports a commitment-based flow (the two-step `prepare_private_balance_increase` plus `finalize_transfer_to_private`), but its prepare step takes no `completer` argument. It always sets the completer to `msg_sender`, the caller of `prepare_private_balance_increase`. The private relayer flow described on this page is still achievable there, but the chosen completer (for example, the relayer) must itself be the caller of `prepare`, rather than the recipient naming an arbitrary completer in a single self-issued call as AIP-20 allows. +::: + +## The problem + +Consider a naming service that resolves `alice.aztec` to something that anyone can pay. If the name resolves to a stable Aztec address, every sender and every registry observer can link payments to the same recipient identifier, even when the payment notes themselves are private. The recipient is forced to share that identifier with every sender, so two senders can collude to confirm they paid the same person. + +A privacy-conscious naming service should let a sender pay `alice.aztec` without: + +- learning the recipient's actual Aztec address, +- producing a sender-visible or registry-visible link between any two payments to the same name, +- requiring the recipient to take any action when the payment is made. + +[Partial notes](./partial_notes.md) provide the primitive. This page covers how to use them to back a stable name with a rotating supply of unlinkable, one-shot payment endpoints. + +The term _payment endpoint_ in this page is application-level. Aztec's protocol terminology stays with "partial note," "completer," and "completion log." + +## What lives where + +The trick is that the name never maps to an address at all. It maps to a list of partial note commitments, and senders pay into a commitment. Concretely, for `alice.aztec`: + +1. **Alice's wallet creates partial notes ahead of time.** Each one is a commitment `H(alice_address, randomness_i)` with fresh randomness per note. Her address is inside the hash, blinded by the randomness, so the commitment reveals nothing about her. +2. **The chain records that the note can be completed, and by whom.** Creating each partial note writes a _validity commitment_ `H(partial_commitment, completer)` to the nullifier tree. This is not the partial note itself, it binds the partial note's commitment to its designated completer, and is what the token checks at completion to confirm the completer is legitimate. It is also just a hash, so no address is visible. +3. **The lookup channel stores `alice.aztec → [c1, c2, c3, ...]`.** The _lookup channel_ is wherever the name resolves: an ENS text record, a JSON file Alice hosts, an onchain registry contract. The commitments are opaque `Field` values that can be stored as a plain array, and this mapping is the only thing senders ever read. +4. **Alice's Private eXecution Environment (PXE) holds the preimages.** Her wallet knows which commitments are hers and watches for their completion logs, so payments land without any action from her. + +The rest of this page unpacks each piece: how a partial note works as an offer to be paid, who is allowed to complete one, and where the lookup channel can live. + +## A partial note as an offer to be paid + +A partial note is a commitment to `(owner, randomness)`. Once created, the commitment is just a `Field`: it can be copied freely, stored anywhere, and shared with any sender. Holding the commitment is not enough to complete it, though; only the note's designated _completer_ can fill in `(storage_slot, value)` and finalize it. + +With AIP-20, the recipient creates a partial note by calling `initialize_transfer_commitment`, choosing both the eventual owner and the completer: + +```rust +#[external("private")] +fn initialize_transfer_commitment(to: AztecAddress, completer: AztecAddress) -> Field +``` + +The completer is bound into a _validity commitment_ `H(partial_commitment, completer)` recorded in the nullifier tree. Despite the name, the nullifier tree is used here only as an append-only set whose entries can be checked for existence. The validity commitment uses its own domain separator, so it is not a note nullifier and consumes nothing. Completion (`transfer_private_to_commitment` or `transfer_public_to_commitment`) sets the completer to the caller's `msg_sender` and checks that the matching validity commitment exists, so a partial note can only be finalized by the address set as its completer. Completion debits a separate, authorized `from` account, so the payer and the completer can be different parties. + +Two properties make this useful as a payment endpoint: + +- **The recipient takes no action at payment time.** When the note is created, the token sends the recipient a private message identifying it. The recipient's PXE holds this pending entry and scans for the completion log whenever it has the chance. Receiving an ordinary transfer is just as passive; the difference here is that the recipient also never has to interact with the sender, because the commitment was published ahead of time. +- **The completer is fixed when the note is created.** The recipient chooses who can finalize the note at creation time, and that choice is enforced cryptographically. + +Each partial note is single-use (see [single-use semantics](./partial_notes.md#single-use-semantics)), so an endpoint that accepts many payments must hold many partial notes. + +## The completer choice + +The choice of `completer` determines who can complete a given partial note, and so how senders pay through it. Two options matter: + +### Completer = a specific sender + +The recipient creates one partial note per known sender ahead of time, with each `completer` set to that sender's address. The sender finalizes the note with their own funds by calling `transfer_private_to_commitment` (or `transfer_public_to_commitment`), acting as both the `from` and the `msg_sender`/completer. + +This works without any new contracts but requires the recipient to know each sender's address in advance. It is useful for repeat payers (subscriptions, regular invoices) but does not scale to "anyone can pay this name." + +### Completer = a relayer contract + +The recipient creates partial notes whose `completer` is a known _relayer_ contract: a contract whose only job is to forward a sender's payment into the token's completion function. Any sender can invoke the relayer, the relayer is what the token sees as `msg_sender` (so the validity commitment check passes), and the sender is debited as the authorized `from`. + +This is the option that scales to unknown senders. The relayer needs no per-name logic, so a single global relayer contract could serve every payment endpoint on the network. + +## The relayer contract + +An illustrative sketch (not a compilable reference): + +```rust +contract PaymentRelayer { + // Called by any sender holding a commitment. Forwards into the token's + // completion from the relayer's call frame, so the token sees the relayer + // as the completer, while the sender is debited as the authorized `from`. + #[external("private")] + fn pay_private(token: AztecAddress, commitment: Field, amount: u128, authwit_nonce: Field) { + let from = self.context.msg_sender(); + self.call(Token::at(token).transfer_private_to_commitment( + from, + commitment, + amount, + authwit_nonce, + )) + } +} +``` + +How the flow works: + +- The recipient creates notes with `initialize_transfer_commitment(to: recipient, completer: relayer)` and publishes the commitments through their [lookup channel](#what-lives-where). Creating a note is a direct call to the token; the relayer only participates at completion. +- A sender calls `pay_private` with a commitment obtained from the recipient's lookup channel. The relayer reads `from = msg_sender` (the sender) and forwards into the token. At the token's frame, `msg_sender` is the relayer, matching the completer the note was created with, so the token debits the sender and completes the note. +- An [authwit](../authentication_witnesses.md) signed by the sender authorizes the relayer to call `transfer_private_to_commitment` with these specific arguments, since the token sees the relayer, not the sender, as the immediate caller. + +Both private and public payments are supported: `transfer_private_to_commitment` debits the sender's private balance, `transfer_public_to_commitment` debits their public balance. Both take an explicit authorized `from`, so the relayer never needs to hold or be pre-funded with the sender's tokens. Fee sponsorship is separate, handled by a [Fee Payment Contract (FPC)](../../../foundational-topics/fees.md) during the transaction's setup phase. + +## The distribution choice + +Where the mapping "name → partial-note commitments" lives is independent of who completes the notes: + +- **Offchain.** A static file at `alice.example/aztec.json`, an ENS text record, IPFS, or any other lookup channel. The chain never sees the name or the list size. Requires trust in the hosting and a way to authenticate the result. +- **Onchain.** A registry contract with public storage mapping names to commitments. Censorship-resistant and allows atomic lookup-and-pay in a single transaction, but exposes the list size, refill cadence, and the plaintext name. + +The two choices are orthogonal. Either channel can hand out commitments created with any completer; only the lookup mechanism differs. + +## A recommended pattern + +For "name → unlinkable payment endpoint, passive recipient," the combination that gives the best privacy / UX ratio is offchain distribution (the recipient hosts the lookup), a relayer contract as the completer, and private→private completion so the amount stays hidden from observers who do not hold the commitment. + +The recipient is responsible for: + +1. Periodically creating fresh partial notes with the relayer as completer. +2. Publishing the resulting commitments through whatever lookup channel they choose. +3. Pruning commitments that have been consumed (the recipient's PXE knows when each partial note has been completed). + +Refill cadence is an operational concern. If senders consume commitments faster than the recipient refills, the lookup will return nothing. Batching the creation of many partial notes in a single transaction reduces the per-payment cost. + +## What is hidden, what leaks + +The pattern hides: + +- The recipient's Aztec address from senders. +- The link between any two payments to the same name, as long as each payment consumes a different partial note. +- The amount, in private→private completion, provided the commitment is not exposed to the observer. The completion log payload is plaintext but only discoverable by a party who can derive the tag, and the tag derives from the commitment. If the recipient's lookup channel hands out the same list to every viewer, any observer who fetches the list can derive tags and read completed amounts from it. Authenticated or sender-specific distribution narrows this exposure. + +The pattern leaks: + +- The fact that the relayer contract was invoked in a transaction. +- The completion log tag for each payment. The tag does not reveal the recipient, but it confirms that _some_ completion happened against _some_ partial note. +- Anything the lookup channel itself reveals. An offchain channel can hide the existence of the name; an onchain registry cannot. +- The recipient's refill cadence, if the transactions that create fresh partial notes are visible. + +A reused partial note breaks both the privacy property (linkable completions) and the discovery property (the recipient's wallet will miss the second completion). The published commitments must rotate; commitments must not be republished after consumption. + +## What this does not solve + +This pattern is not a stealth-address scheme. It requires the recipient to create and publish a list of commitments ahead of time, and to keep refilling it; senders draw from that list. A stealth-address scheme, by contrast, lets a recipient publish a single meta-address once and stay otherwise passive, with each sender deriving a fresh one-time address non-interactively. Partial-note endpoints trade that recipient passivity for an explicit, recipient-controlled supply of payment slots. + +## Forward-looking note + +Partial-note creation uses `MessageDelivery::onchain_unconstrained` today. A constrained delivery mode (`MessageDelivery::onchain_constrained`) now exists, but its log tag is not yet fully constrained and partial notes do not use it. Constrained tagging and handshaking is tracked in [aztec-packages issue #14565](https://github.com/AztecProtocol/aztec-packages/issues/14565) and may change how recipients discover partial-note creation messages. The concepts on this page (single-use commitments, completer binding, distribution choice) are stable across that change; specific code patterns on the recipient's discovery side may shift. + +## Related + +- [Partial notes](./partial_notes.md): the underlying primitive. +- [AIP-20: Fungible Token](../../standards/aip-20.md): the token standard assumed throughout this page. +- [Keys](../../../foundational-topics/accounts/keys.md): how Aztec accounts derive addresses. Partial-note endpoints offer an alternative to addresses for the specific use case of being paid. +- [Fees](../../../foundational-topics/fees.md): how fee payment and sponsorship work, independent of the partial-note pattern. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md new file mode 100644 index 00000000000..2baec4b1bfe --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md @@ -0,0 +1,36 @@ +--- +title: Oracle Functions +sidebar_position: 18 +tags: [functions, oracles] +description: Learn about oracles in Aztec, which provide external data to smart contracts during execution. +references: ["labs/noir-projects/aztec-nr/aztec/src/oracle/*"] +--- + +This page goes over what oracles are in Aztec and how they work. + +Looking for a hands-on guide? You can learn how to use oracles in a smart contract [here](./how_to_use_capsules.md). + +An oracle is something that allows us to get data from the outside world into our contracts. The most widely-known types of oracles in blockchain systems are probably Chainlink price feeds, which allow us to get the price of an asset in USD taking non-blockchain data into account. + +While this is one type of oracle, the more general oracle, allows us to get any data into the contract. In the context of oracle functions or oracle calls in Aztec, it can essentially be seen as user-provided arguments, that can be fetched at any point in the circuit, and don't need to be an input parameter. + +**Why is this useful? Why don't just pass them as input parameters?** +In the world of EVM, you would just read the values directly from storage and call it a day. However, when we are working with circuits for private execution, this becomes more tricky as you cannot just read the storage directly from your state tree, because there are only commitments (e.g. hashes) there. The pre-images (content) of your commitments need to be provided to the function to prove that you actually allowed to modify them. + +If we fetch the notes using an oracle call, we can keep the function signature independent of the underlying data and make it easier to use. A similar idea, applied to the authentication mechanism is used for the Authentication Witnesses that allow us to have a single function signature for any wallet implementation, see [AuthWit](../authentication_witnesses.md) for more information on this. + +Oracles introduce **non-determinism** into a circuit, and thus are `unconstrained`. It is important that any information that is injected into a circuit through an oracle is later constrained for correctness. Otherwise, the circuit will be **under-constrained** and potentially insecure! + +`Aztec.nr` has a [module dedicated to its oracles](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/oracle/index.html) where you can browse the full list. + +## Inbuilt oracles + +- [`debug_log`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/protocol/logging/fn.debug_log) - Provides debug functions that can be used to log information to the console. Read more about debugging [here](../../debugging.md). +- [`auth_witness`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/oracle/auth_witness/index.html) - Provides a way to fetch the authentication witness for a given address. This is useful when building account contracts to support approve-like functionality. +- [`get_l1_to_l2_membership_witness`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/oracle/get_l1_to_l2_membership_witness/index.html) - Returns the leaf index and sibling path for an L1 to L2 message, used to prove message existence in cross-chain applications like token bridges. +- [`notes`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/oracle/notes/index.html) - Provides functions related to notes, such as fetching notes from storage, used behind the scenes for value notes and other pre-built note implementations. +- [`logs`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/oracle/logs/index.html) - Provides functions to log encrypted and unencrypted data. + +Find a full list [on GitHub](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/noir-projects/aztec-nr/aztec/src/oracle). + +Please note that it is **not** possible to write a custom oracle for your dapp. Oracles are implemented in the PXE, so all users of your dapp would have to use a PXE with your custom oracle included. If you want to inject some arbitrary data that does not have a dedicated oracle, you can use [capsules](./how_to_use_capsules.md). diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/writing_efficient_contracts.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/writing_efficient_contracts.md new file mode 100644 index 00000000000..159babe0d5f --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/advanced/writing_efficient_contracts.md @@ -0,0 +1,386 @@ +--- +title: Writing Efficient Contracts +sidebar_position: 2 +tags: [Developers, Contracts] +description: Best practices and techniques for writing gas-efficient contracts on Aztec, optimizing for both proving and execution costs. +--- + +import Image from "@theme/IdealImage"; + +## Writing functions + +On Ethereum L1, all data is public and all execution is completely reproducible. The Aztec L2 takes on the challenge of execution of private functions on private data. This is done client side, along with the generation of corresponding proofs, so that the network can verify the proofs and append any encrypted data/nullifiers (privacy preserving state update). + +This highlights a key difference with how public vs private functions are written. + +:::info Writing efficiently + +- **Public functions** can be written intuitively - optimising for execution/gas as one would for EVM L2s +- **Private functions** are optimized differently, as they are compiled to a circuit to be proven locally (see [Thinking in Circuits](https://noir-lang.org/docs/guides/thinking_in_circuits)) + +::: + +## Assessing efficiency + +On Aztec (like other L2s) there are several costs/limit to consider... + +- L1 costs - execution, blobs, events +- L2 costs - public execution, data, logs +- Local limits - proof generation time, execution + +### Local Proof generation + +Since proof generation is a significant local burden, being mindful of the gate-count of private functions is important. The gate-count is a proportionate indicator of the memory and time required to prove locally, so should not be ignored. + +#### Noir for circuits + +An explanation of efficient use of Noir for circuits should be considered for each subsection under [writing efficient Noir](https://noir-lang.org/docs/guides/thinking_in_circuits#writing-efficient-noir-for-performant-products) to avoid hitting local limits. The general theme is to use language features that favour the underlying primitives and representation of a circuit from code. + +A couple of examples: + +- Since the underlying cryptography uses an equation made of additions and multiplications, these are more efficient (wrt gate count) in Noir than say bit-shifting. +- Unconstrained functions by definition do not constrain their operations/output, so do not contribute to gate count. Using them carefully can bring in some savings, but the results must then be constrained so that proofs are meaningful for your application. + +:::warning Tradeoffs and caveats +Each optimisation technique has its own tradeoffs and caveats so should be carefully considered with the full details in the linked [section](https://noir-lang.org/docs/guides/thinking_in_circuits#writing-efficient-noir-for-performant-products). +::: + +#### Overhead of nested private calls + +Every transaction pays a fixed kernel overhead (~290k gates for init, reset, and tail circuits). Each additional private function call beyond the account entrypoint adds a `private_kernel_inner` iteration (~101k gates). This overhead compounds with the number of distinct private function calls, so be mindful of calling/nesting too many private functions — this may influence your design towards larger private functions rather than conventionally atomic ones. + +For example, if you have a function that calls an external verification step as a separate private function, inlining that verification saves an entire kernel iteration (~101k gates), even if it slightly increases the calling function's own gate count. + +See [Private Kernel Circuit - Performance Impact](../../../foundational-topics/advanced/circuits/private_kernel.md#performance-impact) for detailed numbers. + +#### Profiling + +Measuring gate counts is explained in the [profiling guide](./how_to_profile_transactions). Use `aztec profile gates` for quick per-function gate counts, or `aztec-wallet profile` for full transaction profiling including kernel overhead. + +### L2 Data costs + +Of the L2 costs, the public/private data being updated is most significant. As L2 functions create notes, nullifiers, encrypted logs, all of this get posted into blobs on ethereum and will be quite expensive + +:::tip Data packing +You can reduce storage operation costs by implementing custom `Packable` for your structs, packing multiple sub-`Field` values into fewer `Field` elements. See [Data Packing and Serialization](../data_packing.md) for details. +::: + +### L1 Limits + +While most zk rollups don't leverage the zero-knowledge property like Aztec, they do leverage the succinctness property. +That is, what is stored in an L1 contract is simply a hash. + +For data availability, blobs are utilized since data storage is often cheaper here than in contracts. Like other L2s such costs are factored into the L2 fee mechanisms. These limits can be seen and iterated on when a transaction is simulated/estimated. + +## Examples for private functions (reducing gate count) + +After the first section about generating a flamegraph for an Aztec function, each section shows an example of different optimisation techniques. + +### Inspecting with flamegraphs + +Use the Noir profiler to generate flamegraphs for your contract functions. The profiler is installed automatically with Nargo (starting noirup v0.1.4). + +```bash +# Generate a gates flamegraph (requires bb backend) +noir-profiler gates \ + --artifact-path ./target/counter-Counter.json \ + --backend-path bb \ + --output ./target +``` + +Open the generated `.svg` file in a browser for an interactive view. For more details, see the [profiling guide](./how_to_profile_transactions.md). + +<Image img={require('/img/flamegraph-counter.png')} /> + +To get a sense of things, here is a table of gate counts for common operations: + +| Gates | Operation | +| ----- | ------------------------------------------------------------------------------ | +| ~75 | Hashing 3 fields with Poseidon2 | +| 3500 | Reading a value from a tree (public data tree, note hash tree, nullifier tree) | +| 4000 | Reading a delayed public mutable read | +| ~5,000 | Calculating sha256 (varies by input size) | +| Varies | Constrained encryption of a private log (depends on field count) | +| Varies | Constrained encryption and tagging of a private log (depends on field count) | + +### Optimization: use arithmetic instead of non-arithmetic operations + +Because the underlying equation in the proving backend makes use of multiplication and addition, these operations incur less gates than bit-shifting or bit-masking. + +For example: + +```rust +comptime global TWO_POW_16: Field = 2.pow_32(16); +// ... +{ + #[external("private")] + fn mul_inefficient(number: Field) -> u128 { + number as u128 << 16 as u8 + } // 5244 gates + + #[external("private")] + fn mul_efficient(number: Field) -> u128 { + (number * TWO_POW_16) as u128 + } // 5184 gates (60 gates less) +} +``` + +When comparing the flamegraph of the two functions, the inefficient shift example has a section of gates not present in the multiplication example. This difference equates to a saving of 60 gates. + +In the same vein bitwise `AND`/`OR`, and inequality relational operators (`>`, `<`) are expensive. Try avoid these in your circuits. + +For example, use boolean equality effectively instead of `>=`: + +```rust +{ + #[external("private")] + fn sum_from_inefficient(from: u32, array: [u32; 1000]) -> u32 { + let mut sum: u32 = 0; + for i in 0..1000 { + if i >= from { // condition based on `>=` each time (higher gate count) + sum += array[i]; + } + } + sum + } // 44317 gates + + #[external("private")] + fn sum_from_efficient(from: u32, array: [u32; 1000]) -> u32 { + let mut sum: u32 = 0; + let mut do_sum = false; + for i in 0..1000 { + if i == from { // latches boolean at transition (equality comparison) + do_sum = true; + } + if do_sum { // condition based on boolean true (lower gate count) + sum += array[i]; + } + } + sum + } // 45068 gates (751 gates more due to the boolean operations, but the pattern demonstrates how to avoid range checks) +} +``` + +So for a loop of 1000 iterations, 751 gates were saved by: + +- Adding an equivalence check and a boolean assignment +- Replacing `>=` with a boolean equivalence check + +:::note Difference with Rust +Such designs with boolean flags lend themselves well into logical comparisons too since `&&` and `||` do not exist. With booleans, using `&` and `|` can give you the required logic efficiently. For more points specific to the Noir language, see [this](https://noir-lang.org/docs/guides/thinking_in_circuits#translating-from-rust) section. + +::: + +### Optimization: Loop design + +Since private functions are circuits, their size must be known at compile time, which is equivalent to its execution trace. +See [this example](https://github.com/noir-lang/noir-examples/blob/master/noir_by_example/loops/noir/src/main.nr#L11) for how to use loops when dynamic execution lengths (ie variable number of loops) is not possible. + +### Optimization: considered use of `unconstrained` functions + +#### Example - calculating square root + +Consider the following example of an implementation of the `sqrt` function: + +```rust +use aztec::macros::aztec; + +#[aztec] +pub contract OptimisationExample { + use aztec::macros::{functions::{external, initializer}, storage::storage}; + + #[storage] + struct Storage<Context> {} + + #[external("public")] + #[initializer] + fn constructor() {} + + #[external("private")] + fn sqrt_inefficient(number: Field) -> Field { + super::sqrt_constrained(number) + } + + #[external("private")] + fn sqrt_efficient(number: Field) -> Field { + // Safety: calculate in unconstrained function, then constrain the result + let x = unsafe { super::sqrt_unconstrained(number) }; + assert(x * x == number, "x*x should be number"); + x + } + +} + +fn sqrt_constrained(number: Field) -> Field { + let MAX_LEN = 100; + + let mut guess = number; + let mut guess_squared = guess * guess; + for _ in 1..MAX_LEN as u32 + 1 { + // only use square root part of circuit when required, otherwise use alternative part of circuit that does nothing + // Note: both parts of the circuit exist MAX_LEN times in the circuit, regardless of whether the square root part is used or not + if (guess_squared != number) { + guess = (guess + number / guess) / 2; + guess_squared = guess * guess; + } + } + + guess +} + +unconstrained fn sqrt_unconstrained(number: Field) -> Field { + let mut guess = number; + let mut guess_squared = guess * guess; + while guess_squared != number { + guess = (guess + number / guess) / 2; + guess_squared = guess * guess; + } + guess +} + +``` + +The two implementations after the contract differ in one being constrained vs unconstrained, as well as the loop implementation (which has other design considerations). +Measuring the two, we find the `sqrt_inefficient` to require around 1500 extra gates compared to `sqrt_efficient`. + +To generate flamegraphs for each function: + +```bash +noir-profiler gates \ + --artifact-path ./target/optimisation_example-OptimisationExample.json \ + --backend-path bb \ + --output ./target +``` + +If you make changes to the code, recompile and regenerate the flamegraph, then refresh the `.svg` file in your browser. + +Note: this is largely a factor of the loop size choice based on the maximum size of `number` you are required to be calculating the square root of. For larger numbers, the loop would have to be much larger, so perform in an unconstrained way (then constraining the result) is much more efficient. + +#### Example - sorting an array + +Like with sqrt, we have the inefficient function that does the sort with constrained operations, and the efficient function that uses the unconstrained sort function then constrains the result. + +```rust +//... +{ + #[external("private")] + fn sort_inefficient(array: [u32; super::ARRAY_SIZE]) -> [u32; super::ARRAY_SIZE] { + let mut sorted_array = array; + for i in 0..super::ARRAY_SIZE as u32 { + for j in 0..super::ARRAY_SIZE as u32 { + if sorted_array[i] < sorted_array[j] { + let temp = sorted_array[i as u32]; + sorted_array[i as u32] = sorted_array[j as u32]; + sorted_array[j as u32] = temp; + } + } + } + sorted_array + } // 6823 gates for 10 elements, 127780 gates for 100 elements + + #[external("private")] + fn sort_efficient(array: [u32; super::ARRAY_SIZE]) -> [u32; super::ARRAY_SIZE] { + // Safety: calculate in unconstrained function, then constrain the result + let sorted_array = unsafe { super::sort_array(array) }; + // constrain that sorted_array elements are sorted + for i in 0..super::ARRAY_SIZE as u32 - 1 { + assert(sorted_array[i] <= sorted_array[i + 1], "array should be sorted"); + } + // Note: A production implementation should also verify that sorted_array is a + // permutation of the input array to prevent a malicious prover from returning + // arbitrary sorted values. + sorted_array + } // 5870 gates (953 gates less) for 10 elements, 12582 gates for 100 elements (115198 gates less) +} + +unconstrained fn sort_array(array: [u32; ARRAY_SIZE]) -> [u32; ARRAY_SIZE] { + let mut sorted_array = array; + for i in 0..ARRAY_SIZE as u32 { + for j in 0..ARRAY_SIZE as u32 { + if sorted_array[i] < sorted_array[j] { + let temp = sorted_array[i as u32]; + sorted_array[i as u32] = sorted_array[j as u32]; + sorted_array[j as u32] = temp; + } + } + } + sorted_array +} + +``` + +Like before, `noir-profiler` can be used to visualize the gate counts of the private functions, highlighting that 953 gates could be saved. + +Note: The stdlib provides a highly optimized version of sort on arrays, `array.sort()`, which saves even more gates. + +```rust + #[external("private")] + fn sort_stdlib(array: [u32; super::ARRAY_SIZE]) -> [u32; super::ARRAY_SIZE] { + array.sort() + } // 5943 gates (880 gates less) for 10 elements, 13308 gates for 100 elements (114472 gates less) +``` + +#### Example - refactoring arrays + +In the same vein, refactoring is inefficient when done constrained, and more efficient to do unconstrained then constrain the output. + +```rust +{ + #[external("private")] + fn refactor_inefficient(array: [u32; super::ARRAY_SIZE]) -> [u32; super::ARRAY_SIZE] { + let mut compacted_array = [0; super::ARRAY_SIZE]; + let mut index = 0; + for i in 0..super::ARRAY_SIZE as u32 { + if (array[i] != 0) { + compacted_array[index] = array[i]; + index += 1; + } + } + compacted_array + } // 6570 gates for 10 elements, 93071 gates for 100 elements + + #[external("private")] + fn refactor_efficient(array: [u32; super::ARRAY_SIZE]) -> [u32; super::ARRAY_SIZE] { + let compacted_array = unsafe { super::refactor_array(array) }; + // count non-zero elements in array + let mut count = 0; + for i in 0..super::ARRAY_SIZE as u32 { + if (array[i] != 0) { + count += 1; + } + } + // count non-zero elements in compacted_array + let mut count_compacted = 0; + for i in 0..super::ARRAY_SIZE as u32 { + if (compacted_array[i] != 0) { + count_compacted += 1; + } else { + assert(compacted_array[i] == 0, "trailing compacted_array elements should be 0"); + } + } + assert(count == count_compacted, "count should be equal to count_compacted"); + compacted_array + } // 5825 gates (745 gates less), 12290 gates for 100 elements (80781 gates less) +} + +unconstrained fn refactor_array(array: [u32; ARRAY_SIZE]) -> [u32; ARRAY_SIZE] { + let mut compacted_array = [0; ARRAY_SIZE]; + let mut index = 0; + for i in 0..ARRAY_SIZE as u32 { + if (array[i] != 0) { + compacted_array[index] = array[i]; + index += 1; + } + } + compacted_array +} + +``` + +### Optimizing: Reducing L2 reads + +If a struct has many fields to be read, we can design an extra variable maintained as the hash of all values within it (like a checksum). When it comes to reading, we can now do an unconstrained read (incurring no read requests), and then check the hash of the result against that stored for the struct. This final check is thus only one read request rather than one per variable. + +:::note Leverage unconstrained functions +When needing to make use of large private operations (eg private execution or many read requests), use of [unconstrained functions](https://noir-lang.org/docs/guides/thinking_in_circuits#leverage-unconstrained-execution) wisely to reduce the gate count of private functions. +::: diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/authentication_witnesses.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/authentication_witnesses.md new file mode 100644 index 00000000000..47a78c6bb30 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/authentication_witnesses.md @@ -0,0 +1,79 @@ +--- +title: Authentication Witnesses +description: Enable contracts to execute actions on behalf of user accounts using authentication witnesses. +tags: [accounts, authwit] +sidebar_position: 11 +references: ["labs/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr", "labs/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr"] +--- + +Authentication witnesses (authwit) allow other contracts to execute actions on behalf of your account. This guide shows you how to implement and use authwits in your Aztec smart contracts. + +import YouTubeEmbed from '@site/src/components/YouTubeEmbed'; + +For a video walkthrough of the concepts and the implementation pattern, watch this explainer (find more on the [video lessons](../../resources/video_lessons.mdx) page): + +<YouTubeEmbed videoId="VRZVOCdjGZ4" title="How Authorization Works on Aztec" /> + +## Prerequisites + +- An Aztec contract project set up with `aztec-nr` dependency +- Understanding of private and public functions in Aztec + +For conceptual background, see [Authentication Witnesses](../../foundational-topics/advanced/authwit.md). + +## Import the authwit library + +The `aztec` library includes authwit functionality. Import the necessary components: + +```rust +use aztec::{ + authwit::auth::{compute_authwit_message_hash_from_call, set_authorized}, + macros::functions::authorize_once, +}; +``` + +## Using the `authorize_once` macro + +The `#[authorize_once]` macro validates that a caller has authorization from the `from` address. It handles authwit verification and nullifier emission automatically. + +### Private function example + +#include_code transfer_in_private labs/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust + +### Public function example + +#include_code transfer_in_public labs/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust + +The macro parameters specify: + +- `"from"` - the parameter name containing the address that must have authorized the call +- `"authwit_nonce"` - the parameter name containing the nonce for replay protection + +## Setting authorization from contracts + +When a contract needs to authorize another contract to act on its behalf, use `set_authorized` to update the auth registry. This is common in bridge contracts where contract A authorizes contract B to perform actions. + +#include_code authwit_uniswap_set labs/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr rust + +Key steps: + +1. Compute the message hash using `compute_authwit_message_hash_from_call` +2. Call `set_authorized` to store the approval in the registry +3. Execute the authorized action + +When authorization and consumption happen in the same transaction, state changes are squashed, saving gas. + +## Canceling authwits + +Users can revoke an authwit before it's used by emitting its nullifier: + +#include_code cancel_authwit labs/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust + +:::note +The cancel transaction must be finalized before any transaction attempts to use the authwit. If both are pending simultaneously, the outcome depends on which the sequencer includes first. +::: + +## Next steps + +- [Using authwits in aztec.js](../../aztec-js/how_to_use_authwit.md) - Create and manage authwits from your client application +- [Authentication Witnesses concepts](../../foundational-topics/advanced/authwit.md) - Deeper explanation of the authwit mechanism diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/calling_contracts.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/calling_contracts.md new file mode 100644 index 00000000000..aff6c8d9cfc --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/calling_contracts.md @@ -0,0 +1,112 @@ +--- +title: Calling Other Contracts +sidebar_position: 5 +tags: [functions, contracts, composability] +description: Call functions in other contracts from your Aztec smart contracts to enable composability. +references: ["labs/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr"] +--- + +This guide shows you how to call functions in other contracts from your Aztec smart contracts. + +## Add the target contract as a dependency + +Add the contract you want to call to your `Nargo.toml` dependencies: + +```toml +[dependencies] +token = { git="https://github.com/aztec-labs-eng/aztec-node/", tag="#include_aztec_version", directory="noir-projects/noir-contracts/contracts/app/token_contract" } +``` + +Then import the contract interface at the top of your contract file: + +```rust +use token::Token; +``` + +## Call contract functions + +Use `self.call()` to call functions on other contracts: + +```rust +self.call(Token::at(token_address).transfer(recipient, amount)); +``` + +The pattern is: + +1. Form the call: `Contract::at(address).function_name(args)` +2. Execute it: `self.call(...)` or `self.view(...)` for read-only calls + +### Private-to-private calls + +#include_code private_call /labs/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr rust + +### Public-to-public calls + +From a public function, call other public functions directly: + +```rust +self.call(Token::at(token_address).transfer_in_public(recipient, amount)); +``` + +Capture return values by assigning the result: + +```rust +let balance = self.view(Token::at(token_address).balance_of_public(account)); +``` + +Use `self.view()` for read-only calls that cannot modify state. + +### Private-to-public calls + +From a private function, enqueue public function calls for later execution: + +```rust +self.enqueue(Token::at(token_address).mint_to_public(recipient, amount)); +``` + +:::info +Public functions execute after all private execution completes. Return values are not available in the private context. Learn more about [call types](../../foundational-topics/call_types.md). +::: + +## Utility calls + +Utility functions can call other utility functions. These calls run entirely offchain in PXE and are never proven, so no guarantees are made on the correctness of their results. + +### Utility-to-utility calls + +From a utility function, use `self.call()` as with other call types: + +```rust +let balance = self.call(Token::at(token_address).get_balance_of(owner)); +``` + +To call a utility function of your own contract, use the `self.call_self` stubs instead: + +```rust +self.call_self.my_utility_function(args) +``` + +### Private-to-utility calls + +Private functions can also call utility functions, through `self.utility`. The call executes unconstrained code, so it must be wrapped in `unsafe`, and its result is not proven: use it to inform logic, never as an input to a constrained assertion without validating it first. + +```rust +// Safety: result is unconstrained +let balance = unsafe { self.utility.call(Token::at(token_address).get_balance_of(owner)) }; +``` + +The same-contract stubs are available here as `self.utility.call_self`: + +```rust +unsafe { self.utility.call_self.my_utility_function(args) } +``` + +Public functions cannot call utility functions: they run on the sequencer, which has no access to private state or PXE. + +### Cross-contract authorization + +A utility call to the calling contract itself always succeeds. A call to a _different_ contract can expose that contract's private state to the caller, so PXE asks the wallet to authorize it before proceeding. If the wallet denies the call, or no `authorizeUtilityCall` hook is configured, the simulation fails with `Cross-contract utility call denied`. See [execution hooks](../../foundational-topics/pxe/execution_hooks.md#authorizeutilitycall) for how wallets handle these requests, and for authorizing targets in Noir tests with `with_authorized_utility_call_targets`. + +### `msg_sender` in nested utility calls + +A utility function called from another contract can read the calling contract's address via `self.msg_sender()`, mirroring private and public functions. A utility function invoked directly by an application has no caller: `self.msg_sender()` panics, and `self.context.maybe_msg_sender()` returns `Option::none()`. Within a simulation PXE takes this value from the actual call graph, but utility execution as a whole is unconstrained, so contracts must not rely on it as a security guarantee. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_artifact.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_artifact.md new file mode 100644 index 00000000000..03067f3c2c3 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_artifact.md @@ -0,0 +1,117 @@ +--- +title: Contract Artifacts +description: Understand the structure and contents of Aztec smart contract artifacts. +tags: [contracts] +sidebar_position: 13 +references: ["labs/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr", "labs/yarn-project/stdlib/src/abi/contract_artifact.ts"] +--- + +Compiling an Aztec contract produces a contract artifact file (`.json`) containing everything needed to interact with that contract: its name, functions, their interfaces, and compiled bytecode. Since private function bytecode is never published to the network, you need this artifact file to call private functions. + +:::tip Most developers don't need this +When you [compile a contract](../compiling_contracts.md) and use [`aztec codegen`](../../aztec-js/how_to_deploy_contract.md#generate-typescript-bindings), you get type-safe TypeScript classes that handle artifacts automatically. This page is useful if you're: +- Building custom tooling around Aztec contracts +- Debugging compilation or deployment issues +- Understanding what data is available in artifacts +::: + +## Where to Find Artifacts + +After running `aztec compile`, artifacts are output to the `target/` directory: + +``` +target/ +└── my_contract-MyContract.json # Contract artifact +``` + +Use `aztec codegen` to generate TypeScript bindings from these artifacts for type-safe contract interaction. + +## Contract Artifact Structure + +A contract artifact contains: + +- **`name`**: The contract name as defined in Noir +- **`functions`**: Array of function artifacts (private, public dispatch, and utility functions) +- **`nonDispatchPublicFunctions`**: Public function ABIs (excluding the dispatch function) +- **`outputs`**: Exported structs and globals from the contract +- **`storageLayout`**: Storage slot mappings for contract state +- **`fileMap`**: Source file mappings for debugging + +## Exported globals + +Add `#[abi(tag)]` to a public Noir global to export its name and value in the contract artifact. A tag groups related globals, and you can apply the same tag to multiple globals: + +```noir +use aztec::macros::aztec; + +#[aztec] +pub contract Globals { + #[abi(constants)] + pub global EXPORTED_FIELD_CONSTANT: Field = 1234; + #[abi(constants)] + pub global EXPORTED_STRING_CONSTANT: str<8> = "exported"; + #[abi(limits)] + pub global EXPORTED_LIMIT_CONSTANT: u32 = 100; + #[abi(constants)] + #[abi(limits)] + pub global EXPORTED_SHARED_CONSTANT: u32 = 7; +} +``` + +The example creates two groups under `outputs.globals`: `constants` and `limits`. Each group is an array of `{ name, value }` entries. Stacking attributes on `EXPORTED_SHARED_CONSTANT` exports the same global under both tags. + +When working directly with an artifact, use `getGlobalsByTag` to return the named entries for one tag as raw `AbiValue` objects: + +```typescript +import { getGlobalsByTag } from '@aztec-labs/aztec.js/abi'; + +const constants = getGlobalsByTag(GlobalsContract.artifact, 'constants'); +const exportedString = constants.EXPORTED_STRING_CONSTANT; +// { kind: 'string', value: 'exported' } +``` + +## Function Properties + +Each function in the artifact includes: + +| Property | Description | +|----------|-------------| +| `name` | Function name as defined in Noir | +| `functionType` | One of `private`, `public`, or `utility` | +| `isOnlySelf` | If `true`, function can only be called from within the same contract | +| `isStatic` | If `true`, function cannot alter state | +| `isInitializer` | If `true`, function can be used as a constructor | +| `parameters` | Array of input parameters with name, type, and visibility | +| `returnType` | The type the function returns, omitted when it returns nothing. Multiple return values are expressed as a single `tuple` type | +| `errorTypes` | Custom error types the function can throw | +| `bytecode` | Compiled ACIR bytecode (base64 encoded) | +| `verificationKey` | Verification key for private functions (optional) | +| `debugSymbols` | Compressed debug information linking to source code | + +### Function Types + +- **`private`**: Executed and proved locally by the client. Bytecode is not published to the network. +- **`public`**: Executed and proved by the sequencer. Bytecode is published to the network. +- **`utility`**: Executed locally to compute information (e.g., view functions). Cannot be called in transactions. + +## Parameter and Return Types + +Parameters and return values use these type definitions: + +| Type | Description | +|------|-------------| +| `field` | A field element in the BN254 curve's scalar field | +| `boolean` | True/false value | +| `integer` | Whole number with `sign` (`signed`/`unsigned`) and `width` (bits) | +| `array` | Collection of elements with `length` and element `type` | +| `string` | Character sequence with fixed `length` | +| `struct` | Composite type with named `fields` and a `path` identifier | +| `tuple` | Unnamed composite type with ordered `fields` | + +Parameter visibility can be `public`, `private`, or `databus`. + +## Next Steps + +- [Compile contracts](../compiling_contracts.md) to generate artifacts +- [Deploy contracts](../../aztec-js/how_to_deploy_contract.md) using generated TypeScript bindings +- [Send transactions](../../aztec-js/how_to_send_transaction.md) to interact with deployed contracts diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_structure.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_structure.md new file mode 100644 index 00000000000..5b118285d56 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_structure.md @@ -0,0 +1,175 @@ +--- +title: Contract Structure +sidebar_position: 1 +tags: [contracts] +description: Learn the fundamental structure of Aztec smart contracts including the contract keyword, directory layout, and how contracts manage state and functions. +references: ["docs/examples/contracts/counter_contract/src/main.nr"] +--- + +High-level structure of how Aztec smart contracts including the different components. + +## Directory structure + +When you create a new project with `aztec new my_project`, it generates a two-crate Noir workspace: a contract crate for your smart contract code and a sibling test crate for Noir tests. + +```text title="layout of an aztec contract project" +─── my_project + ├── Nargo.toml <-- workspace file ([workspace] members) + ├── my_project_contract + │ ├── Nargo.toml <-- contract package (type = "contract") + │ └── src + │ └── main.nr <-- your contract + └── my_project_test + ├── Nargo.toml <-- test package (type = "lib") + └── src + └── lib.nr <-- Noir tests +``` + +The top-level `Nargo.toml` is a workspace file. Contract dependencies live in `my_project_contract/Nargo.toml` (with `type = "contract"`). Tests live in the separate `my_project_test` crate and import the contract by package name (for example, `use my_project_contract::MyContract;`) — see [Testing Contracts](../testing_contracts.md). + +To add another contract to the same workspace, run `aztec new <name>` from inside the workspace directory; this adds a new `<name>_contract` and `<name>_test` crate pair. To initialize a project inside an existing empty directory, `cd` into it and run `aztec init`, which scaffolds the same two-crate layout pre-populated with a runnable [Counter example](../../tutorials/contract_tutorials/counter_contract.md) (use `aztec new` if you want a blank starting point instead). + +See the vanilla Noir docs for [more info on packages](https://noir-lang.org/docs/project_structure/crates_and_packages). + +## Contract block + +All contracts start with importing the required files and declaring a contract using the `contract` keyword: + +```rust +// import the `aztec` macro from Aztec.nr +use aztec::macros::aztec; + +// use the 'contract' keyword to declare a contract, applying the `aztec` macro +#[aztec] +pub contract MyContract { + // contract code here +} +``` + +By convention, contracts are named in `PascalCase`. + +The `#[aztec]` macro performs a lot of the low-level operations required to take a circuit language like Noir and build smart contracts out of it - including automatically creating external interfaces, inserting standard contract functions, etc. **All Aztec smart contracts must have this macro applied to them.** + +**Note:** each Noir crate (package) can only have _a single_ contract. If you are writing a multi-contract system, then each of them needs to be in their own separate crate. To learn more about crates and packages, visit the [Noir documentation](https://noir-lang.org/docs/project_structure/crates_and_packages). + +## Imports + +Aside from the [`#[aztec]`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/macros/fn.aztec) macro import, all other imports need to go _inside_ the `contract` block - this is because `contract` acts like `mod`, creating a new [module](https://noir-lang.org/docs/project_structure/modules). + +```rust +use aztec::macros::aztec; + +#[aztec] +pub contract MyContract { + // other imports go here + use aztec::state_vars::{PrivateMutable, PrivateSet}; +} +``` + +**Note:** [Noir's VSCode extension](../installation) is able to take care of most imports and put them in the correct place automatically. + +## State Variables + +With the boilerplate out of the way, it is now the time to begin defining the contract logic. It is recommended to start development by understanding the shape the _state_ of the contract will have: +- Which values will be private? +- Which will be public? +- What properties are required (is mutability or immutability needed? Is there a single global value, like a token total supply, or does each user get one, like a balance?). + +In Solidity, this is done by simply declaring variables inside of the contract, like so: + +```solidity +contract MyContract { + uint128 public my_public_state_variable; +} +``` + +In Aztec, defining state requires a few more steps, as there are both private and public variables (where these keywords refer to the privacy of the variable rather than their accessibility), and multiple _kinds_ of state variables. + +We define state using a [`struct`](https://noir-lang.org/docs/language/data_types/structs) that will hold the entire contract state. We call this struct _the storage struct_, and each variable inside this struct is called [_a state variable_.](./state_variables) + +```rust +use aztec::macros::aztec; + +#[aztec] +pub contract MyContract { + use aztec::{ + macros::storage, + state_vars::{Owned, PrivateMutable, PublicMutable} + }; + use uint_note::UintNote; + + // The storage struct must be named `Storage` and must have the `#[storage]` macro applied to it. + // This struct must also have a generic type called C or Context. + #[storage] + struct Storage<Context> { + // A private numeric value which can change over time. This value will be hidden, and only those with the secret can know its current value. + my_private_state_variable: Owned<PrivateMutable<UintNote, Context>, Context>, + // A public numeric value which can change over time. This value will be known to everyone and is equivalent to the Solidity example above. + my_public_state_variable: PublicMutable<u128, Context>, + } +} +``` + +## Events + +Like Solidity contracts, Aztec contracts can define events to notify that some state has changed. However, in Aztec, events can also be emitted privately, in which case only some users will learn of the event. + +[Events](./events_and_logs.md) are a struct marked with the `#[event]` macro: + +```rust +#[event] +struct Transfer { + from: AztecAddress, + to: AztecAddress, + amount: u128, +} +``` + +## Functions + +Contracts are interacted with by invoking their `external` [functions](./functions/index.md). There are three kinds of `external` functions: + +- External **private** functions, which reveal nothing about their execution and are executed off chain on the user's device, producing a zero-knowledge proof of execution that is sent to the network as part of a transaction. +- External **public** functions, which nodes in the network invoke publicly (like any `external` Solidity contract function). +- External **utility** functions, which are executed off chain on the user's device by applications in order to display useful information, e.g. retrieve contract state. These are never part of a transaction. + +```rust +use aztec::macros::aztec; + +#[aztec] +contract MyContract { + use aztec::macros::functions::external; + use aztec::protocol::address::AztecAddress; + + #[external("private")] + fn my_private_function(parameter_a: u128, parameter_b: AztecAddress) { + // ... + } + + #[external("public")] + fn my_public_function(parameter_a: u128, parameter_b: AztecAddress) { + // ... + } + + #[external("utility")] + unconstrained fn my_utility_function(parameter_a: u128, parameter_b: AztecAddress) { + // ... + } +} +``` + +Contracts can also define `internal` functions, which cannot be called by other contracts (like any `internal` Solidity function). These exist to help organize your code, reuse functionality, etc. + +### Current Limitations + +All `#[external]` contract functions must be defined _directly inside the `contract` block_, that is, in the same file. It is possible to define `#[internal]` and helper functions in `mod`s in other files, but not `#[external]` functions. + +**Noir does not feature inheritance** nor is there currently any other mechanism to extend and reuse contract logic. For example, you cannot take a token contract and extend it to add minting functionality, or reuse it in a liquidity pool. Like Vyper, the entire logic must live in a single file. + +We expect to lift some of these restrictions sometime after the release of Noir 1.0. + +## Next steps + +- [Define functions](./functions/index.md) - Learn about private, public, and utility functions +- [Define storage](./state_variables.md) - Work with persistent state variables +- [Compile your contract](../compiling_contracts.md) - Build your contract artifact diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_upgrades.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_upgrades.md new file mode 100644 index 00000000000..9e4904c5e18 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_upgrades.md @@ -0,0 +1,148 @@ +--- +title: Contract Upgrades +sidebar_position: 14 +tags: [contracts] +description: Understand contract upgrade patterns in Aztec and how to implement upgradeable contracts. +references: ["labs/noir-projects/aztec-nr/contract-instance-registry-interface/*", "labs/yarn-project/aztec.js/src/deployment/publish_class.ts"] +--- + +:::warning[Upgrades are not yet well supported by the framework] +Contract upgrades are currently a low-level protocol feature with minimal framework support. Before relying on them, be aware that: + +- Upgrades are hard to execute correctly, and need lots of careful testing. +- State migrations are non-trivial and also require extensive testing. The new implementation must be compatible with all existing state, including public storage, private notes, and initialization status. +- The `#[aztec]` macros are not at the moment built with upgrades in mind. Macro-generated code makes assumptions that upgrades can violate (for example, initialization checks in the new implementation can make all functions of already-deployed instances permanently uncallable), so users may need to drop the macros until support is added. +- There is no additional tooling at the moment to help with upgrades, such as detection of storage layout compatibility between the old and new implementations. + +All in all, this feature is not well supported by the framework as of now, and should only be used with extreme care. +::: + +Each contract instance refers to a contract class ID for its code. Upgrading a contract's implementation involves updating its current class ID to a new class ID, while retaining the original class ID for address verification. + +## Original class ID + +A contract stores the original contract class it was instantiated with. This original class ID is used when calculating and verifying the contract's [address](../../foundational-topics/contract_creation#instance-address) and remains unchanged even if a contract is upgraded. + +## Current class ID + +When a contract is first deployed, its current class ID equals its original class ID. The current class ID determines which code implementation the contract executes. + +During an upgrade: + +- The original class ID remains unchanged +- The current class ID is updated to the new implementation +- All contract state and data are preserved + +## How to upgrade + +Contract upgrades must be initiated by the contract itself calling the `ContractInstanceRegistry`: + +```rust +use aztec::protocol::{ + constants::CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, + contract_class_id::ContractClassId, +}; +use contract_instance_registry::ContractInstanceRegistry; + +#[external("private")] +fn update_to(new_class_id: ContractClassId) { + self.enqueue( + ContractInstanceRegistry::at(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS) + .update(new_class_id) + ); +} +``` + +:::info +To use the `ContractInstanceRegistry`, add this dependency to your `Nargo.toml`: +```toml +contract_instance_registry = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="contract-instance-registry-interface" } +``` +::: + +The `update` function in the registry is a public function, so you can enqueue it from a private function (as shown above) or call it directly from a public function. + +:::warning[Access Control] +The example `update_to` function above has no access control, meaning anyone could call it to upgrade your contract. Production contracts should implement proper authorization checks to secure against malicious upgrades. +::: + +Contract upgrades use a `DelayedPublicMutable` storage variable in the `ContractInstanceRegistry`, applying to both public and private functions. Upgrades have a delay before taking effect. The default delay is `86400` seconds (one day) but can be configured: + +```rust +#[external("private")] +fn set_update_delay(new_delay: u64) { + self.enqueue( + ContractInstanceRegistry::at(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS) + .set_update_delay(new_delay) + ); +} +``` + +The `new_delay` parameter is in seconds. Changing the update delay is also subject to the previous delay, so the first delay change takes `86400` seconds to take effect. + +:::info +The minimum update delay is `600` seconds. +::: + +### Transaction expiration + +When sending a transaction, the expiration timestamp is calculated as the current block timestamp plus the minimum update delay of all contracts you interact with, minus one second. The expiration timestamp is the last timestamp at which the transaction can be included. For example: + +- If you interact with contracts having delays of 1000 and 10000 seconds, expiration is current timestamp + 999 seconds +- If a contract has a pending upgrade in 100 seconds, expiration would be current timestamp + 99 seconds + +Other `DelayedPublicMutable` storage variables in your transaction may reduce the expiration timestamp further. + +:::note +Only deployed contract instances can upgrade or change their upgrade delay. This restriction may be lifted in the future. +::: + +### Upgrade process + +1. **Register the new implementation**: Register the new contract class if it contains public functions. The new implementation must maintain state variable compatibility with the original contract. + +2. **Perform the upgrade**: Call the update function with the new contract class ID. The contract's original class ID remains unchanged while the current class ID updates to the new implementation. + +3. **Wait for the delay**: The upgrade takes effect after the configured delay period. + +4. **Verify the upgrade**: After the delay, the contract executes functions from the new implementation. The contract address remains the same since it's based on the original class ID. + +### Interacting with an upgraded contract + +The PXE stores contract instances and classes locally. After a contract upgrades, you must register the new artifact with the wallet before interacting with it: + +```typescript +import { getContractClassFromArtifact } from '@aztec-labs/aztec.js/contracts'; +import { publishContractClass } from '@aztec-labs/aztec.js/deployment'; + +// Deploy the original contract (use .wait() to get both contract and instance) +const { contract, instance } = await UpdatableContract.deploy(wallet, ...args) + .send({ from: accountAddress }) + .wait(); + +// Publish the new contract class (required before upgrading) +await (await publishContractClass(wallet, UpdatedContractArtifact)) + .send({ from: accountAddress }) + .wait(); + +// Get the new contract class ID +const updatedContractClassId = ( + await getContractClassFromArtifact(UpdatedContractArtifact) +).id; + +// Trigger the upgrade +await contract.methods + .update_to(updatedContractClassId) + .send({ from: accountAddress }) + .wait(); + +// Wait for the upgrade delay to pass... + +// Register the new artifact with the wallet +await wallet.registerContract(instance, UpdatedContract.artifact); + +// Create a contract instance with the new artifact +const updatedContract = UpdatedContract.at(contract.address, wallet); +``` + +If you try to register a contract artifact that doesn't match the current contract class, the registration will fail. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/custom_notes.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/custom_notes.md new file mode 100644 index 00000000000..e321df53e55 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/custom_notes.md @@ -0,0 +1,204 @@ +--- +title: Custom notes +description: Learn how to create and use custom note types for specialized private data storage in Aztec contracts +sidebar_position: 6 +tags: [smart contracts, notes, privacy] +keywords: [implementing note, note, custom note] +references: ["labs/noir-projects/noir-contracts/contracts/app/nft_contract/*"] +--- + +This guide shows you how to create custom note types for storing specialized private data in your Aztec contracts. + +## Prerequisites + +- Basic understanding of [Aztec private state and notes](../../foundational-topics/state_management.md) +- Aztec development environment set up + +## When to create custom notes + +You may want to create your own note type if you need to: + +- Store specific data types not provided by built-in note libraries +- Combine multiple fields into a single note (e.g., game cards with multiple attributes) +- Implement custom nullifier schemes for advanced use cases + +:::info Built-in Note Types +Aztec.nr provides pre-built note types for common use cases: + +**UintNote** - For numeric values like token balances (supports partial notes): + +```toml +# In Nargo.toml +uint_note = { git="https://github.com/aztec-labs-eng/aztec-nr", tag="#include_aztec_version", directory="uint-note" } +``` + +**FieldNote** - For storing single Field values: + +```toml +# In Nargo.toml +field_note = { git="https://github.com/aztec-labs-eng/aztec-nr", tag="#include_aztec_version", directory="field-note" } +``` + +**AddressNote** - For storing Aztec addresses: + +```toml +# In Nargo.toml +address_note = { git="https://github.com/aztec-labs-eng/aztec-nr", tag="#include_aztec_version", directory="address-note" } +``` + +::: + +## Creating a custom note + +Define your custom note with the `#[note]` macro: + +#include_code nft_note_struct /docs/examples/contracts/nft/src/nft.nr rust + +The `#[note]` macro generates the following for your struct: + +- `NoteType` trait - Provides a unique type ID for the note +- `NoteHash` trait - Handles note hash and nullifier computation +- `NoteProperties` - Enables field selection when querying notes + +### Required traits + +Your note struct must derive: + +- `Packable` - Required by the `#[note]` macro for note hash computation. You can manually implement this for tighter packing — see [Data Packing and Serialization](./data_packing.md). +- `Eq` - Required by storage types like `PrivateSet` for note comparisons + +The `#[note]` macro handles the `NoteType`, `NoteHash`, and `NoteProperties` traits automatically. + +### How note hashing works + +When a note is inserted, the `#[note]` macro generates code that computes the note hash by combining: + +1. **Your packed note data** - The fields you define in your struct +2. **Owner address** - Provided by the storage variable +3. **Storage slot** - Determined by the storage layout +4. **Randomness** - Generated automatically to prevent brute-force attacks + +This happens automatically - you don't need to include owner or randomness fields in your struct. + +## Using notes in storage + +Notes are stored using `Owned<PrivateSet<...>>` which manages note ownership: + +```rust +use aztec::{ + macros::storage::storage, + state_vars::{Owned, PrivateSet}, +}; + +#[storage] +struct Storage<Context> { + // Collection of notes, indexed by owner + nfts: Owned<PrivateSet<NFTNote, Context>, Context>, +} +``` + +### Inserting notes + +#include_code mint /docs/examples/contracts/nft/src/main.nr rust + +### Reading and removing notes + +Use `pop_notes` to read and nullify notes atomically. This is the recommended pattern for most use cases: + +#include_code burn /docs/examples/contracts/nft/src/main.nr rust + +:::warning +There's also a `get_notes` function that reads without nullifying, but use it with caution - the returned notes may have already been spent in another transaction. +::: + +## Custom note hashing + +Most notes should use the standard `#[note]` macro. Use `#[custom_note]` only when you need: + +- Custom nullifier schemes (e.g., notes spendable by anyone with a secret, not tied to an owner) +- Partial notes that can be completed in public execution +- Non-standard hash computation for specific security requirements + +With `#[custom_note]`, you must implement the `NoteHash` trait yourself: + +```rust +use aztec::{ + context::PrivateContext, + keys::getters::{get_nhk_app, get_public_keys, try_get_public_keys}, + macros::notes::custom_note, + note::note_interface::NoteHash, + protocol::{ + address::AztecAddress, + constants::{DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_NULLIFIER}, + hash::poseidon2_hash_with_separator, + traits::Packable, + }, +}; + +#[derive(Eq, Packable)] +#[custom_note] +pub struct CustomHashNote { + pub data: Field, +} + +impl NoteHash for CustomHashNote { + fn compute_note_hash( + self, + owner: AztecAddress, + storage_slot: Field, + randomness: Field, + ) -> Field { + // Custom hash computation + poseidon2_hash_with_separator( + [self.data, owner.to_field(), storage_slot, randomness], + DOM_SEP__NOTE_HASH, + ) + } + + fn compute_nullifier( + self, + context: &mut PrivateContext, + owner: AztecAddress, + note_hash_for_nullification: Field, + ) -> Field { + // Standard nullifier using owner's nullifier hiding key + let owner_npk_m_hash = get_public_keys(owner).npk_m_hash; + let secret = context.request_nhk_app(owner_npk_m_hash); + poseidon2_hash_with_separator( + [note_hash_for_nullification, secret], + DOM_SEP__NOTE_NULLIFIER, + ) + } + + unconstrained fn compute_nullifier_unconstrained( + self, + owner: AztecAddress, + note_hash_for_nullification: Field, + ) -> Option<Field> { + try_get_public_keys(owner).map(|public_keys| { + let secret = get_nhk_app(public_keys.npk_m_hash); + poseidon2_hash_with_separator( + [note_hash_for_nullification, secret], + DOM_SEP__NOTE_NULLIFIER, + ) + }) + } +} +``` + +:::tip Naming note +The secret returned by `request_nhk_app` is the **nullifier hiding key** (abbreviated `nhk`). Older docs and code comments may call it the "nullifier secret key" (`nsk`) — these refer to the same key. Always use `request_nhk_app()` rather than computing this key yourself. +::: + +## Viewing notes (unconstrained) + +For read-only queries without constraints: + +#include_code view_notes /labs/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr rust + +## Further reading + +- [What the `#[note]` macro does](./functions/attributes.md#implementing-notes) +- [Note getter options](./advanced/how_to_retrieve_filter_notes.md) +- [Storage types](../../foundational-topics/state_management.md) +- [Macros reference](./macros.md) diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/data_packing.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/data_packing.md new file mode 100644 index 00000000000..b1866036d5d --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/data_packing.md @@ -0,0 +1,220 @@ +--- +title: Data Packing and Serialization +sidebar_position: 7 +tags: [contracts, storage, gas, optimization] +description: Understand Serialize, Deserialize, and Packable traits, when each is used, how to write custom packing, and the cost implications. +references: ["docs/examples/contracts/packing_example/src/types.nr"] +--- + +Aztec contracts use two separate encoding schemes to convert structs into `Field` arrays. This page tells you which one to reach for and when. + +- **`Serialize` / `Deserialize`**: ABI encoding. Used anywhere a value crosses the contract boundary (function arguments, return values, events). The layout must match Noir's intrinsic format, so you almost never hand-roll it. +- **`Packable`**: Storage encoding. Used wherever data is written to state or hashed into notes. The format is internal to your contract, so you can pack multiple small values into a single `Field` to save gas and proving time. + +Picking the wrong trait, or missing a chance to pack, can waste gas, storage slots, and proving time. + +## When to use Serialize and Deserialize + +`Deserialize` is needed for any struct accepted as a function argument; `Serialize` is needed for any struct returned from a function or emitted as an event. Because args and returns often share the same struct, `#[derive(Serialize, Deserialize)]` is the convenient default. + +The encoding must follow Noir's intrinsic serialization: each struct member becomes one or more `Field` values, with no packing or compression. When a transaction calls a public function, TypeScript serializes the arguments into an initial witness using Noir's built-in format. If your Noir-side implementation produces a different layout, you get an "arguments hash mismatch" error. + +```rust +// Matches Noir's intrinsic format automatically. +#[derive(Serialize, Deserialize)] +struct MyArgs { + amount: u128, // 1 Field + enabled: bool, // 1 Field + owner: Field, // 1 Field +} +// Serialize::N = 3 (one Field per member) +``` + +For events, `#[event]` auto-derives `Serialize` for you. You typically do not need `Deserialize` or `Packable` on events. + +:::warning +Do not hand-roll `Serialize` or `Deserialize` for types passed as function arguments. The encoding must match what TypeScript sends, and the derive macro ensures this automatically. +::: + +## When to use Packable + +Use `Packable` for note `structs` when creating custom notes or as the data type of a state variable (`PublicMutable<T>`, `PublicImmutable<T>`, `DelayedPublicMutable<T>`). `Packable` defines how the value is encoded when written to storage or hashed into a note. It never needs to match any external format; it only needs to roundtrip: `unpack(pack(x)) == x`. + +There are two cases. + +### Case 1: a custom note + +`#[note]` _requires_ the struct to implement `Packable` but does **not** add it for you. Place `#[derive(Packable)]` on the struct before applying `#[note]`, or the macro will fail compilation with an explicit message. + +When a note's members are already `Field`-sized, deriving is enough: + +#include_code field_like_note /docs/examples/contracts/packing_example/src/types.nr rust + +`Eq` is a Noir standard trait for equality comparisons (see [Noir's `Eq` trait](https://noir-lang.org/docs/language/traits)). `#[note]` does not require it, but deriving it is idiomatic because it enables `assert_eq` in tests and note-equality checks. `Serialize` and `Deserialize` are similarly optional here, and useful when a note type crosses a function boundary. + +This is how the built-in note types work too: [`AddressNote`](pathname:///aztec-nr-api/#api_ref_version/address_note/struct.AddressNote) and [`FieldNote`](pathname:///aztec-nr-api/#api_ref_version/field_note/struct.FieldNote) both derive `Packable` directly because their members are already `Field` or `AztecAddress`. + +When a note has members smaller than a `Field` (`bool`, `u8`, `u32`, `u64`), you can skip the `Packable` derive and write a custom implementation that packs multiple values into a single `Field`: + +#include_code card_note /docs/examples/contracts/packing_example/src/types.nr rust + +Derived `Packable` would give `CardNote` an `N = 2`; the custom impl halves that to `N = 1`. Smaller `N` means fewer inputs to the note hash, which directly reduces the gate count of private functions. + +### Case 2: a struct used as a state variable's data type + +Primitive types (`bool`, `u8` through `u128`, `Field`, `AztecAddress`) already implement `Packable`, so `PublicMutable<u64>` or `PublicImmutable<AztecAddress>` works out of the box. This section applies when the data type is a user-defined struct, for example `PublicMutable<MyStruct, Context>`. + +`#[storage]` requires every state variable's data type to implement `Packable`, but it does not add it for you. Put `#[derive(Packable)]` on the struct yourself. + +:::note +`DelayedPublicMutable<T>` also requires `T: Eq`. Add `Eq` to the derive list (`#[derive(Eq, Packable)]`) for structs used in this state variable. `PublicMutable<T>` and `PublicImmutable<T>` only need `Packable`. +::: + +When all members are already `Field`-sized (`Field`, `AztecAddress`, and types built from them), deriving is sufficient: + +#include_code derived_packable /docs/examples/contracts/packing_example/src/types.nr rust + +For [`PublicMutable<T>`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.PublicMutable), each element of the packed `[Field; N]` array maps directly to one `SLOAD` on read and one `SSTORE` on write, so a smaller `N` is a direct gas saving. [`PublicImmutable<T>`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.PublicImmutable) and [`DelayedPublicMutable<T>`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.DelayedPublicMutable) have additional overhead on top (see [Cost impact](#cost-impact) below), but also benefit from a smaller `N`. + +When your struct has members smaller than a `Field`, deriving still uses one whole `Field` per member, which is wasteful. The next section shows how to write a manual implementation that collapses them. + +## Writing a custom Packable implementation + +The technique is bit-packing with powers of 2, which maps efficiently to both AVM opcodes (public functions) and proving-backend primitives (private functions). + +Take a `GameState` struct with a `bool` and two `u32`s. Derived `Packable` gives `N = 3`. A manual implementation collapses it to `N = 1`: + +```rust +// Derived: N = 3 (one Field per member, no packing benefit) +#[derive(Packable)] +struct GameState { + started: bool, // 1 bit, but uses 1 whole Field + round: u32, // 32 bits, but uses 1 whole Field + score: u32, // 32 bits, but uses 1 whole Field +} +``` + +#include_code game_state_manual_packable /docs/examples/contracts/packing_example/src/types.nr rust + +`N = 1` means every storage read or write uses one `SLOAD` / `SSTORE` instead of three. The rest of this section walks through how the implementation was built. + +### Step 1: Determine bit widths + +For each member, determine how many bits it needs: + +| Type | Bit width | +| -------------- | ---------------------------------------------- | +| `bool` | 1 | +| `u8` | 8 | +| `u16` | 16 | +| `u32` | 32 | +| `u64` | 64 | +| `u128` | 128 | +| `Field` | up to 254 (cannot be packed with other values) | +| `AztecAddress` | up to 254 (wraps a `Field`) | + +A `Field` element is an integer modulo the BN254 scalar field prime: + +``` +p = 21888242871839275222246405745257275088548364400416034343698204186575808495617 +``` + +This prime is ~253.58 bits (slightly less than 2^254), so not every 254-bit value is a valid `Field`. To avoid modular wrap-around when packing, keep the sum of all member bit widths **≤ 253 bits**. For example, `u128 + u64 + u32 = 224 bits` packs safely, but `2 × u128 = 256 bits` does not. + +### Step 2: Pack by multiplying with powers of 2 + +`2.pow_32(k)` computes `2^k`, which is equivalent to a left shift by `k` bits for integer-valued `Field`s. Add the shifted values together to concatenate them inside a single `Field`: + +#include_code game_state_pack /docs/examples/contracts/packing_example/src/types.nr rust + +The bit layout you choose is arbitrary; the only requirement is that `pack` and `unpack` agree. The example above places `score` (a `u32`) at the lowest bits because a truncating cast (`packed[0] as u32`) then extracts it for free, with no subtraction or division. When you have a member whose width matches a standard `uN` type, putting it in the lowest position makes `unpack` cleaner. The remaining members can be placed in any order above it. + +### Step 3: Unpack by extracting from lowest bits upward + +Extract values from lowest bits first, to match how we packed them, subtracting each extracted value before extracting the next: + +#include_code game_state_unpack /docs/examples/contracts/packing_example/src/types.nr rust + +### Step 4: Write roundtrip tests + +Always test that `unpack(pack(x)) == x` for boundary values: + +#include_code game_state_tests /docs/examples/contracts/packing_example/src/types.nr rust + +## More custom Packable examples + +### Packing two u32 values into one Field + +#include_code card_custom_packable /docs/examples/contracts/packing_example/src/types.nr rust + +With derived `Packable`, `Card` would have `N = 2`. The manual implementation achieves `N = 1`, halving the storage cost. + +### Packing mixed-width integers + +#include_code mixed_width_packable /docs/examples/contracts/packing_example/src/types.nr rust + +A `u128` and a `u64` pack into a single `Field` (128 + 64 = 192 bits, well within the 253-bit safe limit), while the `AztecAddress` occupies a full `Field` on its own. This reduces `N` from 3 to 2. + +### Roundtrip tests + +Always test custom `Packable` implementations at boundary values: + +#include_code pack_unpack_tests /docs/examples/contracts/packing_example/src/types.nr rust + +## Cost impact + +### Public storage + +Each state variable type has a different storage-op profile, but all of them scale with `N`, the length of the packed `[Field; N]` array for the data type `T`. Reducing `N` reduces cost for every type. + +If a struct has `Packable::N = 4` with derived packing but could be manually packed to `N = 2`, you halve the public `SLOAD` / `SSTORE` count on every read and write. + +### Note hashing (private state) + +Note hashes are computed with Poseidon2 over the packed note data along with the storage slot, owner, and randomness. Fewer packed fields means fewer inputs to the hash, which directly reduces the gate count of private functions. + +### Calldata (function arguments) + +Function arguments use `Serialize`, not `Packable`. The number of fields in calldata affects L2 gas for deserialization. While you cannot change the encoding format (it must match TypeScript), you can reduce calldata size by restructuring your function signatures to pass fewer, larger arguments. + +## When is custom packing worth it? + +Custom packing is worth the effort when: + +- Your struct has **multiple sub-Field members** (bools, small integers) stored in a state variable or used in notes. +- The struct is **read or written frequently** (for example, game state updated every turn). +- You are hitting **gas limits** due to storage-heavy transactions. + +Custom packing is not needed when: + +- All struct members are `Field` or `AztecAddress` (already one Field each, no packing opportunity). +- The struct is used only as a function argument (must use `Serialize`, not `Packable`). +- The struct is small and accessed rarely. + +## Reference: which macros auto-derive which traits + +Aztec's macros only add a derive when the role of the struct strictly requires it. Everything else is on the developer. + +| Macro | Auto-derives `Serialize` | Auto-derives `Deserialize` | Auto-derives `Packable` | +| -------------------------- | ---------------------------- | -------------------------- | ----------------------------------------------------------------------------------------- | +| `#[event]` | Yes (if not already present) | No | No | +| `#[authorization]` | Yes (if not already present) | No | No | +| `#[note]` | No | No | No (but **requires** `Packable` to be implemented; the macro fails compilation otherwise) | +| `#[storage]` | No | No | No (state variable data types must implement `Packable` themselves) | +| `#[aztec]` / `#[contract]` | No | No | No | + +Implications: + +- **Function argument types** (both `#[public]` and `#[private]`): add `#[derive(Serialize, Deserialize)]` yourself. +- **Event types**: `#[event]` covers `Serialize`. You typically do not need `Deserialize` or `Packable` on events. +- **Note types**: place `#[derive(Packable)]` on the struct _before_ `#[note]`, unless you are providing a manual `impl Packable`. +- **State variable data types**: every type used inside `PublicMutable<T>`, `PublicImmutable<T>`, `DelayedPublicMutable<T>`, notes, etc. must implement `Packable`. `#[storage]` does not add this for you. + +## Summary + +- Use `#[derive(Serialize, Deserialize)]` for function arguments and return values. `#[event]` handles events. +- Use `#[derive(Packable)]` (or a manual impl) for note structs and state variable data types. `#[note]` requires `Packable` and will not add it for you. +- When a struct has multiple sub-`Field` members and is accessed frequently, manually implement `Packable` to pack values together using `2.pow_32()`. +- Keep the total packed bit width at or below 253 bits to stay within the BN254 field modulus. +- Always write roundtrip tests (`unpack(pack(x)) == x`) for custom implementations. +- Gas savings scale linearly with the reduction in `N`: halving `N` halves your storage operations. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/dependencies.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/dependencies.md new file mode 100644 index 00000000000..1f8c847633a --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/dependencies.md @@ -0,0 +1,120 @@ +--- +title: Aztec.nr Dependencies +description: Reference list of available Aztec.nr libraries and their Nargo.toml dependency paths. +tags: [contracts] +sidebar_position: 2 +references: ["labs/noir-projects/aztec-nr/address-note/*", "labs/noir-projects/aztec-nr/field-note/*", "labs/noir-projects/aztec-nr/uint-note/*", "labs/noir-projects/aztec-nr/ecdsa-public-key-note/*", "labs/noir-projects/aztec-nr/balance-set/*", "labs/noir-projects/aztec-nr/compressed-string/*", "labs/noir-projects/aztec-nr/contract-instance-registry-interface/*", "labs/noir-projects/aztec-nr/fee-juice-interface/*"] +--- + +This page lists the available Aztec.nr libraries. Add dependencies to the `[dependencies]` section of your `Nargo.toml`: + +```toml +[dependencies] +aztec = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="aztec" } +# Add other libraries as needed +``` + +## Core + +### Aztec (required) + +```toml +aztec = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="aztec" } +``` + +The core Aztec library required for every Aztec.nr smart contract. + +## Note Types + +### Address Note + +```toml +address_note = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="address-note" } +``` + +Provides `AddressNote`, a note type for storing `AztecAddress` values. + +### Field Note + +```toml +field_note = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="field-note" } +``` + +Provides `FieldNote`, a note type for storing a single `Field` value. + +### Uint Note + +```toml +uint_note = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="uint-note" } +``` + +Provides `UintNote`, a note type for storing `u128` values. Also includes `PartialUintNote` for partial note workflows where the value is completed in public execution. + +### ECDSA public key note + +```toml +ecdsa_public_key_note = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="ecdsa-public-key-note" } +``` + +Provides `EcdsaPublicKeyNote`, a note type for storing an ECDSA public key as its 32-byte `x` and `y` coordinates. + +## State Variables + +### Balance Set + +```toml +balance_set = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="balance-set" } +``` + +Provides `BalanceSet`, a state variable for managing private balances. Includes helper functions for adding, subtracting, and querying balances. + +## Utilities + +### Compressed String + +```toml +compressed_string = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="compressed-string" } +``` + +Provides `CompressedString` and `FieldCompressedString` utilities for working with compressed string data. + +## Protocol contract interfaces + +The protocol contracts do not generate Aztec.nr call interfaces. Depend on these crates to call them from your contract. + +### Contract instance registry + +```toml +contract_instance_registry = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="contract-instance-registry-interface" } +``` + +Provides the `ContractInstanceRegistry` interface, used for example to [upgrade a contract](./contract_upgrades.md). + +### Fee juice + +```toml +fee_juice = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="fee-juice-interface" } +``` + +Provides the `FeeJuice` interface, for example to read a public fee juice balance with `balance_of_public`. + +## Updating your aztec dependencies + +When `aztec compile` warns that your aztec dependency tag does not match the CLI version, update +the `tag` field in every Aztec.nr entry in your `Nargo.toml` to match the CLI version you are +running. + +For example, if your CLI is `#include_aztec_version`, change: + +```toml +aztec = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="v<old-version>", directory="aztec" } +``` + +to: + +```toml +aztec = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="aztec" } +``` + +Repeat for every other Aztec.nr dependency in your `Nargo.toml` (e.g. `address_note`, +`balance_set`, etc.). You can check your current CLI version with `aztec --version`. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/ethereum_aztec_messaging.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/ethereum_aztec_messaging.md new file mode 100644 index 00000000000..9d7a7c56115 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/ethereum_aztec_messaging.md @@ -0,0 +1,99 @@ +--- +title: Ethereum<>Aztec Messaging +tags: [contracts, portals] +sidebar_position: 12 +description: Send messages and data between L1 and L2 contracts using portal contracts and cross-chain messaging. +references: ["docs/examples/solidity/example_swap/ExampleTokenPortal.sol", "labs/noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr"] +--- + +This guide covers cross-chain communication between Ethereum (L1) and Aztec (L2) using portal contracts. + +Aztec uses an Inbox/Outbox pattern for cross-chain messaging. Messages sent from L1 are inserted into the `Inbox` contract and later consumed on L2. Messages sent from L2 are inserted into the `Outbox` contract and later consumed on L1. Portal contracts are L1 contracts that facilitate this communication for your application. + +## Prerequisites + +- An Aztec contract project with `aztec-nr` dependency +- Access to Ethereum development environment for L1 contracts +- Deployed portal contract on L1 (see [token bridge tutorial](../../tutorials/js_tutorials/token_bridge.md)) + +## L1 to L2 messaging + +### Send a message from L1 + +Use the `Inbox` contract's `sendL2Message` function: + +| Parameter | Type | Description | +| ------------- | --------- | ------------------------------------------------------- | +| `_recipient` | `L2Actor` | L2 contract address and rollup version | +| `_content` | `bytes32` | Hash of message content (use `Hash.sha256ToField`) | +| `_secretHash` | `bytes32` | Hash of secret for message consumption | + +#include_code deposit_to_aztec_public docs/examples/solidity/example_swap/ExampleTokenPortal.sol solidity + +:::note Message availability +L1 to L2 messages are not available immediately. The proposer batches messages from the Inbox and includes them in the next L2 block. You must wait for this before consuming the message on L2. +::: + +### Consume the message on L2 + +Call `consume_l1_to_l2_message` on the context. The `content` must match the hash sent from L1, and the `secret` must be the pre-image of the `secretHash`. Consuming a message emits a nullifier to prevent double-spending. + +The content hash must be computed identically on both L1 and L2. Create a shared library for your content hash functions—see [`token_portal_content_hash_lib`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/noir-projects/noir-contracts/contracts/libs/token_portal_content_hash_lib) for an example. + +#include_code claim_public labs/noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr rust + +This function works in both public and private contexts. + +## L2 to L1 messaging + +### Send a message from L2 + +Call `message_portal` on the context to send messages to your L1 portal: + +#include_code exit_to_l1_public labs/noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr rust + +This function works in both public and private contexts. + +### Consume the message on L1 + +Use the `Outbox` contract to consume L2 messages. + +:::note Message availability +L2 to L1 messages are only available after the epoch proof is submitted to L1. Since multiple L2 blocks fit within an epoch, there may be a delay—especially if the message was sent near the start of an epoch. +::: + +#include_code withdraw docs/examples/solidity/example_swap/ExampleTokenPortal.sol solidity + +:::info Getting the membership witness + +Compute the witness for the L2 to L1 message in TypeScript: + +```ts +import { computeL2ToL1MessageHash } from "@aztec-labs/stdlib/hash"; + +const l2ToL1Message = computeL2ToL1MessageHash({ + l2Sender: l2BridgeAddress, + l1Recipient: EthAddress.fromString(portalAddress), + content: withdrawContentHash, + rollupVersion: new Fr(version), + chainId: new Fr(chainId), +}); + +const witness = await aztecNode.getL2ToL1MembershipWitness( + txReceipt.txHash, + l2ToL1Message +); + +// Use witness.leafIndex and witness.siblingPath for the L1 consume call +``` + +::: + +## Example implementations + +- [Token Portal (L1)](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/docs/examples/solidity/example_swap/ExampleTokenPortal.sol) +- [Token Bridge (L2)](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr) + +## Next steps + +Follow the [token bridge tutorial](../../tutorials/js_tutorials/token_bridge.md) for a complete implementation example. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/events_and_logs.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/events_and_logs.md new file mode 100644 index 00000000000..d7ed4e0408a --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/events_and_logs.md @@ -0,0 +1,122 @@ +--- +title: Events and Logs +tags: [contracts, events] +sidebar_position: 8 +description: Learn how to emit events from your Aztec smart contracts for offchain applications to consume. +references: ["labs/noir-projects/aztec-nr/aztec/src/macros/events.nr", "labs/noir-projects/aztec-nr/aztec/src/event/*", "labs/noir-projects/aztec-nr/aztec/src/messages/delivery/*"] +--- + +Events allow contracts to communicate with offchain applications. Private events are encrypted and delivered to specific recipients, while public events are visible to everyone. + +## Prerequisites + +- An Aztec contract project set up with `aztec-nr` dependency +- Understanding of private vs public functions in Aztec + +## Define an event + +Declare events using the `#[event]` attribute: + +```rust +#[event] +struct Transfer { + from: AztecAddress, + to: AztecAddress, + amount: u128, +} +``` + +## Emit private events + +In private functions, emit events using `self.emit()` and deliver them to recipients: + +```rust +use aztec::messages::delivery::MessageDelivery; + +#[external("private")] +fn transfer(to: AztecAddress, amount: u128) { + let from = self.msg_sender(); + + // ... transfer logic ... + + self.emit(Transfer { from, to, amount }).deliver_to( + to, + MessageDelivery::onchain_unconstrained(), + ); +} +``` + +:::warning +You **must** call `deliver_to()` on the returned `EventMessage`. If you don't, the event information is lost forever. The compiler will warn you about unused `EventMessage` values. +::: + +### Deliver to multiple recipients + +You can deliver the same event to multiple recipients with different delivery modes: + +```rust +let message = self.emit(Transfer { from, to, amount }); +message.deliver_to(from, MessageDelivery::offchain()); +message.deliver_to(to, MessageDelivery::onchain_constrained()); +``` + +The `MessageDelivery` options are: + +- **`onchain_constrained()`** - Constrained encryption with onchain delivery. Slowest proving but provides cryptographic guarantees that recipients can decrypt messages. +- **`onchain_unconstrained()`** - Unconstrained encryption with onchain delivery. Faster proving, but trusts the sender to encrypt correctly. +- **`offchain()`** - Unconstrained encryption with offchain delivery. Lowest cost, but requires custom infrastructure to deliver messages to recipients. + +:::note +Emitting private events is optional. Onchain delivery publishes encrypted data to Ethereum blobs, inheriting Ethereum's data availability guarantees. You can choose to share information offchain instead. +::: + +## Emit public events + +In public functions, emit events using `self.emit()`: + +```rust +#[external("public")] +fn update_value(value: Field) { + // ... update logic ... + + self.emit(ValueUpdated { value }); +} +``` + +Public events are emitted as plaintext logs, similar to Solidity events. + +## Emit unstructured public logs + +For unstructured data, use `emit_public_log_unsafe` directly on the context. It takes a tag (placed at the first field of the emitted log, which nodes use to index logs) followed by the data: + +```rust +self.context.emit_public_log_unsafe(0, "My message"); +self.context.emit_public_log_unsafe(0, [1, 2, 3]); +``` + +The tag should be domain-separated to prevent collisions with unrelated log types. Prefer `self.emit(event)` where possible, which handles tagging automatically. + +## Query public logs + +Query public logs from offchain applications using the Aztec node. Raw public logs are +attached to each block's transaction effects — fetch a block with `includeTransactions: true` +and read `body.txEffects[*].publicLogs`: + +```typescript +const blockNumber = await node.getBlockNumber(); +const block = await node.getBlock(blockNumber, { includeTransactions: true }); +const publicLogs = block?.body.txEffects.flatMap(tx => tx.publicLogs) ?? []; +``` + +## Cost considerations + +Event data published onchain is stored in Ethereum blobs, which incurs costs. Consider: + +- Use offchain delivery for lower costs when you have custom delivery infrastructure +- Only emit events when necessary for your application's functionality + +## Next steps + +- Learn about [storage](./state_variables.md) to persist data in your contracts +- Explore [calling other contracts](./calling_contracts.md) for cross-contract interactions +- Understand [cross-chain communication](./ethereum_aztec_messaging.md) between Ethereum and Aztec diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/attributes.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/attributes.md new file mode 100644 index 00000000000..454bb1a2ec0 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/attributes.md @@ -0,0 +1,285 @@ +--- +title: Attributes and Macros +sidebar_position: 6 +tags: [functions] +description: Reference for Aztec contract attributes that control function visibility, execution context, storage, and notes. +references: ["labs/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr"] +--- + +This page documents the attributes (macros) available in Aztec.nr for defining contract functions, storage, and notes. + +## Quick reference + +| Attribute | Applies to | Purpose | +|-----------|-----------|---------| +| `#[external("private")]` | functions | Client-side private execution with proofs | +| `#[external("public")]` | functions | Sequencer-side public execution | +| `#[external("utility")]` | functions | Unconstrained queries, not included in transactions | +| `#[internal("private")]` | functions | Private helper functions, inlined at call sites | +| `#[internal("public")]` | functions | Public helper functions, inlined at call sites | +| `#[view]` | functions | Prevents state modification | +| `#[initializer]` | functions | Contract constructor | +| `#[noinitcheck]` | functions | Callable before contract initialization | +| `#[allow_phase_change]` | functions | Allows for phase change to happen during the function's execution | +| `#[only_self]` | functions | Only callable by the same contract | +| `#[authorize_once]` | functions | Requires authwit authorization with replay protection | +| `#[note]` | structs | Defines a private note type | +| `#[custom_note]` | structs | Defines a note with custom hash/nullifier logic | +| `#[storage]` | structs | Defines contract storage layout | +| `#[storage_no_init]` | structs | Storage with manual slot allocation | + +For macro internals, see the [macros reference](../macros.md). + +# External functions #[external("...")] + +Like in Solidity, external functions can be called from outside the contract. +There are 3 types of external functions differing in the execution environment they are executed in: private, public, and utility. +We will describe each type in the following sections. + +## Private functions #[external("private")] + +A private function operates on private information, and is executed by the user on their device. Annotate the function with the `#[external("private")]` attribute to tell the compiler it's a private function. This will make the [private context](./context.md#the-private-context) available within the function's execution scope. The compiler will create a circuit to define this function. + +`#[external("private")]` is just syntactic sugar. At compile time, the Aztec.nr framework inserts code that allows the function to interact with the [kernel](../../../foundational-topics/advanced/circuits/private_kernel.md). + +If you are interested in what exactly the macros are doing we encourage you to run `aztec-nargo expand` on your contract. +This will display your contract's code after the transformations are performed. + +(If you are using VSCode you can display the expanded code by pressing `CMD + Shift + P` and typing `nargo expand` and selecting `Noir: nargo expand on current package`. Make sure the Noir extension's `Nargo Path` is set to `aztec-nargo` — see the [Noir VSCode extension guide](../../installation.md) for setup.) + +Under the hood, the macro: + +- Creates a `PrivateContext` from kernel-provided inputs (chain ID, block data, etc.) +- Initializes the `self` object with context and storage +- Hashes function inputs for the kernel (enabling variable argument counts) +- Returns execution results via `PrivateCircuitPublicInputs` (nullifiers, messages, return values) + +## Utility functions #[external("utility")] + +Utility functions perform state queries from an offchain client and are never included in transactions. They can access both private and public state, and can modify local PXE state (e.g., processing logs). Since execution is unconstrained and relies on [oracle calls](https://noir-lang.org/docs/guides/oracles), no guarantees are made on result correctness. + +A reasonable mental model is a Solidity `view` function that can only be invoked via `eth_call`, never in a transaction. Unlike Solidity `view` functions, utility functions can also modify local offchain PXE state. + +#include_code balance_of_private /labs/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust + +:::info +Utility functions can access both private and historical public data since they're not part of transactions—there's no risk of using stale or unverified state. +::: + +## Public functions #[external("public")] + +A public function is executed by the sequencer and has access to a state model that is very similar to that of the EVM and Ethereum. Even though they work in an EVM-like model for public transactions, they are able to write data into private storage that can be consumed later by a private function. + +:::note +All data inserted into private storage from a public function will be publicly viewable (not private). +::: + +To create a public function you can annotate it with the `#[external("public")]` attribute. This will make the public context available within the function's execution scope. + +#include_code set_minter /labs/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust + +Under the hood, the macro: + +- Creates a `PublicContext` object that provides access to public state and transaction information +- Initializes the storage struct if one is defined +- Wraps the function body in a scope that handles context setup and return values +- Marks the function as `pub` and `unconstrained`, meaning it doesn't generate proofs and is executed directly by the sequencer + +To see the exact generated code, run `aztec-nargo expand` on your contract. + +## Constrained `view` Functions #[view] + +The `#[view]` attribute can be applied to a `#[external("private")]` or a `#[external("public")]` function and it guarantees that the function cannot modify any contract state (just like `view` functions in Solidity). + +## `Initializer` Functions #[initializer] + +This is used to designate functions as initializers (or constructors) for an Aztec contract. These functions are responsible for setting up the initial state of the contract when it is first deployed. The macro does two important things: + +- `assert_initialization_matches_address_preimage(context)`: This checks that the arguments and sender to the initializer match the commitments from the address preimage +- `mark_as_initialized(&mut context)`: This is called at the end of the function to emit the initialization nullifier, marking the contract as fully initialized and ensuring this function cannot be called again + +Key things to keep in mind: + +- A contract can have multiple initializer functions defined, but only one initializer function should be called for the lifetime of a contract instance +- Other functions in the contract will have an initialization check inserted, ie they cannot be called until the contract is initialized, unless they are marked with [`#[noinitcheck]`](#noinitcheck) + +## #[noinitcheck] + +In normal circumstances, all functions in an Aztec contract (except initializers) have an initialization check inserted at the beginning of the function body. This check ensures that the contract has been initialized before any other function can be called. However, there may be scenarios where you want a function to be callable regardless of the contract's initialization state. This is when you would use `#[noinitcheck]`. + +When a function is annotated with `#[noinitcheck]`: + +- The Aztec macro processor skips the [insertion of the initialization check](#initializer-functions-initializer) for this specific function +- The function can be called at any time, even if the contract hasn't been initialized yet + +## #[only_self] + +External functions marked with #[only_self] attribute can only be called by the contract itself - if other contracts try to make the call it will fail. + +This attribute is commonly used when an action starts in private but needs to be completed in public. The public +function must be marked with #[only_self] to restrict access to only the contract itself. A typical example is a private +token mint operation that needs to enqueue a call to a public function to update the publicly tracked total token +supply. + +It is also useful in private functions when dealing with tasks of an unknown size but with a large upper bound (e.g. when needing to process an unknown amount of notes or nullifiers) as they allow splitting the work in multiple circuits, possibly resulting in performance improvements for low-load scenarios. + +This macro inserts a check at the beginning of the function to ensure that the caller is the contract itself. This is done by adding the following assertion: + +```rust +assert(self.msg_sender() == self.address, "Function can only be called internally"); +``` + +## #[allow_phase_change] + +Private functions normally include a check to validate the current transaction phase. The `#[allow_phase_change]` attribute skips this validation, allowing the function to handle phase transitions internally. + +This is primarily used in account contract entrypoints that need to handle fee payment methods spanning multiple phases: + +```rust +#[external("private")] +#[allow_phase_change] +fn entrypoint(app_payload: AppPayload, fee_payment_method: u8, cancellable: bool) { + // Handle different fee payment methods that may span phases +} +``` + +## #[authorize_once] + +The `#[authorize_once]` attribute enables authorization checks via the [authwit mechanism](../../../foundational-topics/advanced/authwit.md) with replay protection. Use this when a function performs actions on behalf of someone who is not the caller. + +```rust +#[authorize_once("from", "authwit_nonce")] +#[external("public")] +fn transfer_in_public(from: AztecAddress, to: AztecAddress, amount: u128, authwit_nonce: Field) { + // Transfer tokens from 'from' to 'to' +} +``` + +The macro: +- Verifies the caller is authorized to act on behalf of the `from` address +- Emits the authorization request as an offchain effect for wallet verification +- Consumes a nullifier with the provided nonce, preventing replay attacks + +## Internal functions #[internal("...")] + +Internal functions are callable only from within the same contract and are inlined at call sites (like Solidity's internal functions). Unlike `#[only_self]`, they don't create a separate call—the code is directly inserted where called. + +```rust +#[internal("private")] +fn _prepare_private_balance_increase(to: AztecAddress) -> PartialNote { + // Helper logic for private balance operations +} + +#[internal("public")] +fn _finalize_transfer(from: AztecAddress, amount: u128) { + // Helper logic for public finalization +} +``` + +Call internal functions via `self.internal`: + +```rust +let partial = self.internal._prepare_private_balance_increase(recipient); +``` + +Key differences from `#[only_self]`: +- **Inlined**: Code is inserted at call site, not a separate circuit/call +- **Private internal**: Can only be called from private external or internal functions +- **Public internal**: Can only be called from public external or internal functions + +## Implementing notes + +The `#[note]` attribute is used to define notes in Aztec contracts. + +When a struct is annotated with `#[note]`, the Aztec macro applies a series of transformations and generates implementations to turn it into a note that can be used in contracts to store private data. + +1. **NoteType trait**: Provides a unique identifier for the note type via `get_id()` + +2. **NoteHash trait**: Implements note hash and nullifier computation: + - `compute_note_hash(self, owner, storage_slot, randomness)` - computes the note's hash + - `compute_nullifier(self, context, owner, note_hash_for_nullification)` - computes the nullifier using the owner's nullifying key + - `compute_nullifier_unconstrained(self, owner, note_hash_for_nullification)` - unconstrained version for use outside circuits + +3. **NoteProperties struct**: A separate struct is generated to describe the note's fields, which is used for efficient retrieval of note data + +### Example + +```rust +#[note] +pub struct CustomNote { + value: Field, +} +``` + +The `owner` is passed as a runtime parameter to the `compute_note_hash` and `compute_nullifier` functions, not stored as a field on the note. + +To see the exact generated code, run `aztec-nargo expand` on your contract. + +Key things to keep in mind: + +- The note struct must implement or derive the `Packable` trait +- Developers can use `#[custom_note]` instead of `#[note]` to provide their own `NoteHash` implementation +- The note's fields are automatically serialized and deserialized in the order they are defined in the struct + +## Storage struct #[storage] + +The `#[storage]` attribute is used to define the storage structure for an Aztec contract. + +When a struct is annotated with `#[storage]`, the macro: + +1. **Context Injection**: Injects a `Context` generic parameter into the storage struct and all its fields, allowing storage to interact with the Aztec context + +2. **Storage Implementation Generation**: Generates an `impl` block with an `init` function that initializes each storage variable with its assigned slot + +3. **Storage Slot Assignment**: Automatically assigns storage slots to each field based on their serialized length + +4. **Storage Layout Generation**: Creates a `StorageLayout` struct exported via `#[abi(storage)]` for use in the contract artifact + +### Example + +```rust +#[storage] +struct Storage { + balance: PublicMutable<Field>, + owner: PublicMutable<AztecAddress>, + token_map: Map<AztecAddress, Field>, +} +``` + +To see the exact generated code, run `aztec-nargo expand` on your contract. Alternatively, use `#[storage_no_init]` if you need manual control over storage slot allocation. + +Key things to keep in mind: + +- Only one storage struct can be defined per contract, and it must be named `Storage` +- `Map` types and private `Note` types always occupy a single storage slot + +## #[storage_no_init] + +The `#[storage_no_init]` attribute is an alternative to `#[storage]` that gives you manual control over storage slot allocation. Use this when you need custom slot assignments or want to maintain compatibility with existing storage layouts. + +With `#[storage_no_init]`, you must provide your own `init` function: + +```rust +#[storage_no_init] +struct Storage<Context> { + balance: PublicMutable<Field, Context>, + owner: PublicMutable<AztecAddress, Context>, +} + +impl<Context> Storage<Context> { + fn init(context: Context) -> Self { + Storage { + balance: PublicMutable::new(context, 1), // Explicit slot assignment + owner: PublicMutable::new(context, 5), // Non-sequential slot + } + } +} +``` + +Unlike `#[storage]`, this macro does not generate: +- The `init` function (you must implement it) +- The `StorageLayout` struct for the contract artifact + +## Further reading + +- [Macros reference](../macros.md) diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/context.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/context.md new file mode 100644 index 00000000000..a4b03b5abbf --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/context.md @@ -0,0 +1,133 @@ +--- +title: Understanding Function Context +sidebar_position: 3 +tags: [functions, context] +description: Learn about the execution context available to Aztec contract functions, including caller information and block data. +references: ["labs/noir-projects/aztec-nr/aztec/src/context/*"] +--- + +import Image from '@theme/IdealImage'; + +## What is the context + +The context is an object that is made available within every function in `Aztec.nr`. As mentioned in the [kernel circuit documentation](../../../foundational-topics/advanced/circuits/private_kernel.md). At the beginning of a function's execution, the context contains all of the kernel information that application needs to execute. During the lifecycle of a transaction, the function will update the context with each of its side effects (created notes, nullifiers etc.). At the end of a function's execution the mutated context is returned to the kernel to be checked for validity. + +Behind the scenes, Aztec.nr will pass data the kernel needs to and from a circuit, this is abstracted away from the developer. In a developer's eyes, the context is a useful structure that allows you to access and mutate the state of the Aztec blockchain. + +On this page, you'll learn + +- The details and functionalities of the private context in Aztec.nr +- Difference between the private and public contexts and their unified APIs +- Components of the private context, such as inputs and block header. +- Elements like return values, read requests, new note hashes, and nullifiers in transaction processing +- Differences between the private and public contexts, especially the unique features and variables in the public context + +## Two contexts, one API + +The `Aztec` blockchain contains two environments - public and private. + +- Private, for private transactions taking place on user's devices. +- Public, for public transactions taking place on the network's sequencers. + +As there are two distinct execution environments, they both require slightly differing execution contexts. Despite their differences, the APIs for interacting with each are unified. Leading to minimal context switch when working between the two environments. + +The following section will cover both contexts. + +## The Private Context + +The code snippet below shows what is contained within the private context. +#include_code private-context /labs/noir-projects/aztec-nr/aztec/src/context/private_context.nr rust + +### Private Context Broken Down + +#### Inputs + +The context inputs includes all of the information that is passed from the kernel circuit into the application circuit. It contains the following values. + +#include_code private-context-inputs /labs/noir-projects/aztec-nr/aztec/src/context/inputs/private_context_inputs.nr rust + +As shown in the snippet, the application context is made up of 3 main structures. The call context, the block header, and the private global variables. + +First of all, the [`CallContext`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/protocol/abis/call_context/struct.CallContext.html), which contains information about the current call being made: + +1. Msg Sender + - The message sender is the account (Aztec Contract) that sent the message to the current context. In the first call of the kernel circuit (often the account contract call), this value will be empty. For all subsequent calls the value will be the previous call. + +> The graphic below illustrates how the message sender changes throughout the kernel circuit iterations. + +<Image img={require("@site/static/img/context/sender_context_change.png")} /> + +2. Contract address + + - This value is the address of the current context's contract address. This value will be the value of the current contract that is being executed. + +3. Flags + - Furthermore there are a series of flags that are stored within the application context: + - is_static_call: This will be set if and only if the current call is a static call. In a static call, state changing altering operations are not allowed. + +### Block Header + +Another structure that is contained within the context is the [`BlockHeader`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/protocol/abis/block_header/struct.BlockHeader.html) object, which is the header of the block used to generate proofs against. + +### Transaction Context + +The private context provides access to the [`TxContext`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/protocol/abis/transaction/tx_context/struct.TxContext.html) as well, which holds user-defined values for the transaction in general that stay constant throughout its execution. + +### Args Hash + +To allow for flexibility in the number of arguments supported by Aztec functions, all function inputs are reduced to a singular value which can be proven from within the application. + +The `args_hash` is the result of poseidon2 hashing all of a function's inputs. + +### Return Values + +The return values are a set of values that are returned from an applications execution to be passed to other functions through the kernel. Developers do not need to worry about passing their function return values to the `context` directly as `Aztec.nr` takes care of it for you. See the documentation surrounding `Aztec.nr` [macro expansion](./attributes.md#private-functions-externalprivate) for more details. + +```rust +return_hash: Field, +``` +## Expiration Timestamp + +Some data structures impose time constraints, e.g. they may make it so that a value can only be changed after a certain delay. Interacting with these in private involves creating proofs that are only valid as long as they are included before a certain future point in time. To achieve this, the `set_expiration_timestamp` function can be used to set this property: + +#include_code expiration-timestamp /labs/noir-projects/aztec-nr/aztec/src/context/private_context.nr rust + +A transaction that sets this value will never be included in a block with a timestamp larger than the requested value, since it would be considered invalid. This can also be used to make transactions automatically expire after some time if not included. + +### Read Requests + +Read requests are used to prove that certain notes existed at a specific point in time. When a private function reads a note, it generates a read request that gets validated by the kernel circuit to ensure the note was valid at the time of the transaction. + +### New Note Hashes + +New note hashes contains an array of all of the note hashes created in the current execution context. + +### New Nullifiers + +New nullifiers contains an array of the new nullifiers emitted from the current execution context. + +### Nullified Note Hashes + +Nullified note hashes is an optimization for introduced to help reduce state growth. There are often cases where note hashes are created and nullified within the same transaction. +In these cases there is no reason that these note hashes should take up space on the node's commitment/nullifier trees. Keeping track of nullified note hashes allows us to "cancel out" and prove these cases. + +### Private Call Stack + +The private call stack contains all of the external private function calls that have been created within the current context. Any function call objects are hashed and then pushed to the execution stack. +The kernel circuit will orchestrate dispatching the calls and returning the values to the current context. + +### Public Call Stack + +The public call stack contains all of the external function calls that are created within the current context. Like the private call stack above, the calls are hashed and pushed to this stack. Unlike the private call stack, these calls are not executed client side. Whenever the function is sent to the network, it will have the public call stack attached to it. At this point the sequencer will take over and execute the transactions. + +### New L2 to L1 msgs + +New L2 to L1 messages contains messages that are delivered to the l1 outbox on the execution of each rollup. + +## Public Context + +The Public Context includes all of the information passed from the `Public VM` into the execution environment. Its interface is very similar to the [Private Context](#the-private-context), however it has some minor differences (detailed below). + +### Public Global Variables + +The public global variables ([`GlobalVariables`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/protocol/abis/global_variables/struct.GlobalVariables.html)) are provided by the rollup sequencer and consequently contain some more values than the private global variables. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/function_transforms.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/function_transforms.md new file mode 100644 index 00000000000..6f02a1ff2da --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/function_transforms.md @@ -0,0 +1,174 @@ +--- +title: Inner Workings of Functions +sidebar_position: 5 +tags: [functions] +description: Understand how Aztec transforms contract functions during compilation for privacy and efficiency. +references: ["labs/noir-projects/aztec-nr/aztec/src/macros/*", "labs/noir-projects/aztec-nr/aztec/src/context/*"] +--- + +This page explains what happens under the hood when you create a function in an Aztec contract. The [next page](./attributes.md) covers what the function attributes do. + +## Overview + +Private functions in Aztec compile to standalone circuits that must conform to the protocol's kernel circuit interface. Public functions compile to AVM bytecode. The transformations described below bridge the gap between developer-friendly Aztec.nr syntax and these underlying requirements. + +Utility functions (marked with `#[external("utility")]`) do not undergo these transformations—they remain as regular Noir functions. + +## Function transformation + +When you define a private or public function in an Aztec contract, it undergoes several transformations during compilation: + +- [Creating a context for the function](#context-creation) +- [Handling function inputs](#private-and-public-input-injection) +- [Processing return values](#return-value-handling) + +## Context creation + +Every function in an Aztec contract operates within a specific context that provides execution information and functionality. This is either a `PrivateContext` or `PublicContext` object, depending on whether it is a private or public function. + +### Private functions + +For private functions, context creation involves serializing and hashing all input parameters: + +```rust +// Parameters are serialized into an array +let serialized_args: [Field; N] = /* serialized parameters */; + +// Hash the arguments using poseidon2 +let args_hash = aztec::hash::hash_args(serialized_args); + +// Create the context with the inputs and args hash +let mut context = PrivateContext::new(inputs, args_hash); +``` + +This hashing is important because the kernel circuit uses it to verify the function received the correct parameters without exposing the input data. + +### Public functions + +For public functions, context creation uses a lazy evaluation pattern: + +```rust +let mut context = PublicContext::new(|| { + // compute args hash when needed + hash_args(serialized_args) +}); +``` + +### Using the context + +The context object provides methods for interacting with the blockchain. Storage access and contract calls are handled through a `ContractSelf` wrapper that the macros generate automatically. + +## Private and public input injection + +An additional parameter is automatically added to every private function. + +The injected input is always the first parameter of the transformed function and is of type `PrivateContextInputs` for private functions. + +Original function definition: + +```rust +fn my_function(param1: Type1, param2: Type2) { ... } +``` + +Transformed function with injected input: + +```rust +fn my_function(inputs: PrivateContextInputs, param1: Type1, param2: Type2) { ... } +``` + +The `PrivateContextInputs` struct contains: + +- `call_context` - information about how the function was called (msg_sender, contract_address, function_selector, is_static_call) +- `anchor_block_header` - the historical block header used during private execution +- `tx_context` - transaction-level data (chain_id, version, gas_settings) +- `start_side_effect_counter` - the side effect counter at function entry + +These inputs are made available through the `PrivateContext` object within your function. + +Public functions run in the AVM and access their context data through AVM opcodes rather than injected inputs. + +## Return value handling + +Return values in Aztec contracts are processed differently from traditional smart contracts. + +### Private functions + +For private functions, the return value is serialized, hashed, and stored in the context: + +```rust +// The original return value is captured +let macro__returned__values = original_return_expression; + +// The return value is serialized and hashed +let serialized_return: [Field; N] = /* serialized return value */; +self.context.set_return_hash(serialized_return); +``` + +The function's return type is changed to `PrivateCircuitPublicInputs`, which is returned by calling `context.finish()` at the end of the function. + +This process allows the return values to be included in the function's computation result while maintaining privacy. The actual return values are stored in the execution cache and can be retrieved by the caller using the hash. + +### Public functions + +In public functions, the return value is handled directly by the AVM and the function's return type remains as specified by the developer. + +## Function signature generation + +Each contract function has a unique 4-byte function selector. The selector is computed by hashing the function's signature string using Poseidon2: + +```rust +impl FunctionSelector { + pub fn from_signature<let N: u32>(signature: str<N>) -> Self { + let bytes = signature.as_bytes(); + let hash = poseidon2_hash_bytes(bytes); + // hash is truncated to fit within 32 bits (4 bytes) + FunctionSelector::from_field(hash) + } +} +``` + +The signature string follows the format `function_name(param_types)`. For example, `transfer(Field,Field)`. + +This approach is inspired by Solidity's function selector mechanism, but uses Poseidon2 instead of Keccak-256 for compatibility with Aztec's circuit-friendly hash functions. + +## Contract artifacts + +Contract artifacts are automatically generated structures that describe the contract's interface. They preserve the original function signatures (parameters and return types) before macro transformations are applied. + +For each function in the contract, an ABI export is generated with: + +1. A parameters struct containing all function parameters +2. An ABI struct marked with `#[abi(functions)]` containing the parameters and return type + +For example, given a function: + +```rust +fn increment(owner: AztecAddress) -> Field { ... } +``` + +The following structs are generated: + +```rust +pub struct increment_parameters { + pub owner: AztecAddress +} + +#[abi(functions)] +pub struct increment_abi { + parameters: increment_parameters, + return_type: Field +} +``` + +The `#[abi(functions)]` attribute marks the struct for inclusion in the contract ABI's `outputs.functions` array. This is important because macro processing changes the actual return type of private functions to `PrivateCircuitPublicInputs`, but the toolchain needs access to the original signatures. + +Contract artifacts enable: + +- Machine-readable contract interface descriptions +- TypeScript binding generation (see [how to compile contracts](../../compiling_contracts.md)) +- Function return value decoding in the simulator + +## Further reading + +- [Function attributes and macros](./attributes.md) +- [Aztec.nr macro source code](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/noir-projects/aztec-nr/aztec/src/macros) - for those who want to see the actual transformation implementation diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md new file mode 100644 index 00000000000..da818aa1544 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md @@ -0,0 +1,125 @@ +--- +title: How to Define Functions +sidebar_position: 1 +tags: [functions, smart-contracts] +description: Define different types of functions in your Aztec contracts for private, public, and utility execution. +references: ["docs/examples/contracts/bob_token_contract/src/main.nr", "docs/examples/contracts/counter_contract/src/main.nr"] +--- + +## Overview + +This guide shows you how to define different types of functions in your Aztec contracts, each serving specific purposes and execution environments. + +## Quick reference + +| Annotation | Execution | State access | +| ------------------------ | ----------------- | ------------------------------------------------------------ | +| `#[external("private")]` | User device | Private state (and selected public values via storage types) | +| `#[external("public")]` | Sequencer | Public state | +| `#[external("utility")]` | Offchain client | Public + private (unconstrained) | +| `#[internal("private")]` | N/A | Inlined private helper (non-entrypoint) | +| `#[internal("public")]` | N/A | Inlined public helper (non-entrypoint) | +| `#[view]` | Private or public | Read-only (no state mutation) | +| `#[only_self]` | Private or public | Callable only by the same contract | +| `#[initializer]` | Private or public | One-time initialization | + +## Prerequisites + +- An Aztec contract project set up with the `aztec-nr` dependency +- Basic understanding of [Noir programming language](https://noir-lang.org/docs) +- Familiarity with Aztec Protocol's [call types](../../../foundational-topics/call_types.md) (private vs public) + +## Define private functions + +Use `#[external("private")]` to create functions that execute privately on user devices. For example: + +#include_code increment /docs/examples/contracts/counter_contract/src/main.nr rust + +Private functions run in a private context, can access private state, and can read certain public values through storage types like [`DelayedPublicMutable`](../state_variables.md#delayedpublicmutable). + +## Define public functions + +Use `#[external("public")]` to create functions that execute on the sequencer: + +#include_code mint_public /docs/examples/contracts/bob_token_contract/src/main.nr rust + +Public functions operate on public state, similar to EVM contracts. They can write to private storage, but any data written from a public function is publicly visible. + +## Define utility functions + +Create offchain query functions using the `#[external("utility")]` annotation with `unconstrained`. + +Utility functions are unconstrained functions that applications call to perform auxiliary tasks, like querying contract state or processing offchain messages. Their execution is never proven, so no guarantees are made on the correctness of their results. They can also be called from other utility functions and from private functions. See [utility calls](../calling_contracts.md#utility-calls). Public functions cannot call them. Example: + +#include_code get_counter /docs/examples/contracts/counter_contract/src/main.nr rust + +Use `aztec.js` `simulate` to execute utility functions and read their return values. For details, see [Call Types](../../../foundational-topics/call_types.md#simulate). + +## Define view functions + +Create read-only functions using the `#[view]` annotation combined with `#[external("private")]` or `#[external("public")]`: + +```rust +#[external("public")] +#[view] +fn get_config_value() -> Field { + // logic +} +``` + +View functions cannot modify contract state. They're akin to Ethereum's `view` functions. +`#[view]` only applies to `#[external("private")]` and `#[external("public")]` functions. + +## Define only-self functions + +Create contract-only functions using the `#[only_self]` annotation: + +#include_code _assert_is_owner /docs/examples/contracts/bob_token_contract/src/main.nr rust + +Only-self functions are only callable by the same contract, which is useful when a private function enqueues a public call that should only be callable internally. + +## Define initializer functions + +Create constructor-like functions using the `#[initializer]` annotation: + +#include_code constructor /docs/examples/contracts/counter_contract/src/main.nr rust + +### Use multiple initializers + +Define multiple initialization options: + +1. Mark each function with `#[initializer]` +2. Choose which one to call during deployment +3. Any initializer marks the contract as initialized + +## Define internal functions + +Create helper functions using `#[internal("private")]` or `#[internal("public")]`. Internal functions are inlined at call sites and do not create separate entrypoints: + +```rust +#[internal("private")] +fn _prepare_transfer(to: AztecAddress, amount: u128) -> Field { + // helper logic for private functions +} + +#[internal("public")] +fn _update_balance(owner: AztecAddress, amount: u128) { + // helper logic for public functions +} +``` + +Call internal functions via `self.internal`: + +```rust +let result = self.internal._prepare_transfer(recipient, amount); +``` + +Key constraints: + +- Private internal functions can only be called from private external or internal functions +- Public internal functions can only be called from public external or internal functions + +## Next steps + +- [Attributes and Macros](./attributes.md) +- [Call Types](../../../foundational-topics/call_types.md) diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/index.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/index.md new file mode 100644 index 00000000000..ab9949c88b5 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/index.md @@ -0,0 +1,30 @@ +--- +title: Defining Functions +sidebar_position: 0 +tags: [functions] +description: Overview of Aztec contract functions, including private, public, and utility function types. +references: ["labs/noir-projects/aztec-nr/aztec/src/macros/functions/*"] +--- + +Functions serve as the building blocks of smart contracts. Functions can be either **public**, ie they are publicly available for anyone to see and can directly interact with public state, or **private**, meaning they are executed completely client-side in the [PXE](../../../foundational-topics/pxe/index.md). Read more about how private functions work [here](./attributes.md#private-functions-externalprivate). + +Currently, any function is "mutable" in the sense that it might alter state. However, we also support static calls, similarly to EVM. A static call is essentially a call that does not alter state (it keeps state static). + +## Initializer functions + +Smart contracts may have one, or many, initializer functions which are called when the contract is deployed. + +Initializers are regular functions that set an "initialized" flag (a nullifier) for the contract. A contract can only be initialized once, and contract functions can only be called after the contract has been initialized, much like a constructor. However, if a contract defines no initializers, it can be called at any time. Additionally, you can define as many initializer functions in a contract as you want, both private and public. + +## Oracles + +There are also special oracle functions, which can get data from outside of the smart contract. In the context of Aztec, oracles are often used to get user-provided inputs. + +## Learn more about functions + +- [How function visibility works in Aztec](./visibility.md) +- How to write an [initializer function](./how_to_define_functions.md#define-initializer-functions) +- [Oracles](../advanced/protocol_oracles.md) and how Aztec smart contracts might use them +- [How functions work under the hood](./attributes.md) + +Find a function macros reference [here](../macros.md) diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/visibility.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/visibility.md new file mode 100644 index 00000000000..5931edecbf6 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/visibility.md @@ -0,0 +1,56 @@ +--- +title: Visibility +sidebar_position: 4 +tags: [functions] +description: Understand function visibility modifiers in Aztec and how they affect function execution and accessibility. +references: ["labs/noir-projects/aztec-nr/aztec/src/macros/functions/*"] +--- + +In Aztec there are multiple different types of visibility that can be applied to functions. Namely we have `data visibility` and `function visibility`. This page explains these types of visibility. + +## Data visibility + +Data visibility describes whether the data (or state) used in a function is generally accessible (public) or on a need-to-know basis (private). + +## Function visibility + +Function visibility describes whether a function is callable from other contracts, or only from within the same contract. This is similar to the visibility modifiers you may be familiar with from Solidity. + +### The `#[external(...)]` attribute + +In Aztec.nr, the `#[external(...)]` attribute marks a function as externally callable - meaning it can be invoked via a transaction or by other contracts. The attribute takes a parameter specifying the execution context: + +- `#[external("private")]` - The function executes in a private context with access to private state +- `#[external("public")]` - The function executes in a public context with access to public state + +### The `#[only_self]` attribute + +By default, all external functions are callable from other contracts, similar to Solidity's `public` visibility. To restrict a function so it can only be called by the same contract, use the `#[only_self]` attribute: + +```rust +#[external("public")] +#[only_self] +fn _increase_public_balance(to: AztecAddress, amount: u128) { + // This function can only be called by this contract + let new_balance = self.storage.public_balances.at(to).read().add(amount); + self.storage.public_balances.at(to).write(new_balance); +} +``` + +A common use case for `#[only_self]` is when a private function needs to modify public state. Since private functions cannot directly modify public state, they enqueue calls to public functions. By marking the public function with `#[only_self]`, you ensure that only your contract can call it - preventing external parties from manipulating the public state directly. + +:::danger +Note that functions without `#[only_self]` can be used directly as an entry-point, which currently means that the `msg_sender` would be `0`. For this reason, using address `0` as a burn address is not recommended. You can learn more about this in the [Accounts concept page](../../../foundational-topics/accounts/keys.md). +::: + +### The `#[internal]` attribute + +The `#[internal]` attribute is different from `#[only_self]`. While `#[only_self]` restricts _who_ can call a function (only the same contract, but still via an external call), `#[internal]` functions are **inlined** into the calling function. This is similar to how Solidity's `internal` functions use EVM's `JUMP` instruction rather than `CALL`. + +Internal functions: + +- Cannot be called externally (no transaction can invoke them directly) +- Are inlined at compile time into the functions that call them +- Have access to the calling function's context + +To understand how visibility works under the hood, check out the [Inner Workings page](./attributes.md). diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/globals.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/globals.md new file mode 100644 index 00000000000..f98a498adb2 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/globals.md @@ -0,0 +1,83 @@ +--- +title: Global Variables +description: Access chain ID, block number, timestamps, and gas information in your Aztec contracts +sidebar_position: 10 +--- + +# Global Variables + +Similar to Solidity's global `block` variable, Aztec exposes contextual values within each function via the `context` object. + +Aztec has two execution environments—Private and Public—each with different available globals. + +## Private Global Variables + +Private functions access transaction context via [`TxContext`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/protocol/abis/transaction/tx_context/struct.TxContext.html). The following fields are accessible via `context` methods: + +### Chain Id + +The unique identifier for the Aztec network instance (not the Ethereum chain the rollup settles to). + +```rust +self.context.chain_id(); +``` + +### Version + +The Aztec protocol version number. The genesis block has version 1. + +```rust +self.context.version(); +``` + +### Gas Settings + +The gas limits, max fees per gas, and inclusion fee set by the user for the transaction. + +```rust +self.context.gas_settings(); +``` + +## Public Global Variables + +Public functions access block-level context via [`GlobalVariables`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/protocol/abis/global_variables/struct.GlobalVariables.html). + +:::note +Not all fields in `GlobalVariables` are exposed via context methods. The `coinbase`, `fee_recipient`, and `slot_number` fields are used internally by the protocol. +::: + +Public functions have access to `chain_id()` and `version()` (same syntax as private), plus the following block-level values: + +### Timestamp + +The unix timestamp when the block is executed. Provided by the block proposer, so it may have slight variance. Always increases monotonically. + +```rust +self.context.timestamp(); +``` + +### Block Number + +The sequential block identifier. Genesis block is 1, incrementing by 1 for each subsequent block. + +```rust +self.context.block_number(); +``` + +### Gas Fees + +The current L2 and DA gas prices for the block. You can access gas-related information via: + +```rust +self.context.l2_gas_left(); // Remaining L2 gas +self.context.da_gas_left(); // Remaining DA gas +self.context.min_fee_per_l2_gas(); // L2 gas price +self.context.min_fee_per_da_gas(); // DA gas price +self.context.transaction_fee(); // Final tx fee (only available in teardown phase) +``` + +:::info Why do available globals differ between environments? +Private functions execute on the user's device before the transaction is submitted, so they cannot know which block will include the transaction. Therefore, `timestamp` and `block_number` are unavailable in private context. + +Public functions execute on a sequencer who knows the current block's timestamp and number, making these values accessible. +::: diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/immutables.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/immutables.md new file mode 100644 index 00000000000..0e93cfda9fb --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/immutables.md @@ -0,0 +1,33 @@ +--- +title: Immutables via Salt +sidebar_position: 4 +tags: [contracts, storage, immutables, optimization] +description: Commit immutable values into a contract's address salt, eliminating initialization transactions. +--- + +Aztec contracts can commit immutable values directly into the contract's address by encoding them into the deployment salt, removing the need for a separate initialization transaction. + +## Overview + +Rather than storing immutables in private storage (which requires an initializer function and an extra transaction), the [aztec-immutables-macro](https://github.com/defi-wonderland/aztec-immutables-macro/tree/dev) library encodes them into the contract's salt: + +``` +salt = poseidon2_hash([actual_salt, constant_0, constant_1, ...]) +``` + +Since the salt is part of the address derivation, the immutable values become cryptographically bound to the contract's address itself. + +## Key benefits + +- **No initialization transaction** — immutables are committed at deployment time, not in a separate setup call +- **Runtime verification** — at execution time, capsule data is loaded and verified against the stored salt, ensuring data integrity +- **Persistent storage** — immutables are persisted to the PXE's [CapsuleStore](./advanced/how_to_use_capsules.md) after deployment, so capsules don't need to be attached to every transaction +- **Compatible with standard storage** — works alongside `#[storage]` and initializers when needed + +## Performance + +Initialization cost is completely eliminated (no constructor transaction). The per-transaction overhead is approximately 1,098 gates (+0.2%) in the account entrypoint. + +## Getting started + +For installation instructions, usage examples, and a reference implementation of an initializerless Schnorr account contract, see the [aztec-immutables-macro README](https://github.com/defi-wonderland/aztec-immutables-macro/tree/dev). diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/macros.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/macros.md new file mode 100644 index 00000000000..3ef64fbf1dc --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/macros.md @@ -0,0 +1,51 @@ +--- +title: Aztec Macros +description: Learn about macros available in Aztec.nr for code generation and abstraction. +sidebar_position: 9 +tags: [contracts, functions] +references: ["labs/noir-projects/aztec-nr/aztec/src/macros/*"] +--- + +Aztec.nr provides macros (attributes) that transform your code during compilation to handle the complexities of private execution, proof generation, and state management. + +## Quick reference + +### Contract + +| Attribute | Purpose | +|-----------|---------| +| `#[aztec]` | Marks a module as an Aztec contract | + +### Functions + +| Attribute | Purpose | +|-----------|---------| +| `#[external("private")]` | Client-side private execution with proofs | +| `#[external("public")]` | Sequencer-side public execution | +| `#[external("utility")]` | Unconstrained queries, not included in transactions | +| `#[internal("private")]` | Private helper, only callable within the same contract | +| `#[internal("public")]` | Public helper, only callable within the same contract | +| `#[view]` | Prevents state modification | +| `#[initializer]` | Contract constructor | +| `#[noinitcheck]` | Callable before contract initialization | +| `#[allow_phase_change]` | Allows for phase change to happen during the function's execution | +| `#[only_self]` | Only callable by the same contract | +| `#[authorize_once]` | Requires authwit authorization with replay protection | + +Functions can have multiple attributes (e.g., `#[external("public")]` with `#[view]` and `#[only_self]`). + +### Structs + +| Attribute | Purpose | +|-----------|---------| +| `#[note]` | Defines a private note type | +| `#[custom_note]` | Note with custom hash/nullifier logic | +| `#[storage]` | Defines contract storage layout | +| `#[storage_no_init]` | Storage with manual slot allocation | + +For detailed explanations and examples, see the [Attributes and Macros reference](./functions/attributes.md). + +## Further reading + +- [Attributes and Macros reference](./functions/attributes.md) - detailed documentation for each macro +- [Inner workings of functions](./functions/function_transforms.md) - how macros transform your code diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/note_delivery.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/note_delivery.md new file mode 100644 index 00000000000..69eb0f06919 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/note_delivery.md @@ -0,0 +1,267 @@ +--- +title: Note Delivery +tags: [storage, concepts, notes] +description: Learn how to deliver notes to recipients in Aztec smart contracts using different delivery modes to balance proving time, transaction costs, and delivery guarantees. +sidebar_position: 4 +references: ["labs/noir-projects/aztec-nr/aztec/src/messages/delivery/*", "labs/noir-projects/aztec-nr/aztec/src/messages/discovery/*", "labs/noir-projects/aztec-nr/aztec/src/oracle/resolve_tagging_strategy.nr"] +--- + +When you create a note in an Aztec smart contract, you must deliver it to the recipient so they can use it. This page explains how note delivery works and how to choose the right delivery mode for your use case. + +## Overview + +In Aztec, creating a note involves two steps: +1. **Creating the note** - Adding the note hash to the note hash tree +2. **Delivering the note** - Sending the note contents to the recipient so they can decrypt and use it + +Without delivery, the recipient won't know the note exists or be able to access its contents, even though the note hash is onchain. + +## The `.deliver()` method + +When you create a note using state variables like `PrivateMutable`, `PrivateSet`, `BalanceSet`, or `SinglePrivateMutable`, the creation methods return a `NoteMessage` or `MaybeNoteMessage` object. A message contains arbitrary information emitted from a contract - currently this includes notes and private events, though developers may define other message types in the future. You must call `.deliver()` on this object to send the message (containing the note) to the recipient. + +```rust +#[aztec] +pub contract PrivateToken { + use aztec::messages::delivery::MessageDelivery; + + #[external("private")] + fn mint(amount: u128, recipient: AztecAddress) { + // Adding to the balance returns a MaybeNoteMessage + self.storage.balances.at(recipient).add(amount) + .deliver(MessageDelivery::onchain_constrained()); + } +} +``` + +## Delivery Modes + +Aztec provides three delivery modes that offer different tradeoffs between cost, proving time, and guarantees: + +### `MessageDelivery::offchain()` + +**Fully offchain delivery with no guarantees.** + +This delivery method encrypts messages without constraints and emits them via an oracle call as offchain effects, rather than through the protocol's log stream (which would post data to Ethereum blobs). With offchain delivery, you must manually handle both message transmission and processing. + +#### How it works + +Offchain messages bypass Aztec's default private log infrastructure entirely: + +1. **Message emission**: The contract encrypts the message (without constraints) and emits it via an oracle call. This creates an "offchain effect" that is included in the transaction but not posted to L1. + +2. **Extraction**: When the transaction is sent, you read the emitted messages from the send result (`offchainMessages` in aztec.js), or extract them from a proven transaction's offchain effects. + +3. **Manual delivery**: You deliver the message through your own channel - a claim link, QR code, messaging service, cloud storage, etc. + +4. **Receipt and processing**: The recipient calls the `offchain_receive` utility function on the contract that emitted the message (this function is generated automatically by the `#[aztec]` macro). The message is stored in a local inbox and processed during private state sync once the transaction that emitted it is found onchain. + +The PXE cannot automatically discover offchain messages during private state sync because they are not in the log stream that nodes load from Ethereum blobs. **You are responsible for implementing the delivery mechanism and ensuring the recipient receives the message.** + +See [Offchain message delivery](./offchain_message_delivery.md) for the complete workflow, including sender and recipient code, message processing details, and how to test it from Noir. + +#### When to use + +- **Use when:** The sender is incentivized to deliver correctly (e.g., sending to yourself, payment for goods/services where recipient must receive the note to complete the transaction) +- **Costs:** Zero delivery fees (no blob space), zero proving time overhead +- **Guarantees:** None. The sender can fail to deliver or deliver incorrect content +- **Privacy:** Maximum. No onchain data is emitted + +This is expected to be the most common delivery method when you don't need constrained delivery guarantees, as it completely eliminates blob space costs. + +#### Example use cases + +- Change notes when transferring tokens (you're sending to yourself) +- Payments where the recipient won't provide goods/services without the note +- Messages to local accounts controlled by the sender +- Low-value use-cases like delivering game state updates to a game server + +```rust +// Change note - sender is motivated to deliver to themselves +self.storage.balances.at(sender).add(change_amount) + .deliver(MessageDelivery::offchain()); +``` + +#### JavaScript implementation + +When using offchain delivery, extract the emitted messages from the send result and deliver them to the recipient in your application. The recipient hands each message to their wallet via the auto-generated `offchain_receive` utility function: + +```typescript +// Sender: send the transaction and extract the emitted offchain messages +const { receipt, offchainMessages } = await token.methods + .transfer_in_private_with_offchain_delivery(alice, bob, amount, 0) + .send({ from: alice }); + +const messageForBob = offchainMessages.find((msg) => msg.recipient.equals(bob)); + +// Deliver via your chosen channel (e.g. a claim link, QR code, messaging service). +// This is what you'd have to implement. +await deliverViaMyChannel(messageForBob, receipt.txHash, recipient); + +// Recipient: receive the message so it gets processed during sync +await token.methods + .offchain_receive([ + { + ciphertext: messageForBob.payload, + recipient: bob, + tx_hash: receipt.txHash.hash, + anchor_block_timestamp: messageForBob.anchorBlockTimestamp, + }, + ]) + .simulate({ from: bob }); +``` + +See [Offchain message delivery](./offchain_message_delivery.md) for the full workflow. + +### `MessageDelivery::onchain_unconstrained()` + +**Onchain delivery with no content guarantees.** + +This mode provides the same low proving time as offchain delivery while avoiding the need to implement custom delivery infrastructure. The tradeoff: you pay for DA (blob space) without gaining additional guarantees. If you're willing to build offchain delivery, use it instead - it's strictly cheaper with the same guarantees. + +- **Use when:** The sender is incentivized to deliver correctly but you don't want to implement offchain delivery infrastructure +- **Costs:** DA gas fees for the encrypted log, zero proving time overhead +- **Guarantees:** Message stored onchain and retrievable, but sender can deliver incorrect content or wrong tag +- **Privacy:** High for the message contents. By default, reaching a recipient the sender has not handshaked with before establishes a handshake that reveals the recipient was contacted (see [Tagging secret strategy](#tagging-secret-strategy)) + +```rust +// Minting to an admin who controls the contract +self.storage.balances.at(admin).add(amount) + .deliver(MessageDelivery::onchain_unconstrained()); +``` + +### `MessageDelivery::onchain_constrained()` + +**Onchain delivery with guaranteed correct content.** + +- **Use when:** The sender cannot be trusted to deliver correctly (e.g., paying fees, creating notes for others, multisig configuration changes). Use this when you need to prove to a contract that the delivery has been done correctly. You can imagine a private NFT sale escrow contract where the escrow would be holding the NFT (the contract itself would be the NFT note owner) and then the escrow would release the NFT to the buyer once the NFT buyer pays the seller. In this case the `NFTSale::buy(...)` function would trigger the payment token transfer from the buyer to the seller and it would need to use constrained delivery otherwise the escrow contract would be willing to transfer the NFT without the NFT seller actually being able to then spend the money. Note that for the transfer of the NFT from the escrow contract to the buyer you could use offchain delivery because the delivery and encryption would be done in the buyer's PXE and hence there is alignment. +- **Costs:** DA gas fees for the encrypted log, proving time overhead for encryption and tagging +- **Guarantees:** Recipient will always be able to find correctly encrypted content: both the encryption and the discovery tag are constrained and stored onchain. +- **Privacy:** High for the message contents. By default, reaching a recipient the sender has not handshaked with before establishes a handshake that reveals the recipient was contacted (see [Tagging secret strategy](#tagging-secret-strategy)) + +```rust +// Minting to an arbitrary recipient - must guarantee delivery +self.storage.balances.at(recipient).add(amount) + .deliver(MessageDelivery::onchain_constrained()); +``` + +## Choosing a delivery mode + +Ask yourself: **"Is the sender incentivized to deliver this note correctly?"** + +- **Yes, and they can contact the recipient offchain** Use offchain delivery +- **Yes, but they cannot or prefer not to contact them offchain or you don't want to implement offchain delivery** Use unconstrained delivery +- **No, the sender might not deliver correctly** Use constrained delivery + +## Tagging secret strategy + +Onchain delivery tags every message so the recipient can find it efficiently (see [note discovery](#note-discovery-and-the-sender) below). Computing a tag requires a secret shared between sender and recipient, and there is more than one way for the two parties to come to share it. The derivation is decided in this order: + +1. A contract can fix it itself at the point of delivery (see [overriding the strategy from the contract](#overriding-the-strategy-from-the-contract)). +2. Otherwise, when an onchain handshake has been registered for the pair, the secret derived from it is reused directly. +3. Otherwise, the wallet decides how to proceed, since it knows which secrets it holds and how it wants to reach the recipient. + +The wallet's answer is a **tagging secret strategy**: it expresses *which* secret to use, and if necessary, PXE performs a [Diffie-Hellman key exchange](https://www.geeksforgeeks.org/computer-networks/diffie-hellman-key-exchange-and-perfect-forward-secrecy/) and/or app-siloing before handing the ready-to-use secret to the contract. Wallets therefore never reimplement that derivation. There are four strategies today: + +- **Non-interactive handshake**: the secret comes from a handshake published onchain that the recipient can derive. A non-recipient can at most learn that a sender did a handshake with the recipient, not the message itself, but the recipient discovers it without any prior coordination. Works for both constrained and unconstrained delivery. +- **Interactive handshake**: the secret comes from a handshake the recipient authorizes with a signature, which the registry requests through the custom request oracle and verifies in-circuit. Nothing announcing the handshake is published onchain (the recipient learns the secret while signing), but the recipient must be reachable to answer the request, or the send fails. Works for both constrained and unconstrained delivery. +- **Address-derived secret**: the PXE derives the secret from the sender's and recipient's address keys via Diffie-Hellman. The wallet supplies no material, only the choice. It leaves no onchain trace, but the recipient only finds the message if they registered the sender in their PXE. Unconstrained delivery only. +- **Arbitrary secret**: a raw secret point the two parties already share offchain, having coordinated out of band to agree on it. The wallet supplies the point and the PXE app-silos it. It leaves no onchain trace, but no onchain handshake backs the secret. Unconstrained delivery only. + +| | Non-interactive handshake | Interactive handshake | Address-derived secret | Arbitrary secret | +|---|---|---|---|---| +| Onchain footprint when establishing | A handshake revealing information about the recipient | None | None | None | +| Who provides the material | The onchain registry | The onchain registry, with the recipient's signed authorization | Nobody (PXE computes it) | The wallet (a raw point) | +| Recipient coordination | None | The recipient must answer the signature request | The recipient must have registered the sender | Agreed out of band | +| Constrained delivery | Supported | Supported | Not sound: not backed by an onchain handshake | Not sound: not backed by an onchain handshake | + +### Defaults + +When no `resolveTaggingSecretStrategy` hook is configured, the PXE applies a default: + +- **Unconstrained delivery**: a non-interactive handshake when the recipient is external, so the recipient discovers the message without having registered the sender in advance. When the recipient is one of the wallet's own accounts (a self-send), an address-derived secret is used instead: the wallet holds both sides' keys, so no handshake is needed and nothing is revealed onchain. +- **Constrained delivery**: a non-interactive handshake (constrained delivery must be backed by a handshake). + +An interactive handshake is never the default, since it fails when the recipient cannot be reached: a wallet opts in through the hook when it knows how to reach them. + +### Configuring the strategy + +Wallets provide the strategy through the `resolveTaggingSecretStrategy` [execution hook](../../foundational-topics/pxe/execution_hooks.md) when creating their PXE. The hook receives the message context (executing contract, sender, recipient and delivery mode), so a wallet can answer per message instead of with a fixed value. That page also covers how to configure a strategy in Noir tests. + +### Overriding the strategy from the contract + +A contract can fix the derivation at the point of delivery with the builder's `via_*` methods. When it does, the wallet is not consulted at all; otherwise the wallet resolves the strategy as usual: + +```rust +MessageDelivery::onchain_unconstrained().via_address_derived_secret() +``` + +Unconstrained delivery exposes `via_non_interactive_handshake()`, `via_interactive_handshake()` and `via_address_derived_secret()`. Constrained delivery exposes only `via_non_interactive_handshake()` and `via_interactive_handshake()`, since an address-derived secret cannot back constrained delivery. + +## Note discovery and the sender + +When a note is delivered, recipients need to discover it among all the encrypted logs on the network. Aztec.nr uses a **tagging system** that requires computing a shared secret between the sender and recipient. + +### Who is the "sender"? + +The "sender" for note discovery is **not the contract calling `.deliver()`**. Instead, it's the **account contract** that initiated the transaction. + +When your wallet submits a transaction, it tells PXE which address to use as the sender for tags (typically the originating account). Recipients compute the tag to find their notes from a secret shared between the sender and recipient, and there is [more than one way to establish that secret](#tagging-secret-strategy), chosen by the wallet. Contracts can override the sender at message delivery via the `with_sender` builder method, which works for both constrained and unconstrained delivery, e.g. `MessageDelivery::onchain_constrained().with_sender(address)`. They can similarly override how the tag secret is derived via the builder's `via_*` methods; see [overriding the strategy from the contract](#overriding-the-strategy-from-the-contract). + +**Example:** If Alice uses her account contract to call a token contract that mints tokens to Bob, the "sender for tags" is Alice's account contract address, not the token contract address. + +### Discovering notes from unknown senders + +When the tag is derived from an address-based shared secret, you cannot compute it for a sender you haven't registered in advance, so you cannot receive those notes from an unknown sender. Handshake protocols let the two parties agree on the secret another way and lift this restriction. + +See [You cannot receive address-derived tagged notes from an unknown sender](../../foundational-topics/advanced/storage/note_discovery.md#you-cannot-receive-address-derived-tagged-notes-from-an-unknown-sender) in the note discovery documentation for the approaches and workarounds. + +## Delivering to someone other than the note owner + +You can deliver a note to an address other than the note's owner using `.deliver_to()`: + +```rust +// Create a note owned by `owner` but deliver it to `auditor` +self.storage.balances.at(owner).add(amount) + .deliver_to(auditor, MessageDelivery::onchain_constrained()); +``` + +**Important:** The recipient (e.g. an `auditor`) can see the note was created but **cannot use it** - only the owner can spend the note (this is authorized by the contract logic). The recipient also cannot see when/if the note is nullified. + +**Use cases:** +- Traditional finance model of compliance where the third party sees all the activity (e.g. a bank) +- Game servers that track all note creation and then quickly serve you the game state (results in better UX) +- Analytics or monitoring services + +## Code examples + +### Private token transfer + +```rust +#[external("private")] +fn transfer(amount: u128, sender: AztecAddress, recipient: AztecAddress) { + // Subtract from sender - unconstrained since sender is the caller + self.storage.balances.at(sender) + .sub(amount) + .deliver(MessageDelivery::onchain_unconstrained()); + + // Add to recipient - constrained delivery for untrusted sender + self.storage.balances.at(recipient) + .add(amount) + .deliver(MessageDelivery::onchain_constrained()); +} +``` + +### Admin initialization + +```rust +#[external("private")] +#[initializer] +fn constructor(admin: AztecAddress) { + // Admin is the owner of the note and is motivated to receive it + // Use unconstrained delivery since we don't know if deployer is incentivized + self.storage.admin + .initialize(AddressNote { address: admin }, admin) + .deliver(MessageDelivery::onchain_constrained()); +} +``` diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/offchain_message_delivery.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/offchain_message_delivery.md new file mode 100644 index 00000000000..e5a8fa56711 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/offchain_message_delivery.md @@ -0,0 +1,119 @@ +--- +title: Offchain message delivery +tags: [storage, concepts, notes] +description: Deliver notes and events to recipients through your own channel with offchain message delivery, avoiding data availability costs entirely. +sidebar_position: 4 +references: ["labs/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr", "labs/noir-projects/noir-contracts/contracts/app/token_contract/src/test/transfer_in_private_with_offchain_delivery.nr", "labs/noir-projects/aztec-nr/aztec/src/messages/offchain_messages.nr", "labs/noir-projects/aztec-nr/aztec/src/messages/processing/offchain/*"] +--- + +Offchain message delivery lets you send notes and events to recipients without posting any data onchain. The encrypted message is returned to the sender's application, which transports it to the recipient through its own channel: a link, a QR code, a direct message, or any other medium. This eliminates data availability (DA) costs and adds zero proving time, at the cost of the application handling transport itself. + +This page walks through the complete workflow. For guidance on when offchain delivery is the right choice compared to the onchain modes, see [Note delivery](./note_delivery.md). + +## Overview + +An offchain-delivered message goes through five stages: + +1. **Emission**: The contract delivers a note or event with `MessageDelivery::offchain()`. The message is encrypted to the recipient and emitted as an offchain effect, which is part of the transaction's execution output but never posted onchain. +2. **Extraction**: After sending the transaction, the sender's application reads the emitted messages from the send result (`offchainMessages`). +3. **Transport**: The application delivers each message to its recipient through a channel of its choosing. This step is entirely the application's responsibility. +4. **Receipt**: The recipient hands the message to their wallet by calling the `offchain_receive` utility function on the contract that emitted it. This function is generated automatically by the `#[aztec]` macro, so every Aztec.nr contract has it. +5. **Processing**: The message sits in a local inbox until the transaction that emitted it is found onchain. During private state sync, the wallet decrypts the message, validates the resulting notes and events against onchain data, and adds them to its database. + +Because processing validates the message against the onchain transaction, a malformed or dishonest message cannot trick the recipient: a message that cannot be decrypted or validated is simply ignored. + +## Emitting offchain messages from a contract + +Choose offchain delivery by passing `MessageDelivery::offchain()` to `.deliver()` (for notes) or `.deliver_to()` (for events). The token contract's `transfer_in_private_with_offchain_delivery` function delivers both resulting balance notes and a `Transfer` event offchain: + +#include_code transfer_in_private_with_offchain_delivery /labs/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust + +Nothing else changes in the contract: the same notes and events are created, only their delivery mechanism differs. + +Under the hood, `MessageDelivery::offchain()` encrypts the message without constraints and emits it via the [`deliver_offchain_message`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/offchain_messages/fn.deliver_offchain_message) function. See the [`MessageDelivery`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/delivery/struct.MessageDelivery) API reference for details on guarantees, costs, and privacy. + +## Extracting messages on the sender side + +In `aztec.js`, the result of sending a transaction includes the offchain messages emitted during execution. Each message carries the encrypted `payload`, the `recipient` address, the emitting `contractAddress`, and the `anchorBlockTimestamp` of the transaction: + +```typescript +const { receipt, offchainMessages } = await token.methods + .transfer_in_private_with_offchain_delivery(alice, bob, amount, 0) + .send({ from: alice }); + +// Find the message addressed to the recipient +const messageForBob = offchainMessages.find((msg) => msg.recipient.equals(bob)); +``` + +If you manage proving manually, you can extract the messages from a proven transaction's offchain effects with `extractOffchainOutput`: + +```typescript +import { extractOffchainOutput } from "@aztec-labs/aztec.js/contracts"; + +const { offchainMessages } = extractOffchainOutput( + provenTx.offchainEffects, + provenTx.data.constants.anchorBlockHeader.globalVariables.timestamp, +); +``` + +## Transporting messages to the recipient + +The recipient needs four pieces of information to receive a message: + +- the message payload (ciphertext) +- the recipient address +- the hash of the transaction that emitted the message +- the anchor block timestamp + +How you get these to the recipient is up to your application. Common patterns include encoding them into a claim link or QR code that the recipient opens, sending them through a messaging service, or storing them where the recipient can fetch them. + +:::warning Back up your messages +Offchain messages are not stored onchain, so they cannot be recovered from the network. Until the recipient has processed a message, losing it means the recipient cannot decrypt the corresponding note contents (the note itself still exists onchain). +::: + +## Receiving messages + +The recipient calls the auto-generated `offchain_receive` utility function on the contract that emitted the message. Utility functions run locally and do not create a transaction, so this call is free: + +```typescript +await token.methods + .offchain_receive([ + { + ciphertext: messageForBob.payload, + recipient: bob, + tx_hash: receipt.txHash.hash, + anchor_block_timestamp: messageForBob.anchorBlockTimestamp, + }, + ]) + .simulate({ from: bob }); +``` + +`offchain_receive` accepts up to 16 messages per call. It stores them in a persistent inbox scoped to each message's recipient; processing happens later, during sync. + +## How messages are processed + +Once received, messages are managed by an inbox that the wallet syncs against (see [`receive`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/processing/offchain/fn.receive) in the API reference): + +- **Transaction resolution**: A message is only processed once the transaction identified by its `tx_hash` is found onchain. This provides the context needed to validate the notes and events the message contains. Until then, the message waits in the inbox. +- **Reorg safety**: Messages remain in the inbox even after processing. If a reorg reverts the transaction that emitted a message, its effects are rolled back; when the transaction is mined again, the message is reprocessed automatically without needing to be delivered again. +- **Expiry**: Messages are evicted from the inbox once they expire, which happens 26 hours after their anchor block timestamp (the maximum transaction lifetime of 24 hours plus a 2 hour tolerance). A transaction can no longer be included after that point, so an unresolved message can never become processable. + +## Current limitations + +- **Senders must self-deliver their own messages.** Messages addressed to the sender (such as the change note in a token transfer) are not processed automatically yet. Call `offchain_receive` for them just like you would on the recipient side. +- **Messages must be bound to a transaction.** Messages without an associated transaction hash are not processed in the current version; they sit in the inbox until they expire. +- **Batch size**: A single `offchain_receive` call accepts at most 16 messages. Split larger sets into multiple calls. + +## Testing offchain delivery from Noir + +The TXE test environment buffers offchain messages emitted during calls and exposes them via `env.offchain_messages()`. Feed them back through `offchain_receive` to complete the delivery loop inside a test: + +#include_code token_transfer_offchain_delivery_test /labs/noir-projects/noir-contracts/contracts/app/token_contract/src/test/transfer_in_private_with_offchain_delivery.nr rust + +`env.offchain_messages()` returns up to 64 messages, so drain it into batches of at most 16 when a test emits more messages than one `offchain_receive` call accepts. + +## Next steps + +- Compare delivery modes and their guarantees in [Note delivery](./note_delivery.md) +- Learn how onchain-delivered notes are found in [Note discovery](../../foundational-topics/advanced/storage/note_discovery.md) +- Browse the [`MessageDelivery`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/delivery/struct.MessageDelivery) API reference diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/state_variables.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/state_variables.md new file mode 100644 index 00000000000..a4e5f68d7c3 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/state_variables.md @@ -0,0 +1,618 @@ +--- +title: State Variables +sidebar_position: 3 +tags: [contracts, storage, data-types, smart-contracts] +description: Define and manage storage state in your Aztec smart contracts using various storage types. +references: ["labs/noir-projects/aztec-nr/aztec/src/state_vars/*", "labs/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr", "labs/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr", "labs/noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr", "labs/noir-projects/noir-contracts/contracts/app/private_token_contract/src/main.nr", "labs/noir-projects/noir-contracts/contracts/app/app_subscription_contract/src/main.nr"] +--- + +# State Variables + +A contract's state is defined by multiple values. For example, in a token contract, these include the total supply, user balances, outstanding approvals, accounts with minting permission, etc. Each of these persisting values is called a _state variable_. + +One of the first design considerations for any smart contract is how it'll store its state. This is doubly true in Aztec due to there being **both public and private state** - the tradeoff space is large, so there's room for lots of decisions. + +## Choosing the right storage type + +| Need | Use | +|------|-----| +| Public value anyone can read/write | `PublicMutable` | +| Public value set once (contract name, decimals) | `PublicImmutable` | +| Public key-value mapping | `Map<K, PublicMutable<V>>` | +| Private collection per user (token balances) | `Owned<PrivateSet<...>>` | +| Single private value per user | `Owned<PrivateMutable<...>>` | +| Immutable private value per user | `Owned<PrivateImmutable<...>>` | +| Contract-wide private singleton (admin key) | `SinglePrivateMutable` | +| Public value readable in private execution | `DelayedPublicMutable` | + +## Prerequisites + +- An Aztec contract project set up with `aztec-nr` dependency +- Understanding of Aztec's private and public state model +- Familiarity with Noir struct syntax + +For storage concepts, see [storage overview](../../foundational-topics/state_management.md). + +## The Storage Struct + +State variables are declared in Solidity by simply listing them inside of the contract, like so: + +```solidity +contract MyContract { + uint128 public my_public_state_variable; +} +``` + +In Aztec.nr, we define a [`struct`](https://noir-lang.org/docs/language/data_types/structs) that holds _all_ state variables. This struct is called **the storage struct**, and it is identified by having the [`#[storage]` macro](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/macros/storage/fn.storage) applied to it. + +```rust +use aztec::macros::aztec; + +#[aztec] +contract MyContract { + use aztec::macros::storage; + + #[storage] + struct Storage<Context> { + // state variables go here e.g, the admin of the contract + admin: PublicMutable<AztecAddress, Context>, + } +} +``` + +This struct must also have a generic type called `C` or `Context` - an unfortunate boilerplate parameter that provides execution mode information. + +The `#[storage]` macro can only be used once, so all contract state must be in a **single** struct. + +### Accessing Storage + +The contract's storage is accessed via `self.storage` in any contract function. It will automatically be tailored to the execution context of that function, hiding all methods that cannot be invoked there. + +Consider, for example, a `PublicMutable` state variable, which is a value that is fully accessible in public functions, read-only in utility functions, and not accessible in private functions: + +```rust +#[storage] +struct Storage<Context> { + my_public_variable: PublicMutable<Field, Context>, +} + +#[external("public")] +fn my_public_function() { + let current = self.storage.my_public_variable.read(); + self.storage.my_public_variable.write(current + 1); +} + +#[external("private")] +fn my_private_function() { + let current = self.storage.my_public_variable.read(); // compilation error - 'read' is not available in private + self.storage.my_public_variable.write(current + 1); // compilation error - 'write' is not available in private +} + +#[external("utility")] +fn my_utility_function() { + let current = self.storage.my_public_variable.read(); + self.storage.my_public_variable.write(current + 1); // compilation error - 'write' is not available in utility +} +``` + +## Public State Variables + +These are state variables that have _public_ content: everyone on the network can see the values they store. They can be considered to be equivalent to Solidity state variables. + +### Choosing a Public State Variable + +Public state variables are stored in the network's public storage tree and can only be written to by public contract functions. You can read _historic_ values of a public state variable in a private contract function, but the current values in the network's public state tree are not accessible in private functions. This means that most public state variables cannot be read from a private function, though there are some exceptions documented in the table below. + +Below is a table comparing the key properties of the different public state variables that Aztec.nr offers: + +| State variable | Mutable? | Readable in private? | Writable in private? | Example use case | +| ---------------------- | ------------------- | -------------------- | -------------------- | ---------------------------------------------------------------------------------- | +| [`PublicMutable`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.PublicMutable) | yes | no | no | Configuration of admins, global state (e.g. token total supply, total votes) | +| [`PublicImmutable`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.PublicImmutable) | no | yes | no | Fixed configuration, one-way actions (e.g. initialization settings for a proposal) | +| [`DelayedPublicMutable`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.DelayedPublicMutable) | yes (after a delay) | yes | no | Non time sensitive system configuration | + +### PublicMutable + +`PublicMutable` is the simplest kind of public state variable: a value that can be read and written. It is essentially the same as a non-`immutable` or `constant` Solidity state variable. + +It **cannot be read or written to privately**, but it is possible to have private functions enqueue a public call in which a `PublicMutable` is accessed. For example, a voting contract may allow private submission of votes which then enqueue a public call in which the vote count, represented as a `PublicMutable<u128>`, is incremented. This would let anyone see how many votes have been cast, while preserving the privacy of the account that cast the vote. + +#### Declaration + +Store mutable public state using `PublicMutable<T>` for values that need to be updated throughout the contract's lifecycle. + +For example, storing the address of the collateral asset in a lending contract: + +#include_code public_mutable /labs/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr rust + +#### `read` + +`PublicMutable` variables have a `read` method to read the value at the location in storage: + +#include_code public_mutable_read /labs/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr rust + +#### `write` + +The `write` method on `PublicMutable` variables takes the value to write as an input and saves this in storage: + +#include_code public_mutable_write /labs/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr rust + +### PublicImmutable + +`PublicImmutable` is a simplified version of `PublicMutable`: it's a public state variable that can only be written (initialized) once, at which point it can only be read. Unlike Solidity `immutable` state variables, which must be set in the contract's constructor, a `PublicImmutable` can be initialized _at any point in time_ during the contract's lifecycle. Attempts to read it prior to initialization will revert. + +Due to the value being immutable, you can also read it during private execution - once a circuit proves that the value was set in the past, it knows it cannot have possibly changed. This makes this state variable suitable for immutable public contract configuration or one-off public actions, such as user registration status. + +#### Declaration + +For example, in the `Storage` struct in a simple token contract, the name, symbol, and decimals are `PublicImmutable` variables: + +#include_code public_immutable /labs/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr rust + +#### `initialize` + +This function sets the immutable value. It can only be called once. + +#include_code public_immutable_initialize /labs/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr rust + +:::warning +A `PublicImmutable`'s storage **must** only be set once via `initialize`. Attempting to override this by manually accessing the underlying storage slots breaks all properties of the data structure, rendering it useless. +::: + +#### `read` + +Returns the stored immutable value. This function is available in public, private and utility contexts. + +#include_code public_immutable_read /labs/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr rust + +### DelayedPublicMutable + +It is sometimes necessary to read public mutable state in private. For example, a decentralized exchange might have a configurable swap fee that some admin sets, but which needs to be read by users in their private swaps. This is where `DelayedPublicMutable` comes in. + +`DelayedPublicMutable` is the same as a `PublicMutable` in that it is a public value that can be read and written, but with a caveat: writes only take effect _after some time delay_. These delays are configurable, but they're typically on the order of a couple hours, if not days, making this state variable unsuitable for actions that must be executed immediately - such as an emergency shutdown. It is these very delays that enable private contract functions to _read the current value of a public state variable_, which is otherwise typically impossible. + +The existence of minimum delays means that a private function that reads a public value at an anchor block has a guarantee that said historical value will remain the current value until _at least_ some time in the future - before the delay elapses. As long as the transaction gets included in a block before that time (by using the `expiration_timestamp` tx property), the read value is valid. + +#### Declaration + +Unlike other state variables, `DelayedPublicMutable` receives not only a type parameter for the underlying datatype, but also a `DELAY` type parameter with the value change delay as a number of seconds. + +#include_code delayed_public_mutable_storage /labs/noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr rust + +#### `schedule_value_change` + +This is the means by which a `DelayedPublicMutable` variable mutates its contents. It schedules a value change for the variable at a future timestamp after the `DELAY` has elapsed. + +#include_code schedule_value_change /labs/noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr rust + +#### `get_current_value` + +Returns the current value in a public, private or utility execution context. + +#include_code get_current_value /labs/noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr rust + +:::warning Privacy Consideration +Reading `DelayedPublicMutable` in private sets the `expiration_timestamp` property, which may reveal timing information. Choose delays that align with common values to maximize privacy sets. +::: + +#### `get_scheduled_value` + +Returns the scheduled value and when it takes effect: + +#include_code get_scheduled_value /labs/noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr rust + +## Private State Variables + +Private state variables have _private_ content meaning that only some people know what is stored in them. These work _very_ differently from public state variables and are unlike anything in languages such as Solidity, since they are built from fundamentally different primitives (UTXO-based notes and nullifiers instead of a key-value updatable public database). + +Aztec.nr provides three private state variable types: + +- `Owned<PrivateMutable<NoteType, Context>, Context>`: Single mutable private value +- `Owned<PrivateImmutable<NoteType, Context>, Context>`: Single immutable private value +- `Owned<PrivateSet<NoteType, Context>, Context>`: Collection of private notes + +These private state variables are "owned" and must be wrapped in the `Owned<>` container, which enables owner-specific access via the `.at(owner)` method. Each also requires a `NoteType`. To understand this, let's go through notes and nullifiers and how they can be used so we can understand how private state works. + +### Notes and Nullifiers + +Just as public state is stored in a single public data tree (equivalent to the `key-value` store used for state on the EVM), private state is managed using two separate trees: + +- **The note hash tree**: stores hashes of the private data, called notes, which are just structs containing private data with some methods. +- **The nullifier tree**: the nullifier for a certain note is deterministic, and the presence of the nullifier in the nullifier tree determines that the note has been spent/used. + +Understanding these primitives and how they can be used is key to understanding how private state works. + +#### Notes + +Notes are user-defined data that can be stored privately on the blockchain. A note can represent any private data, such as an amount (e.g., some token balance), an ID (e.g., a vote proposal ID), or an address (e.g., an authorized account). + +They also have some metadata, including a storage slot to avoid collisions with other notes, a `randomness` value that helps hide the content, and an `owner` who can nullify the note. + +The note content plus the metadata are all hashed together, and it is this hash that gets stored onchain in the note hash tree. This hash is called a commitment. The underlying note content (the note hash preimage) is not stored anywhere onchain, so third parties cannot access it and it remains private. The note hash tree is append-only - if it wasn't, when a note was spent, external observers would notice that the tree leaf inserted in some transaction was modified in a second transaction, linking them together and leaking privacy. For example, when a user made a payment to a third party, the recipient would be able to know when they spent the received funds. Nullifiers exist to solve this issue. + +Note: Aztec.nr comes with some prebuilt note types, including [`UintNote`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/noir-projects/aztec-nr/uint-note) and [`AddressNote`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/noir-projects/aztec-nr/address-note), but users are also free to create their own with the `#[note]` macro. + +##### Note Lifecycle + +Notes are more complicated than public state, and so it helps to see the different stages one goes through, and when and where each stage happens: + +- **Creation**: an account executing a private contract function creates a new note according to contract logic, e.g., transferring tokens to a recipient. Note values (e.g., a token amount) and metadata are set, the note hash is computed, and inserted as one of the effects of the transaction. + +- **Encryption**: the content of the note is encrypted with a key only the sender and intended recipient know - no other account can decrypt this message. + +- **Delivery**: the encrypted message is delivered to the recipient via some means. Options include storing it onchain as a transaction log, or sending it offchain, e.g., via email or by having the recipient scan a QR code on the sender's device. + +- **Insertion**: the transaction is sent to the network and gets included in a block. The note hash is inserted into the note hash tree - this is visible to the entire network, but the content of the note remains private. + +- **Discovery**: the recipient processes the encrypted message they were sent, decrypting it and finding the note's content (i.e., the hash preimage). They verify that the note's hash exists onchain in the note hash tree. They store the note's content in their own private database and can now spend the note. + +- **Reading**: while executing a private contract function, the recipient fetches the note's content and metadata from their private database (in their PXE) and shows that its hash exists in the note hash tree as part of the zero-knowledge proof. + +- **Nullification**: the recipient computes the note's nullifier and inserts it as one of the effects of the transaction, preventing the note from being read again. + +#### Nullifiers + +A nullifier is a value which indicates a resource has been spent. Nullifiers are unique and stored onchain in the nullifier tree. The protocol forbids the same nullifier from being inserted into the tree twice. Spending the same resource therefore results in a duplicate nullifier, which invalidates the transaction. + +The nullifier tree is **append-only** for the same reason that the note hash tree is append-only. + +Most often, nullifiers are used to mark a note as being spent, which prevents note double spends. This requires two properties from the function that computes a note's nullifier: + +- **Deterministic**: the nullifier **must** be deterministic given a note, so that the same nullifier value is computed every time the note is attempted to be spent. A non-deterministic nullifier would result in a note being spendable more than once because the nullifiers would not be duplicates. +- **Secret**: the nullifier **must** not be computable by anyone except the owner, _even by someone who knows the full note content_. This is because some third parties _do_ know the note content: when paying someone and creating a note for them, the payer creates the note on their device and thus has access to all of its data and metadata. + +There are multiple ways to compute nullifiers that fulfill this property, but typically they are computed as a **hash of the note contents concatenated with a private key of the note's owner**. These values are **immutable**, and only the owner knows their private keys, ensuring both determinism and secrecy. These nullifiers are sometimes called 'zcash-style nullifiers' because this is the format ZCash uses for their note nullifiers. + +### Note Messages and Discovery + +Because notes are private, not even the intended recipient is aware of their existence, and therefore they must be somehow notified. For example, when making a payment and creating a note for the payee with the intended amount, they must be shown the preimage of the note that was inserted in the note hash tree in a given transaction in order to acknowledge the payment. + +Recipients learning about notes created for them is known as 'note discovery', which is a process Aztec.nr handles efficiently and automatically. However, it does mean that when a note is created, a _message_ with the content of the note is created and needs to be delivered to a recipient via one of multiple means detailed below. + +When working with private state variables, many operations return a `NoteMessage<Note>` type rather than the note directly. This is a type-safe wrapper that ensures you explicitly decide how to deliver the note to its recipient. + +#### Delivery Methods + +Private notes need to be communicated to their recipients so they know the note exists and can use it. The [`NoteMessage`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/note/struct.NoteMessage) wrapper forces you to make an explicit choice about how this happens: + - [`MessageDelivery::onchain_constrained()`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/delivery/struct.MessageDelivery): Verified in the circuit (most secure, but highest cost) - Use when the sender cannot be trusted to deliver correctly (e.g., protocol fees, multisig config updates). + - [`MessageDelivery::onchain_unconstrained()`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/delivery/struct.MessageDelivery): Message stored onchain but no guarantees on content - Use when the sender is incentivized to deliver correctly but may not have an offchain channel to the recipient. + - [`MessageDelivery::offchain()`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/delivery/struct.MessageDelivery): Lowest cost, no onchain data - Use when the sender and recipient can communicate and the sender is incentivized to deliver correctly. + +#include_code note_delivery /labs/noir-projects/noir-contracts/contracts/app/private_token_contract/src/main.nr rust + +Methods that return `NoteMessage` include `initialize()`, `get_note()`, and `replace()` on `PrivateMutable`, `initialize()` on `PrivateImmutable`, and `insert()` on `PrivateSet` (more on these methods and private state variable types shortly). + +### How Aztec.nr Abstracts Private State Variables + +Implementing a private state variable requires careful coordination of multiple primitives and concepts (creating notes, encrypting, delivering, discovering and processing messages, reading notes, and computing their nullifiers). Aztec.nr provides convenient types and functions that handle all of these low-level details to allow developers to write safe code without having to understand the nitty-gritty. + +By applying the `#[note]` [macro](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/macros/notes/fn.note) to a [noir struct](https://noir-lang.org/docs/language/data_types/structs), users can define values that will be storable in notes. Private state variables can then hold these notes and be used to read, write, and deliver note messages to the intended recipient. + +:::note +Advanced users can change this default behavior by either defining their [own custom note](./custom_notes.md) hash and nullifier functions, implementing their own state variables, or even accessing the note hash and nullifiers tree directly. +::: + +The snippet below shows a contract with two private state variables: an admin address (stored in an `AddressNote`) and a counter of how many calls the admin has made (stored in a `UintNote`). These values will be private and therefore not known except by the accounts that own these notes (the admin). In the `perform_admin_action` private function, the contract checks that it is being called by the correct admin and updates the call count by incrementing it by one. + +(Note that this is not a real snippet, it's missing some small irrelevant details - but the gist of it is correct) + +```rust +#[note] +pub struct AddressNote { + value: AztecAddress, +} + +#[note] +pub struct UintNote { + value: u128, +} + +#[storage] +struct Storage { + admin: Owned<PrivateImmutable<AddressNote, Context>, Context>, + admin_call_count: Owned<PrivateMutable<UintNote, Context>, Context>, +} + +#[external("private")] +fn perform_admin_action() { + // Read the contract's admin address and check against the caller + let admin = self.storage.admin.get_note().value; + assert(self.msg_sender() == admin); + // Update the call count by replacing (updating - rename soon) the current note with a new one that equals the + // current value + 1 - this requires knowing what the current value is in the first place, i.e., reading the variable. + // + // We then deliver the encrypted message with the note's content to the admin so that they become aware of the new + // value of the counter and can update it again in the future. + self.storage.admin_call_count + .replace(|current| UintNote{ value: current.value + 1 }) // wouldn't it be great if we didn't have to deal with this wrapping and unwrapping? + .deliver(MessageDelivery::onchain_constrained()); + + // ... +} +``` + +### Choosing a Private State Variable + +Due to the complexities of Aztec's private state model, private state variables do not map 1:1 with public state variables. Understanding these differences between the different private state variables is important when it comes to designing private smart contracts. + +Below is a table comparing certain key properties of the different private state variables Aztec.nr offers: + +| State variable | Mutable? | Cost to read? | Writable by third parties? | Example use case | +| ------------------ | -------- | ------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------- | +| [`PrivateMutable`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.PrivateMutable) | yes | yes | no | Mutable user state only accessible by them (e.g. user settings or keys) | +| [`PrivateImmutable`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.PrivateImmutable) | no | no | no | Fixed configuration, one-way actions (e.g. initialization settings for a proposal) | +| [`PrivateSet`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.PrivateSet) | yes | yes | yes | Aggregated state others can add to, e.g. token balance (set of amount notes), nft collections (set of nft ids) | + +### Owned State Variables + +Private state variables like `PrivateMutable`, `PrivateImmutable`, and `PrivateSet` implement the `OwnedStateVariable` trait. You must wrap them in `Owned`. + +Access the underlying state variable for a specific owner using `.at(owner)` + +### PrivateMutable + +`PrivateMutable` is conceptually similar to `PublicMutable` and regular Solidity state variables in that it is a variable that has exactly one value at any point in time that can be read and written. However, for `PrivateMutable`: + +- The value is, of course, _private_, meaning only the account the value belongs to can read it. +- _Only ONE account can read and write the state variable_. It is not possible, for example, to use a `PrivateMutable` to store user settings and then have some admin account alter these settings. +- Reading the current value results in the state variable being updated, increasing tx costs and requiring delivery of a note message. +- There is no `write` function - the current value is instead `replace`d. + +#### Declaration + +#include_code owned_private_mutable /labs/noir-projects/noir-contracts/contracts/app/app_subscription_contract/src/main.nr rust + +#### `is_initialized` + +An unconstrained method to check whether the `PrivateMutable` has been initialized or not: + +#include_code owned_private_mutable_is_initialized /labs/noir-projects/noir-contracts/contracts/app/app_subscription_contract/src/main.nr rust + +#### `initialize` and `initialize_or_replace` + +The `PrivateMutable` should be initialized to create the first note and value. This can be done with either `initialize` or `initialize_or_replace`: + +#include_code owned_private_mutable_initialize /labs/noir-projects/noir-contracts/contracts/app/app_subscription_contract/src/main.nr rust + +#### `get_note` + +This function allows us to get the note of a `PrivateMutable`, essentially reading the value: + +```rust +#[external("private")] +fn read_settings() { + let owner = self.msg_sender(); + self.storage.user_settings.at(owner).get_note().deliver(MessageDelivery::onchain_constrained()); +} +``` + +:::info +To ensure that a user's private execution always uses the latest value of a `PrivateMutable`, the `get_note` function will nullify the note that it is reading. This means that if two people are trying to use this function with the same note, only one will succeed. + +Reading a `PrivateMutable` nullifies and recreates the note. This makes reads indistinguishable from writes and ensures the sequencer cannot learn the note's value. +::: + +#### `replace` + +To update the value of a `PrivateMutable`, we can use the `replace` method: + +#include_code owned_single_private_mutable_replace /labs/noir-projects/noir-contracts/contracts/app/private_token_contract/src/main.nr rust + +### PrivateImmutable + +`PrivateImmutable` represents a unique private state variable that, as the name suggests, is immutable. Once initialized, its value cannot be altered. This is the private equivalent of `PublicImmutable`, except the value is only known to its owner. + +Unlike `PrivateMutable`, the `get_note` function for a `PrivateImmutable` doesn't nullify the current note and returns the `Note` directly (not wrapped in `NoteMessage`). This means that multiple accounts can concurrently call this function to read the value. + +#### Declaration + +#include_code private_immutable /labs/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr rust + +`PrivateImmutable` variables also have the `initialize` and `get_note` functions on them but no `initialize_or_replace` since they cannot be modified. + +### PrivateSet + +`PrivateSet` is used for managing a collection of notes. Like `PrivateMutable`, this is a private state variable that can be modified. There are two key differences: + +- A `PrivateSet` is not a single value but a _set_ (a collection) of values (represented by notes) +- Any account can insert values into someone else's set. + +The set's current value is the collection of notes in the set that have not yet been nullified. These notes can have any type: they could be NFT IDs representing a user's NFT collection, or they might be token amounts, in which case _the sum_ of all values in the set would be the user's current balance. + +#### Declaration + +For example, to add private token balances to storage: + +#include_code private_set /labs/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr rust + +#### `insert` + +Allows us to modify the storage by inserting a note into the `PrivateSet`: + +#include_code private_set_insert /labs/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr rust + +Note: The `Owned` wrapper requires calling `.at(owner)` to access the underlying `PrivateSet` for a specific owner. This binds the owner to the state variable instance. + +#### `get_notes` + +Retrieves notes the account has access to. You can optionally provide filtering options. Returns `ConfirmedNote` instances: + +#include_code private_set_get_notes /labs/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr rust + +#### `pop_notes` + +This function pops (gets, removes and returns) the notes the account has access to. Unlike `get_notes`, this immediately nullifies the notes and returns them directly (not wrapped in `ConfirmedNote`): + +#include_code private_set_pop_notes /labs/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr rust + +#### `remove` + +Will remove a note from the `PrivateSet` if it previously has been read from storage. Takes a `ConfirmedNote` as returned by `get_notes`: + +```rust +let options = NoteGetterOptions::new(); +let confirmed_notes = self.storage.balances.at(owner).get_notes(options); +// ... select a note to remove ... +self.storage.balances.at(owner).remove(confirmed_notes.get(0)); +``` + +Note that if you obtained the note via `get_notes`, it's much better to use `pop_notes`, as `pop_notes` results in significantly fewer constraints due to avoiding an extra hash and read request check. + +### SinglePrivateMutable and SinglePrivateImmutable + +For contract-wide private values (not per-owner), use `SinglePrivateMutable` or `SinglePrivateImmutable`. These store exactly one value for the entire contract - a global singleton - rather than separate values per owner. + +| Type | Use Case | Access Pattern | +|------|----------|----------------| +| `Owned<PrivateMutable<...>>` | Per-owner private state (like balances) | `.at(owner).get_note()` | +| `SinglePrivateMutable` | Contract-wide singleton (like admin) | `.get_note()` directly | + +Since there's only one value at the storage slot, there's no need to specify an owner to look it up: + +```rust +#[storage] +struct Storage<Context> { + admin: SinglePrivateMutable<AddressNote, Context>, + config: SinglePrivateImmutable<ConfigNote, Context>, +} + +// Access directly without .at(owner) +let note_message = self.storage.admin.get_note(); +let config = self.storage.config.get_note(); +``` + +When initializing, you still pass an owner address, but this specifies who can decrypt the note, not the storage location: + +```rust +// owner_address determines who can see the note, not where it's stored +self.storage.admin.initialize(note, owner_address).deliver(MessageDelivery::onchain_constrained()); +``` + +:::warning +`SinglePrivateMutable` uses a nullify-and-recreate pattern when reading. Unless the caller is incentivized to deliver the note message correctly, you should use `MessageDelivery::onchain_constrained()` to prevent malicious actors from bricking the contract by failing to deliver the note. +::: + +## Containers + +### Map + +A `Map` is a key-value container that maps keys to state variables - just like Solidity's `mapping`. It can be used with any state variable to create independent instances for each key. + +For example, a `Map<AztecAddress, PublicMutable<u128>>` can be accessed with an address to obtain the `PublicMutable` that corresponds to it. This is exactly equivalent to a Solidity `mapping (address => uint)`. + +#### Declaration + +```rust +#[storage] +struct Storage<Context> { + // Map of addresses to public balances + public_balances: Map<AztecAddress, PublicMutable<u128, Context>, Context>, + + // Map of addresses to authorized users + authorized_users: Map<AztecAddress, PublicMutable<bool, Context>, Context>, +} +``` + +#### Usage + +Use the `.at()` method to access values by key: + +```rust +#[external("public")] +fn increase_balance(account: AztecAddress, amount: u128) { + let current = self.storage.public_balances.at(account).read(); + self.storage.public_balances.at(account).write(current + amount); +} +``` + +:::note +Maps can only be used with public state variables (`PublicMutable`, `PublicImmutable`, `DelayedPublicMutable`) or other `Map`s. For private state, use the `Owned` wrapper described above. +::: + +### Owned + +The `Owned` wrapper is used with private state variables (`PrivateMutable`, `PrivateImmutable`, and `PrivateSet`) to associate them with a specific owner. This is necessary because private state variables need to know which address owns the notes they manage. + +#### Declaration + +```rust +#[storage] +struct Storage<Context> { + // Single owner's private balance + balances: Owned<PrivateSet<UintNote, Context>, Context>, + + // Single owner's private settings + user_settings: Owned<PrivateMutable<SettingsNote, Context>, Context>, +} +``` + +#### Usage + +Use the `.at(owner)` method to access the underlying state variable for a specific owner: + +```rust +#[external("private")] +fn transfer(from: AztecAddress, to: AztecAddress, amount: u128) { + // Access the balance for the 'from' address + let options = NoteGetterOptions::new(); + let notes = self.storage.balances.at(from).pop_notes(options); + + // Access the balance for the 'to' address + let new_note = UintNote { value: amount }; + self.storage.balances.at(to).insert(new_note).deliver(MessageDelivery::onchain_unconstrained()); +} +``` + +The `Owned` wrapper is essential for private state variables because it binds the owner's address to the state variable instance, enabling proper note encryption, nullifier computation, and access control. + +## Custom Structs in Public Storage + +Both `PublicMutable` and `PublicImmutable` are generic over any serializable type, which means you can store custom structs in public storage. + +### Define a Custom Struct + +To use a custom struct in public storage, it must implement the `Packable` trait: + +```rust +use aztec::protocol::{ + address::AztecAddress, + traits::{Deserialize, Packable, Serialize} +}; + +#[derive(Deserialize, Packable, Serialize)] +pub struct Asset { + pub interest_accumulator: u128, + pub last_updated_ts: u64, + pub loan_to_value: u128, + pub oracle: AztecAddress, +} +``` + +### Store and Use Custom Structs + +```rust +#[storage] +struct Storage<Context> { + assets: Map<Field, PublicMutable<Asset, Context>, Context>, +} + +#[external("public")] +fn update_asset(asset_id: Field, new_accumulator: u128) { + let mut asset = self.storage.assets.at(asset_id).read(); + asset.interest_accumulator = new_accumulator; + self.storage.assets.at(asset_id).write(asset); +} +``` + +## Storage Slots + +Each state variable gets assigned a different numerical value for their **storage slot**. How they are used depends on the kind of state variable: + +- For public state variables, storage slots are related to slots in the public data tree +- For private state variables, storage slots are metadata that gets included in the note hash + +The purpose of slots is the same for both domains: they keep the values of different state variables _separate_ so that they do not interfere with one another. + +Storage slots are a low-level detail that developers don't typically need to concern themselves with. They are automatically allocated to each state variable by Aztec.nr. Utilizing storage slots directly can be dangerous as it may accidentally result in data collisions across state variables or invariants being broken. + +In some advanced use cases, it can be useful to have access to these low-level details, such as when implementing [contract upgrades](./contract_upgrades.md) or when interacting with protocol contracts. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/index.md b/docs/lines/v6/docs-developers/docs/aztec-nr/index.md new file mode 100644 index 00000000000..10f13e0e04d --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/index.md @@ -0,0 +1,76 @@ +--- +title: Overview +sidebar_position: 0 +tags: [aztec.nr, smart contracts] +description: Comprehensive guide to writing smart contracts for the Aztec network using Noir. +references: ["docs/examples/contracts/counter_contract/src/main.nr"] +--- + +import DocCardList from "@theme/DocCardList"; + +Aztec.nr is a Noir framework used to develop and test Aztec smart contracts. It contains both high-level abstractions (state variables, messages) and low-level protocol primitives, providing granular control to developers if they want custom contracts. + +:::tip +If you are already familiar with writing Aztec smart contracts and Aztec.nr, visit the [API reference](pathname:///aztec-nr-api/#api_ref_version/). +::: + +## Motivation + +Noir _can_ be used to write circuits, but Aztec contracts are more complex than this. They include multiple external functions, each of a different type: circuits for private functions, AVM bytecode for public functions, and brillig bytecode for utility functions. The circuits for private functions also need to interact with the protocol's kernel circuits in specific ways, so manually writing them, and then combining everything into a contract artifact is involved work. Aztec.nr takes care of all of this heavy lifting and makes writing contracts as simple as marking functions with the corresponding attributes e.g. `#[external("private")]`. + +It allows safe and easy implementation of well understood design patterns, such as the multiple kinds of private state variables, meaning developers don't need to understand the low-levels of how the protocol works. These features are optional, however, advanced developers are not prevented from building their own custom solutions. + +## Design principles + +- Make it hard to shoot yourself in the foot by making it clear when something is unsafe. +- Dangerous actions should be easy to spot. e.g. ignoring return values or calling functions with the `_unsafe` prefix. +- This is achieved by having rails that intentionally trigger a developer's "WTF?" response, to ensure they understand what they're doing. + +A good example of this is writing to private state variables. These functions return a `NoteMessage` struct, which results in a compiler error unless used. This is because writing to private state also requires sending an encrypted message with the new state to the people that need to access it - otherwise, because it is private, they will not even know the state changed. + +```rust +storage.votes.insert(new_vote); // compiler error - unused NoteMessage return value +storage.votes.insert(new_vote).deliver(MessageDelivery::onchain_constrained()); // deliver the note message onchain +``` + +## Contract Development + +### Prerequisites + +- Install [Aztec Local Network and Tooling](../../getting_started_on_local_network.md) +- Install the [Noir VSCode Extension](./installation.md) for syntax highlighting and error detection. + +### Flow + +1. Write your contract and specify your contract dependencies. Create a new project with `aztec new my_project`, which scaffolds a workspace with two crates: a `my_project_contract` crate for your contract and a `my_project_test` crate for tests, with the `aztec` dependency already configured. If you need additional dependencies, add them to `my_project_contract/Nargo.toml`: + +```toml +# my_project_contract/Nargo.toml +[dependencies] +aztec = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="aztec" } +``` + +Update your `my_project_contract/src/main.nr` contract file to use the Aztec.nr macros for writing contracts. + +#include_code setup /docs/examples/contracts/counter_contract/src/main.nr rust + +and import dependencies from the Aztec.nr library. + +#include_code imports /docs/examples/contracts/counter_contract/src/main.nr rust + +:::info + +You can see a complete example of a simple counter contract written with Aztec.nr [here](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/docs/examples/contracts/counter_contract/src/main.nr). + +::: + +2. [Profile](./framework-description/advanced/how_to_profile_transactions.md) the private functions in your contract to get + a sense of how long generating client side proofs will take +3. Write unit tests [directly in Noir](testing_contracts.md) and end-to-end + tests [with TypeScript](../aztec-js/how_to_test.md) +4. [Compile](compiling_contracts.md) your contract +5. [Deploy](../aztec-js/how_to_deploy_contract.md) your contract with Aztec.js + +## Section Contents + +<DocCardList /> diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/installation.md b/docs/lines/v6/docs-developers/docs/aztec-nr/installation.md new file mode 100644 index 00000000000..69de6cfce07 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/installation.md @@ -0,0 +1,31 @@ +--- +title: Noir VSCode Extension +sidebar_position: 1 +tags: [local network, sandbox] +description: Learn how to install and configure the Noir Language Server for a better development experience. +references: ["labs/aztec-up/bin/*"] +--- + +Install the [Noir Language Support extension](https://marketplace.visualstudio.com/items?itemName=noir-lang.vscode-noir) to get syntax highlighting, syntax error detection, and go-to definitions for your Aztec contracts. + +The extension drives its language server with `nargo`. The Aztec installer ships a bundled `nargo` and exposes it as the `aztec-nargo` symlink on your `PATH`. Bare `nargo` is intentionally not provided so it does not shadow your own install (if any). Verify the symlink is on your `PATH`: + +```bash +which aztec-nargo +# expected: $HOME/.aztec/current/bin/aztec-nargo +``` + +If you have not installed the Aztec toolchain yet, follow [Getting Started on Local Network](../../getting_started_on_local_network.md) first. + +## Configure the extension + +Set the extension's `Noir: Nargo Path` setting to the absolute path printed by `which aztec-nargo` (for example `$HOME/.aztec/current/bin/aztec-nargo`), then reload the window. `aztec-nargo` is a symlink to the bundled `nargo`, so any tool that invokes it speaks plain `nargo` (LSP included). + +To confirm the extension is using the bundled toolchain, hover over **Nargo** in the VSCode status bar in the bottom right corner: it should show the path you set. + +If you have your own `nargo` install and want the extension to use that instead, leave `Noir: Nargo Path` empty so the extension auto-discovers `nargo` from your `PATH`. + +## Troubleshooting + +- **LSP reports `startFailed` after setting a custom path**: confirm `aztec-nargo` is executable and that the path is correct, reload the window, and check the **Output** panel for the language server log. +- **Extension picks up the wrong `nargo`**: the Aztec installer no longer puts bare `nargo` on `PATH`. Set `Noir: Nargo Path` explicitly to `aztec-nargo` (for the bundled version) or to your own install (for any other version). diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/logging.md b/docs/lines/v6/docs-developers/docs/aztec-nr/logging.md new file mode 100644 index 00000000000..d291a079829 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/logging.md @@ -0,0 +1,240 @@ +--- +title: Logging from Contracts +sidebar_position: 4 +tags: [contracts, logging, debugging, aztec.nr] +description: Add log statements to your Aztec contracts and control log verbosity in tests and local networks. +references: ["docs/examples/contracts/logging_example/src/main.nr", "labs/noir-projects/aztec-nr/aztec/src/logging.nr"] +--- + +Aztec contracts can emit log messages at seven severity levels. Private function logs appear immediately during local simulation in the Private eXecution Environment (PXE), while public function logs are collected and displayed in test mode. + +## Prerequisites + +- An Aztec contract project set up with the `aztec-nr` dependency +- Basic understanding of [private, public, and utility functions](./framework-description/functions/visibility.md) + +## Import the logging functions + +Logging functions live under `aztec::oracle::logging`. Import the specific functions you need: + +#include_code logging_imports /docs/examples/contracts/logging_example/src/main.nr rust + +Or import only what you need: + +```rust +use aztec::oracle::logging::{info_log, debug_log, debug_log_format}; +``` + +:::warning Old import path removed +The previous import path `dep::aztec::oracle::debug_log` has been removed. Update your imports to use `aztec::oracle::logging` instead. +::: + +## Log levels + +Aztec supports seven log levels, ordered from least to most verbose: + +| Level | Value | When to use | +|---------|-------|-------------| +| `fatal` | 1 | Unrecoverable errors that should always be visible | +| `error` | 2 | Recoverable errors or unexpected conditions | +| `warn` | 3 | Potential issues worth investigating | +| `info` | 4 | General operational information | +| `verbose` | 5 | Detailed information for troubleshooting | +| `debug` | 6 | Development-time debugging output | +| `trace` | 7 | Fine-grained tracing of execution flow | + +When you set `LOG_LEVEL=info`, you see `fatal`, `error`, `warn`, and `info` messages, but `verbose`, `debug`, and `trace` are hidden. + +Here is an example using all seven levels: + +#include_code log_all_levels /docs/examples/contracts/logging_example/src/main.nr rust + +## Simple log messages + +Each level has a function that accepts a plain string with no format arguments: + +#include_code log_simple /docs/examples/contracts/logging_example/src/main.nr rust + +## Log messages with format arguments + +Each level also has a `_format` variant that accepts a format string and an array of `Field` values. Use `{0}`, `{1}`, etc. to insert individual arguments by index, or `{}` to print the entire array: + +#include_code log_format_patterns /docs/examples/contracts/logging_example/src/main.nr rust + +:::note +Format arguments must be `Field` values. Use `.to_field()` to convert addresses and other types: + +#include_code log_address /docs/examples/contracts/logging_example/src/main.nr rust +::: + +## Viewing logs + +### In `aztec test` (Noir tests) + +To see contract logs, set `LOG_LEVEL` to include the `debug_log` module: + +```bash +LOG_LEVEL="error;trace:debug_log" aztec test +``` + +:::tip +Use different log levels strategically: add `info_log` calls for key state transitions you always want to see, and `debug_log` or `trace_log` calls for detailed inspection. +::: + +### In TypeScript tests (jest, vitest) + +TypeScript test environments do not enable contract logs by default. Set the `LOG_LEVEL` environment variable to include `^contract:`, which matches the logs of every contract (see the [`LOG_LEVEL` syntax reference](#log_level-syntax-reference) for how module filters match). Logs that Aztec.nr emits on its own use the `aztecnr:` module instead; add `^aztecnr:` to the filter (for example, `debug:^contract:,^aztecnr:`) to see them too: + +```bash +# Show contract logs at debug level and above +LOG_LEVEL="info;debug:^contract:" yarn test + +# Show all contract log levels (most verbose) +LOG_LEVEL="error;trace:^contract:" yarn test +``` + +### With a local network + +Contract logs appear in the process that runs the PXE — your test process, not the network process. The network window only shows system-level infrastructure logs (archiver, world-state, etc.), which are generally not useful for contract debugging. + +When running TypeScript tests against a local network, set `LOG_LEVEL` on the **test command**: + +```bash +# Your test process sees contract logs +LOG_LEVEL="error;trace:^contract:" yarn test +``` + +You do not need to change the `LOG_LEVEL` on `aztec start --local-network` to see contract logs. + +## `LOG_LEVEL` syntax reference + +The `LOG_LEVEL` environment variable uses a semicolon-delimited format: + +``` +<default_level>;<level>:<module1>,<module2>;<level>:<module3> +``` + +- **First segment (required)**: the default log level for all modules. A bare `level:module` with no preceding default (e.g. `LOG_LEVEL="warn:simulator"`) is invalid and throws `Invalid log level` — the parser always reads the segment before the first `;` as the default level. To filter only specific modules, start with `silent` (e.g. `LOG_LEVEL="silent;debug:simulator"`) +- **Remaining segments**: `level:module` pairs that override the default for specific modules +- Modules are comma-separated within a segment +- The `aztec:` prefix is automatically stripped from module names +- Module filters are regular expressions matched anywhere in the module name, so anchor a filter with `^` (for example, `^contract:`) to match only the modules whose names start with it. A filter that is not a valid regular expression matches the modules whose names start with it + +### Common configurations + +| Scenario | `LOG_LEVEL` value | +|----------|-------------------| +| Contract logs in `aztec test` (TXE) | `error;trace:debug_log` | +| Contract logs in TypeScript tests (PXE) | `error;trace:^contract:` | +| Contract debug+ logs with system info | `info;debug:^contract:` | +| Only contract warnings and errors | `warn;warn:^contract:` | +| Everything verbose | `verbose` | +| Debug a specific system module | `info;debug:sequencer` | +| Multiple module overrides | `warn;debug:sequencer,archiver;trace:^contract:` | + +## How contract logs are displayed + +### In `aztec test` (TXE) + +Contract logs appear under the `debug_log` module: + +``` +[07:40:29.947] DEBUG: txe:top_level_context:debug_log your message here +``` + +### In TypeScript tests (PXE) + +When running through the PXE, the output includes the contract name with an abbreviated address: + +``` +[07:40:29.937] INFO: contract:Counter(0x1234abcd) 164f9c87bca0cf8c Transfer completed +[07:40:29.947] DEBUG: contract:Counter(0x1234abcd) 164f9c87bca0cf8c Processing value: 0x2a +``` + +The hex value after the address (`164f9c87bca0cf8c`) is an internal request identifier. If the contract name cannot be resolved, you see `Unknown` in its place. + +## Logging in public functions + +Private and public functions handle logging differently: + +- **Private functions** execute locally in the PXE. You see logs immediately during simulation. +- **Public functions** execute on the sequencer. You see logs during `.simulate()` calls and after `.send().wait()` completes in test mode. + +The logging API is the same in public functions: + +#include_code log_public /docs/examples/contracts/logging_example/src/main.nr rust + +### Accessing logs programmatically + +In test mode (when not using real proofs), you can access public function debug logs on the `TxReceipt`: + +```typescript +import { applyStringFormatting } from '@aztec-labs/foundation/log'; + +const { receipt } = await contract.methods.myPublicFunction(args).send({ + from: address, + fee: { paymentMethod }, + wait: { timeout: 600 }, +}); + +// Logs are automatically printed to your console. +// You can also access them programmatically: +if (receipt.debugLogs) { + for (const log of receipt.debugLogs) { + console.log(`[${log.level}] ${applyStringFormatting(log.message, log.fields)}`); + } +} +``` + +Each entry contains: + +- `contractAddress` - the contract that emitted the log +- `level` - the log level (`info`, `debug`, etc.) +- `message` - the unformatted message string +- `fields` - the raw `Field` values passed as arguments + +:::warning +`receipt.debugLogs` is only available in test mode (when not using real proofs). In production, debug log collection is disabled. +::: + +## Verify logging works + +Add a `debug_log` call to any contract function, then run with logging enabled: + +```bash +LOG_LEVEL="error;trace:debug_log" aztec test +``` + +You should see output like: + +``` +[07:40:29.947] DEBUG: txe:top_level_context:debug_log your message here +``` + +If no output appears, check the troubleshooting section below. + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| No contract logs appear in `aztec test` | Set `LOG_LEVEL` to include `debug_log`, e.g., `LOG_LEVEL="error;trace:debug_log" aztec test`. Also verify you are calling a log function inside the contract function being tested. | +| No contract logs in TypeScript tests | Set `LOG_LEVEL` to include `^contract:`, e.g., `LOG_LEVEL="error;trace:^contract:" yarn test`. | +| Import error on `dep::aztec::oracle::debug_log` | This path was removed. Update to `use aztec::oracle::logging::{debug_log, debug_log_format};`. | +| `receipt.debugLogs` is `undefined` | Debug logs are only collected in test mode (non-real-proofs). They are not available in production. | +| Too much noise in log output | Narrow the default level and use module filters, e.g., `LOG_LEVEL="error;debug:^contract:"`. | + +## Quick reference + +| Task | Code or command | +|------|-----------------| +| Import logging | `use aztec::oracle::logging::{debug_log, debug_log_format};` | +| Simple log | `debug_log("message");` | +| Log with values | `debug_log_format("val: {0}", [my_field]);` | +| Run Noir tests with logs | `LOG_LEVEL="error;trace:debug_log" aztec test` | +| JS tests with contract logs | `LOG_LEVEL="error;trace:^contract:" yarn test` | + +## Next steps + +- [Debugging Aztec Code](./debugging.md) for error codes, profiling, and common issues +- [Events and Logs](./framework-description/events_and_logs.md) for emitting events that offchain applications can consume +- [Testing Contracts](./testing_contracts.md) for writing and running contract tests diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/standards/_category_.json b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/_category_.json new file mode 100644 index 00000000000..ae26581fa3f --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 10, + "collapsible": true, + "collapsed": true, + "label": "Standards" +} diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-20.md b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-20.md new file mode 100644 index 00000000000..95cfbcc08f1 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-20.md @@ -0,0 +1,112 @@ +--- +title: "AIP-20: Fungible Token" +sidebar_position: 1 +description: Fungible token standard with private balances, partial-note transfers, and recursive note consumption. +--- + +[Source](https://github.com/defi-wonderland/aztec-standards/tree/dev/src/token_contract) + +AIP-20 defines a fungible token with support for private balances (stored as notes in the note hash tree), public balances (stored in contract public storage), and a hybrid transfer path between the two. + +## Storage layout + +The token contract stores its name, symbol, and decimals as immutable public fields. Private balances are held in an `Owned<BalanceSet>` that restricts note access to the balance owner. Public balances use a simple `Map` keyed by address. + +```rust +#[storage] +struct Storage<Context> { + name: PublicImmutable<FieldCompressedString, Context>, + symbol: PublicImmutable<FieldCompressedString, Context>, + decimals: PublicImmutable<u8, Context>, + private_balances: Owned<BalanceSet<Context>, Context>, + total_supply: PublicMutable<u128, Context>, + public_balances: Map<AztecAddress, PublicMutable<u128, Context>, Context>, + minter: PublicImmutable<AztecAddress, Context>, + upgrade_authority: PublicImmutable<AztecAddress, Context>, + asset: PublicImmutable<AztecAddress, Context>, + vault_offset: PublicImmutable<u128, Context>, +} +``` + +The `asset` and `vault_offset` fields exist to support the [AIP-4626 vault pattern](./aip-4626.md). A standalone AIP-20 token that is not used as a vault underlying asset does not need to populate these fields. + +## Note count constants + +In Aztec, every time a user receives private tokens, a new encrypted note is added to their balance. Over time, a user's balance can be spread across dozens of small notes. A transfer must consume enough of these notes to cover the amount, but each note consumed adds computational overhead (gates) to the zero-knowledge proof the user's device must generate. Without a bound, a single transfer could take minutes to prove. Two constants cap how many notes a single proof handles, keeping proving times practical: + +```rust +global INITIAL_TRANSFER_CALL_MAX_NOTES: u32 = 2; +global RECURSIVE_TRANSFER_CALL_MAX_NOTES: u32 = 8; +``` + +The initial call attempts to settle the transfer with at most two notes. If that is not enough to cover the amount, the contract recurses into itself and tries up to eight notes per recursive call. + +A **placeholder address** is used in partial-note flows to signal that a transfer destination is not yet known at the time the sender initiates the operation. This distinguishes "recipient not yet determined" from "recipient is the zero address," and allows offchain indexers to detect [partial-note transfers](#partial-note-transfers) in event logs without decrypting the note contents: + +```rust +global PRIVATE_ADDRESS_MAGIC_VALUE: AztecAddress = + AztecAddress::from_field(0x1ea7e01501975545617c2e694d931cb576b691a4a867fed81ebd3264); +``` + +## Partial-note transfers + +AIP-20 supports partial-note (or "commitment-based") transfers. In Aztec, private functions execute on the user's device before the transaction reaches the network, so they cannot read public state (like a DEX order book or auction result). Partial notes solve this by splitting the operation: the sender privately locks funds into a commitment, and later a public function — which *can* read public state — completes the transfer to the correct recipient. This is what makes private DeFi composability possible. + +Concretely, the sender locks funds in a note whose destination address is not yet known. A completer — typically a contract acting as a relayer or settlement layer — later fills in the recipient and finalizes the note. + +The sender calls `initialize_transfer_commitment` to create the commitment: + +```rust +#[external("private")] +fn initialize_transfer_commitment(to: AztecAddress, completer: AztecAddress) -> Field { + let commitment = self.internal._initialize_transfer_commitment(to, completer); + commitment.to_field() +} +``` + +The returned `Field` is an opaque commitment to the destination and completer. A separate call then subtracts the balance and completes the note: + +```rust +#[external("private")] +fn transfer_private_to_commitment( + from: AztecAddress, + commitment: Field, + amount: u128, + _nonce: Field, +) { + _validate_from_private::<4>(self.context, from); + + self.internal._decrease_private_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES); + + let completer = self.msg_sender(); + PartialUintNote::from_field(commitment).complete_from_private( + self.context, + completer, + amount, + ); +} +``` + +This two-step design is useful in DeFi protocols where the recipient of funds depends on some offchain or asynchronous computation. + +## Recursive balance subtraction + +When `INITIAL_TRANSFER_CALL_MAX_NOTES` notes are insufficient to cover a transfer, the contract calls itself recursively until the full amount is consumed: + +```rust +#[internal("private")] +fn _subtract_balance(account: AztecAddress, amount: u128, max_notes: u32) -> u128 { + let subtracted = self.storage.private_balances.at(account).try_sub(amount, max_notes); + if subtracted >= amount { + subtracted - amount + } else { + assert(subtracted > 0, "Balance too low"); + let remaining = amount - subtracted; + self.call_self.recurse_subtract_balance_internal(account, remaining) + } +} +``` + +The recursion terminates when either the full amount has been deducted or the assertion fires. + +Without recursion, you would have to either size every circuit for the worst-case note count (making the common case expensive to prove) or fail transfers when the note count exceeds a fixed limit. Recursion gives the best of both worlds: the common case (2 notes) proves fast, while larger balances are handled by chaining multiple smaller proofs. Each recursive call is a separate private kernel circuit, so proving cost scales with the actual note count rather than the worst case. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-4626.md b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-4626.md new file mode 100644 index 00000000000..7db638cf283 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-4626.md @@ -0,0 +1,66 @@ +--- +title: "AIP-4626: Tokenized Vault" +sidebar_position: 3 +description: Yield-bearing vault standard with share conversion across private and public contexts. +--- + +[Source](https://github.com/defi-wonderland/aztec-standards/tree/dev/src/vault_contract) (extends the AIP-20 token contract) + +AIP-4626 extends [AIP-20](./aip-20.md) to describe a tokenized vault: a contract that holds an underlying asset and issues shares representing a proportional claim on that asset. It mirrors the design of ERC-4626 but adapts the share conversion arithmetic for Aztec's `u128` integer type. + +## Share conversion + +The vault tracks the total supply of shares and a `vault_offset` that prevents inflation attacks on the initial deposit. The conversion functions use integer arithmetic with configurable rounding direction: + +```rust +#[internal("public")] +fn _convert_to_shares(assets: u128, total_assets: u128, rounding: bool) -> u128 { + let mul_term = + assets * (self.storage.total_supply.read() + self.storage.vault_offset.read()); + let denominator = (total_assets + 1); + let mut shares = mul_term / denominator; + if (rounding == ROUND_UP) & (mul_term % denominator > 0) { + shares = shares + 1; + } + shares +} + +#[internal("public")] +fn _convert_to_assets(shares: u128, total_assets: u128, rounding: bool) -> u128 { + let mul_term = shares * (total_assets + 1); + let denominator = (self.storage.total_supply.read() + self.storage.vault_offset.read()); + let mut assets = mul_term / denominator; + if (rounding == ROUND_UP) & (mul_term % denominator > 0) { + assets = assets + 1; + } + assets +} +``` + +The `+ 1` in the denominator and the `vault_offset` together implement the "virtual shares" technique that prevents the first depositor from manipulating the exchange rate for subsequent depositors. Without this protection, an attacker could deposit 1 wei, then donate a large amount of the underlying asset directly to the vault, inflating the share price so that the next depositor's deposit rounds down to zero shares. + +Deposits round shares down (in favor of the vault), while redemptions round assets down (also in favor of the vault). This is consistent with ERC-4626 rounding conventions and prevents rounding-based extraction attacks. + +## Deposit flow + +A public-to-public deposit transfers assets from the caller to the vault, computes the shares due, and mints them to the recipient: + +```rust +#[external("public")] +fn deposit_public_to_public(from: AztecAddress, to: AztecAddress, assets: u128, _nonce: Field) { + self.internal._validate_from_public(from); + + let total_assets = self.internal._total_assets(); + let shares = self.internal._convert_to_shares(assets, total_assets, ROUND_DOWN); + + // Transfer assets from sender to vault + self.call(Token::at(self.storage.asset.read()).transfer_public_to_public( + from, self.address, assets, _nonce, + )); + + // Mint shares to the recipient + self.internal._mint_to_public(to, shares); +} +``` + +The vault exposes similar entry points for the other combinations of private and public contexts (`deposit_private_to_public`, `deposit_public_to_private`, `deposit_private_to_private`). Each variant transfers assets using the corresponding AIP-20 transfer function and then mints shares into the chosen output context. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-721.md b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-721.md new file mode 100644 index 00000000000..d661cde80af --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/aip-721.md @@ -0,0 +1,72 @@ +--- +title: "AIP-721: Non-Fungible Token" +sidebar_position: 2 +description: Non-fungible token standard with private ownership, partial-note support, and commitment-based transfers. +--- + +[Source](https://github.com/defi-wonderland/aztec-standards/tree/dev/src/nft_contract) + +AIP-721 defines a non-fungible token (NFT). Each token is identified by a unique `token_id` field. Tokens can be held privately in the note hash tree or publicly in a map from `token_id` to owner address. + +## Storage layout + +```rust +#[storage] +struct Storage<Context> { + symbol: PublicImmutable<FieldCompressedString, Context>, + name: PublicImmutable<FieldCompressedString, Context>, + private_nfts: Owned<PrivateSet<NFTNote, Context>, Context>, + nft_exists: Map<Field, PublicMutable<bool, Context>, Context>, + public_owners: Map<Field, PublicMutable<AztecAddress, Context>, Context>, + minter: PublicImmutable<AztecAddress, Context>, + upgrade_authority: PublicImmutable<AztecAddress, Context>, +} +``` + +`nft_exists` tracks whether a given `token_id` has been minted, while `public_owners` records the current public owner. When an NFT is moved to a private note, the `public_owners` entry is cleared and the NFT is stored as an `NFTNote` in the holder's private set. + +## NFTNote and partial-note support + +Each private NFT is represented as an `NFTNote` containing only the `token_id`: + +```rust +#[derive(Eq, Serialize, Packable)] +#[custom_note] +pub struct NFTNote { + pub token_id: Field, +} + +impl NFTNote { + pub fn partial( + owner: AztecAddress, + storage_slot: Field, + context: &mut PrivateContext, + recipient: AztecAddress, + completer: AztecAddress, + ) -> PartialNFTNote { + let randomness = unsafe { random() }; + let commitment = compute_partial_commitment(owner, storage_slot, randomness); + // ... creates encrypted log and validity commitment + let partial_note = PartialNFTNote { commitment }; + let validity_commitment = partial_note.compute_validity_commitment(completer); + context.push_nullifier_unsafe(validity_commitment); + partial_note + } +} +``` + +The `partial` constructor creates a `PartialNFTNote` whose `commitment` field commits to the future owner and storage slot. Without some form of access control, any party could call the completion function and claim the NFT for themselves. The validity commitment prevents this — it is pushed as a nullifier, and only the designated completer can produce the matching preimage needed to finalize the note. This mirrors the partial-note pattern in [AIP-20](./aip-20.md) but applies it to NFT transfers. + +## Partial-note transfer commitment + +The external entry point for initiating a partial NFT transfer is: + +```rust +#[external("private")] +fn initialize_transfer_commitment(to: AztecAddress, completer: AztecAddress) -> Field { + let commitment = self.internal._initialize_transfer_commitment(to, completer); + commitment.commitment() +} +``` + +This function returns `commitment.commitment()` — an opaque `Field` representing the commitment. The AIP-20 equivalent returns `commitment.to_field()` for `PartialUintNote`. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/standards/dripper.md b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/dripper.md new file mode 100644 index 00000000000..7032fcd9076 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/dripper.md @@ -0,0 +1,9 @@ +--- +title: Dripper (Development Faucet) +sidebar_position: 6 +description: Convenience faucet for minting tokens into private or public balances during development. +--- + +[Source](https://github.com/defi-wonderland/aztec-standards/tree/dev/src/dripper) + +The `aztec-standards` repository also ships a **Dripper** contract — a convenience faucet for minting tokens into private or public balances during development. It is not a formal AIP standard and should not be used in production. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/standards/escrow.md b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/escrow.md new file mode 100644 index 00000000000..00761e65fea --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/escrow.md @@ -0,0 +1,79 @@ +--- +title: Escrow +sidebar_position: 4 +description: Minimal token and NFT custody contract with salt-based authorization. +--- + +[Source](https://github.com/defi-wonderland/aztec-standards/tree/dev/src/escrow_contract) + +The Escrow standard provides a minimal contract for holding tokens or NFTs on behalf of a single owner. Rather than storing the owner in mutable private state — which would require note discovery and decryption on every authorization check — the owner is encoded in the contract's own `salt` and thus baked into the contract address at deploy time. This makes authorization a simple field comparison against immutable deployment parameters: cheaper, simpler, and impossible to front-run. + +## Escrow contract + +```rust +#[aztec] +pub contract Escrow { + #[external("private")] + fn withdraw(token: AztecAddress, amount: u128, recipient: AztecAddress) { + self.internal._assert_msg_sender(); + self.call(Token::at(token).transfer_private_to_private( + self.address, recipient, amount, 0, + )); + } + + #[external("private")] + fn withdraw_nft(nft: AztecAddress, token_id: Field, recipient: AztecAddress) { + self.internal._assert_msg_sender(); + self.call(NFT::at(nft).transfer_private_to_private( + self.address, recipient, token_id, 0, + )); + } + + #[internal("private")] + fn _assert_msg_sender() { + let msg_sender = self.msg_sender(); + let escrow_instance: ContractInstance = get_contract_instance(self.address); + assert(AztecAddress::from_field(escrow_instance.salt) == msg_sender, "Not Authorized"); + } +} +``` + +The authorization check in `_assert_msg_sender` reads the `salt` field of the escrow's own `ContractInstance` and compares it against `msg_sender`. Because the `ContractInstance` is fixed at deployment time, this check cannot be spoofed by manipulating storage after deployment. + +## Escrow logic library + +A DeFi protocol (like a lending market or DEX) often needs to give each user a personal escrow to hold collateral or pending settlements. The standard ships a companion library that lets the parent contract deterministically compute escrow addresses from its own address and the user's keys — no onchain deployment transaction required: + +```rust +#[contract_library_method] +pub fn _get_escrow( + context: &mut PrivateContext, + escrow_class_id: Field, + master_secret_keys: MasterSecretKeys, +) -> AztecAddress { + let computed_public_keys: PublicKeys = _secret_keys_to_public_keys(master_secret_keys); + let escrow_instance = ContractInstance { + salt: context.this_address().to_field(), + deployer: AztecAddress::from_field(0), + original_contract_class_id: ContractClassId::from_field(escrow_class_id), + initialization_hash: 0, + public_keys: computed_public_keys, + }; + escrow_instance.to_address() +} + +#[contract_library_method] +pub fn _share_escrow( + context: &mut PrivateContext, + account: AztecAddress, + escrow: AztecAddress, + master_secret_keys: MasterSecretKeys, +) { + let event_struct = EscrowDetailsLogContent { escrow, master_secret_keys }; + emit_event_in_private(context, event_struct).deliver_to( + account, MessageDelivery::onchain_constrained(), + ); +} +``` + +`_get_escrow` reconstructs the escrow address deterministically from the calling contract's address (used as the salt) and a set of master secret keys. `_share_escrow` emits an encrypted log so that the designated `account` can discover the escrow address and the keys needed to access its notes. Without this notification, the user's PXE would have no way to find the escrow or decrypt notes held there. Constrained delivery ensures the log is validated against the note hash tree before the recipient's PXE trusts it. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/standards/generic-proxy.md b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/generic-proxy.md new file mode 100644 index 00000000000..06c3c696c8b --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/generic-proxy.md @@ -0,0 +1,42 @@ +--- +title: Generic Proxy +sidebar_position: 5 +description: Forwarding layer for account abstraction that routes calls by argument count. +--- + +In Aztec, account contracts authorize every transaction the user sends and must be able to forward calls to any contract. However, Noir requires function signatures to be known at compile time, so an account contract cannot call an arbitrary function with an arbitrary number of arguments in a single generic entrypoint. + +The Generic Proxy contract solves this by providing a fixed set of forwarding functions — one per argument count — that the account contract can call. This avoids hard-coding every possible target function signature while keeping the account contract simple. + +```rust +#[aztec] +pub contract GenericProxy { + #[external("private")] + fn forward_private_0(target: AztecAddress, selector: FunctionSelector) { + let _ = self.context.call_private_function_no_args(target, selector); + } + + #[external("private")] + fn forward_private_4(target: AztecAddress, selector: FunctionSelector, args: [Field; 4]) { + let _ = self.context.call_private_function(target, selector, args); + } + + #[external("private")] + fn forward_private_4_and_return( + target: AztecAddress, + selector: FunctionSelector, + args: [Field; 4], + ) -> Field { + let returns: Field = + self.context.call_private_function(target, selector, args).get_preimage(); + returns + } + // ... forward_private_1 through forward_private_8 +} +``` + +The proxy exposes a family of `forward_private_N` functions, each accepting a different fixed argument count. Because Noir's type system requires array lengths to be known at compile time, the contract implements one overload per arity rather than a single variadic function. The `_and_return` variant captures the return value from the callee and passes it back to the caller. + +:::note +The Generic Proxy does not implement any access control by itself. Callers are responsible for ensuring that forwarding to `target` is appropriate. In most protocols, the proxy is called from within an account contract that enforces its own authorization rules before delegating to the proxy. +::: diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/standards/index.md b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/index.md new file mode 100644 index 00000000000..076e7577523 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/standards/index.md @@ -0,0 +1,33 @@ +--- +title: Aztec Contract Standards +sidebar_position: 0 +description: Overview of Aztec Improvement Proposal (AIP) contract standards maintained by DeFi Wonderland. +--- + +Aztec contract standards define shared interfaces and behaviors for common onchain primitives. They serve the same role that ERC standards play on Ethereum: establishing conventions that allow contracts, wallets, and tooling to interoperate without prior coordination. + +The standards described in this section are maintained by [DeFi Wonderland](https://github.com/defi-wonderland/aztec-standards) in the `aztec-standards` repository. Each standard is identified by an **Aztec Improvement Proposal (AIP)** number that mirrors its Ethereum counterpart where applicable (AIP-20 corresponds to ERC-20, AIP-721 to ERC-721, AIP-4626 to ERC-4626). + +Because Aztec contracts have both private and public execution contexts, the standards are more involved than their Ethereum equivalents. Transfers can move value between private notes and public balances, and many operations require coordination between encrypted state and transparent state within a single transaction. + +:::note +The code examples in this section are taken from the [aztec-standards repository](https://github.com/defi-wonderland/aztec-standards) maintained by DeFi Wonderland. They will differ from the reference contract implementations shipped in the [aztec-packages repo](https://github.com/AztecProtocol/aztec-packages) under `noir-projects/noir-contracts/contracts/`. When in doubt, consult the aztec-standards github repo for the canonical standard interfaces. +::: + +## Standards + +- [AIP-20: Fungible Token](./aip-20.md) — private and public balances, partial-note transfers, recursive note consumption +- [AIP-721: Non-Fungible Token](./aip-721.md) — private NFT ownership, partial-note support, commitment-based transfers +- [AIP-4626: Tokenized Vault](./aip-4626.md) — yield-bearing vaults with share conversion across private and public contexts +- [Escrow](./escrow.md) — minimal token/NFT custody with salt-based authorization +- [Generic Proxy](./generic-proxy.md) — forwarding layer for account abstraction patterns +- [Dripper](./dripper.md) — development faucet for testing + +## Related tutorials + +- [Private Token Contract](../../tutorials/contract_tutorials/token_contract.md) — build a privacy-preserving fungible token that closely parallels AIP-20 +- [NFT Bridge](../../tutorials/js_tutorials/token_bridge.md) — build a private NFT with custom `NFTNote` and `PrivateSet`, covering patterns extended by AIP-721 +- [Deploying a Token Contract](../../tutorials/js_tutorials/aztecjs-getting-started.md) — deploy and interact with the reference token contract using Aztec.js +- [Counter Contract](../../tutorials/contract_tutorials/counter_contract.md) — introduces private state, notes, and balance management + +For the canonical implementations and latest interface specifications, refer to the [aztec-standards repository](https://github.com/defi-wonderland/aztec-standards) maintained by DeFi Wonderland. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/testing_contracts.md b/docs/lines/v6/docs-developers/docs/aztec-nr/testing_contracts.md new file mode 100644 index 00000000000..0f1f081d18a --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/testing_contracts.md @@ -0,0 +1,383 @@ +--- +title: Testing Contracts +tags: [contracts, tests, testing, noir] +keywords: [tests, testing, noir] +sidebar_position: 6 +description: Write and run tests for your Aztec smart contracts using Noir's TestEnvironment. +references: ["labs/noir-projects/aztec-nr/aztec/src/test/*", "labs/yarn-project/aztec/src/cli/cmds/start_txe.ts"] +--- + +This guide shows you how to test your Aztec smart contracts using Noir's `TestEnvironment` for fast, lightweight testing. + +## Prerequisites + +- An Aztec contract project with functions to test +- Basic understanding of Noir syntax + +:::tip +For complex cross-chain or integration testing, see the [TypeScript testing guide](../aztec-js/how_to_test.md). +::: + +## Write Aztec contract tests + +Use `TestEnvironment` from `aztec-nr` for contract unit testing: + +- **Fast**: Lightweight environment with mocked components +- **Convenient**: Similar to Foundry for simple contract tests +- **Limited**: No rollup circuits or cross-chain messaging + +For complex end-to-end tests, use [TypeScript testing](../aztec-js/how_to_test.md) with `aztec.js`. + +## Run your tests + +Execute Aztec Noir tests using: + +```bash +aztec test +``` + +### Test execution process + +1. Compile contracts +2. Run `aztec test` + +:::warning +Always use `aztec test` instead of `nargo test`. The `TestEnvironment` requires the test environment oracle resolver provided by the `aztec` CLI. +::: + +## Keep tests in the test crate + +`aztec new` and `aztec init` scaffold a workspace with two crates: a contract crate and a separate test crate. For `aztec new my_project`, these are `my_project_contract` and `my_project_test`. Keep all `#[test]` functions in the test crate, not in the contract crate. + +If tests end up inside a contract crate, `aztec compile` emits a warning: + +```text +WARNING: Found tests in contract crate(s): + my_project_contract::test_something +Tests should be in a dedicated test crate, not in the contract crate. +``` + +The reason is **unnecessary recompilation**: a contract's compiled artifact depends on everything in its crate, so a test-only edit forces the contract to recompile even though its logic has not changed. Keeping tests in the separate test crate lets `aztec test` skip contract recompilation when only test code changed. + +## Basic test structure + +`aztec new my_project` scaffolds a workspace with two crates: a `contract` crate that holds the contract code, and a separate `test` crate that holds your `#[test]` functions: + +```text +my_project/ +├── Nargo.toml # [workspace] members = ["my_project_contract", "my_project_test"] +├── my_project_contract/ +│ ├── Nargo.toml # type = "contract" +│ └── src/main.nr +└── my_project_test/ + ├── Nargo.toml # type = "lib", depends on my_project_contract + └── src/lib.nr # #[test] functions go here +``` + +The motivation for the split of contract and tests into its own crates is **faster iteration**: editing a test does not invalidate the contract's compiled artifact, so `aztec test` skips contract recompilation when only test code changed. + +`aztec compile` warns if it finds `#[test]` functions inside a contract crate. + +The generated test crate template imports the contract by package name and then initializes it: + +```rust +// my_project_test/src/lib.nr +use aztec::test::helpers::test_environment::TestEnvironment; +use my_project_contract::Main; + +#[test] +unconstrained fn test_constructor() { + let mut env = TestEnvironment::new(); + let deployer = env.create_light_account(); + + let _contract_address = env.deploy("@my_project_contract/Main") + .with_private_initializer(deployer, Main::interface().constructor()); +} +``` + +Because tests live in their own crate, we refer to the contract via its crate name using the `@crate_name/ContractName` syntax. + +:::info Test execution notes + +- Tests run in parallel by default +- Use `unconstrained` functions for faster execution +- See all `TestEnvironment` methods [here](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/test/helpers/test_environment/struct.TestEnvironment) +- It is always necessary to deploy a contract in order to test it +::: + +If you'll add arguments to your contract's constructor you pass them directly to the constructor function in the test: + +```rust +let initializer = MyContract::interface().constructor(param1, param2); +``` + +Since Aztec contracts can be initialized both in private and public or they can be interacted with without any kind of initialization (see [Contract creation](../foundational-topics/contract_creation.md) for how Aztec's deployment model differs from Ethereum's) there are 3 options on the deployer: + +```rust +let contract_address = deployer.with_private_initializer(owner, initializer); +let contract_address = deployer.with_public_initializer(owner, initializer); +let contract_address = deployer.without_initializer(); +``` + +:::tip Reusable setup functions +Create a setup function to avoid repeating initialization code: + +```rust +pub unconstrained fn setup(initial_value: Field) -> (TestEnvironment, AztecAddress, AztecAddress) { + let mut env = TestEnvironment::new(); + let owner = env.create_light_account(); + let initializer = MyContract::interface().constructor(initial_value, owner); + let contract_address = env.deploy("@my_project_contract/MyContract").with_private_initializer(owner, initializer); + (env, contract_address, owner) +} + +#[test] +unconstrained fn test_something() { + let (env, contract_address, owner) = setup(42); + // Your test logic here +} +``` + +::: + +## Calling contract functions + +TestEnvironment provides methods for different function types: + +### Private functions + +```rust +// Call private function +env.call_private(caller, Token::at(token_address).transfer(recipient, 100)); + +// Returns the result +let result = env.call_private(owner, Contract::at(address).get_private_data()); +``` + +### Public functions + +```rust +// Call public function +env.call_public(caller, Token::at(token_address).mint_to_public(recipient, 100)); + +// View public state (read-only) +let balance = env.view_public(Token::at(token_address).balance_of_public(owner)); +``` + +### Utility/Unconstrained functions + +```rust +// Simulate utility/view functions (unconstrained) +let total = env.execute_utility(Token::at(token_address).balance_of_private(owner)); + +// To set the `msg_sender` the utility function observes, use the `_opts` variant +let secret = env.execute_utility_opts( + ExecuteUtilityOptions::new().with_from(caller), + Registry::at(registry_address).get_app_siloed_secret(sender, recipient), +).map(|secrets| secrets.shared); +``` + +:::tip Helper function pattern +Create helper functions for common assertions: + +```rust +pub unconstrained fn check_balance( + env: TestEnvironment, + token_address: AztecAddress, + owner: AztecAddress, + expected: u128, +) { + assert_eq( + env.execute_utility(Token::at(token_address).balance_of_private(owner)), + expected + ); +} +``` + +::: + +## Creating accounts + +Two types of accounts are available: + +```rust +// Light account - fast, limited features +let owner = env.create_light_account(); + +// Contract account - full features, slower +let owner = env.create_contract_account(); +``` + +:::info Account type comparison +**Light accounts:** + +- Fast to create +- Work for simple transfers and tests +- Cannot process authwits +- No account contract deployed + +**Contract accounts:** + +- Required for authwit testing +- Support account abstraction features +- Slower to create (deploys account contract) +- Needed for cross-contract authorization + ::: + +:::tip Choosing account types + +```rust +pub unconstrained fn setup(with_authwits: bool) -> (TestEnvironment, AztecAddress, AztecAddress) { + let mut env = TestEnvironment::new(); + let (owner, recipient) = if with_authwits { + (env.create_contract_account(), env.create_contract_account()) + } else { + (env.create_light_account(), env.create_light_account()) + }; + // ... deploy contracts ... + (env, owner, recipient) +} +``` + +::: + +## Testing with authwits + +[Authwits](./framework-description/authentication_witnesses.md) allow one account to authorize another to act on its behalf. + +:::warning +Authwits require **contract accounts**, not light accounts. +::: + +### Import authwit helpers + +```rust +use aztec::test::helpers::authwit::{ + add_private_authwit_from_call, + add_public_authwit_from_call, +}; +``` + +### Private authwits + +```rust +#[test] +unconstrained fn test_private_authwit() { + // Setup with contract accounts (required for authwits) + let (env, token_address, owner, spender) = setup(true); + + // Create the call that needs authorization + let amount = 100; + let nonce = 7; // Non-zero nonce for authwit + let burn_call = Token::at(token_address).burn_private(owner, amount, nonce); + + // Grant authorization from owner to spender + add_private_authwit_from_call(env, owner, spender, burn_call); + + // Spender can now execute the authorized action + env.call_private(spender, burn_call); +} +``` + +### Public authwits + +```rust +#[test] +unconstrained fn test_public_authwit() { + let (env, token_address, owner, spender) = setup(true); + + // Create public action that needs authorization + let transfer_call = Token::at(token_address).transfer_in_public(owner, recipient, 100, nonce); + + // Grant public authorization + add_public_authwit_from_call(env, owner, spender, transfer_call); + + // Execute with authorization + env.call_public(spender, transfer_call); +} +``` + +## Time traveling + +Contract calls do not advance the timestamp by default, despite each of them resulting in a block with a single transaction. Block timestamp can instead be manually manipulated by any of the following methods: + +```rust +// Sets the timestamp of the next block to be mined, i.e. of the next public execution. Does not affect private execution. +env.set_next_block_timestamp(block_timestamp); + +// Same as `set_next_block_timestamp`, but moving time forward by `duration` instead of advancing to a target timestamp. +env.advance_next_block_timestamp_by(duration); + +// Mines an empty block at a given timestamp, causing the next public execution to occur at this time (like `set_next_block_timestamp`), but also allowing for private execution to happen using this empty block as the anchor block. +env.mine_block_at(block_timestamp); +``` + +## Testing failure cases + +Test functions that should fail using annotations: + +### Generic failure + +```rust +#[test(should_fail)] +unconstrained fn test_unauthorized_access() { + let (env, contract, owner) = setup(false); + let attacker = env.create_light_account(); + + // This should fail because attacker is not authorized + env.call_private(attacker, Contract::at(contract).owner_only_function()); +} +``` + +### Specific error message + +```rust +#[test(should_fail_with = "Balance too low")] +unconstrained fn test_insufficient_balance() { + let (env, token, owner, recipient) = setup(false); + + // Try to transfer more than available + let balance = 100; + let transfer_amount = 101; + + env.call_private(owner, Token::at(token).transfer(recipient, transfer_amount)); +} +``` + +### Testing authwit failures + +```rust +#[test(should_fail_with = "Unknown auth witness for message hash")] +unconstrained fn test_missing_authwit() { + let (env, token, owner, spender) = setup(true); + + // Try to burn without authorization + let burn_call = Token::at(token).burn_private(owner, 100, 1); + + // No authwit granted - this should fail + env.call_private(spender, burn_call); +} + +``` + +## Test environment oracle versioning + +The test environment uses an oracle interface to communicate between your Noir test code and the `aztec test` CLI. This interface is versioned so that mismatches between the Aztec.nr dependency used to compile the test and the CLI version are detected automatically. + +The version uses two components, `major.minor`, with the same compatibility rules as [PXE oracle versioning](../foundational-topics/pxe/index.md#oracle-versioning): + +- **`major`** must match exactly. A major bump means oracles were removed or had their signatures changed, and a test environment on a different major cannot safely run the test. +- **`minor`** indicates additive changes (new oracles). The test environment uses a best-effort approach: a test compiled against a higher `minor` is still allowed to run, and an error is only thrown if the test actually invokes an oracle the test environment does not know about. + +### Resolving a version mismatch + +If you see an error like _"Incompatible test environment version: The test was compiled with a newer version of Aztec.nr than your test environment supports"_, the test uses oracles from a newer Aztec.nr than your `aztec test` CLI supports. + +To fix it, make sure your `aztec` CLI version and the `aztec` dependency in the test crate's `Nargo.toml` are on the same release. Note that the test crate's Aztec.nr version can differ from the contract crate's version, depending on your project configuration. For example, if your CLI is on `#include_aztec_version`, the test crate's `Nargo.toml` should reference the matching tag: + +```toml +[dependencies] +aztec = { git="https://github.com/aztec-labs-eng/aztec-nr", tag="#include_aztec_version", directory="aztec" } +``` + +If the test environment reports a version that _should_ include every oracle the test needs but an oracle is still missing, this is likely a bug rather than a version problem. diff --git a/docs/lines/v6/docs-developers/docs/cli/_category_.json b/docs/lines/v6/docs-developers/docs/cli/_category_.json new file mode 100644 index 00000000000..268e67ea550 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/cli/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "CLI", + "position": 5, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/cli/aztec_cli_reference.md b/docs/lines/v6/docs-developers/docs/cli/aztec_cli_reference.md new file mode 100644 index 00000000000..1677f7f7278 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/cli/aztec_cli_reference.md @@ -0,0 +1,1951 @@ +--- +title: Aztec CLI Reference +description: Comprehensive auto-generated reference for the Aztec CLI with all commands and options. +tags: [cli, reference, autogenerated] +sidebar_position: 1 +--- + +# Aztec CLI Reference + +*This documentation is auto-generated from the `aztec` CLI help output.* + + +*Generated: Thu 29 Oct 2026 20:14:29 UTC* + +*Command: `aztec`* + +## Table of Contents + +- [aztec](#aztec) + - [aztec add-l1-validator](#aztec-add-l1-validator) + - [aztec advance-epoch](#aztec-advance-epoch) + - [aztec block-number](#aztec-block-number) + - [aztec codegen](#aztec-codegen) + - [aztec compile](#aztec-compile) + - [aztec compute-genesis-values](#aztec-compute-genesis-values) + - [aztec debug-rollup](#aztec-debug-rollup) + - [aztec decode-enr](#aztec-decode-enr) + - [aztec deploy-l1-contracts](#aztec-deploy-l1-contracts) + - [aztec deploy-new-rollup](#aztec-deploy-new-rollup) + - [aztec deposit-governance-tokens](#aztec-deposit-governance-tokens) + - [aztec execute-governance-proposal](#aztec-execute-governance-proposal) + - [aztec fast-forward-epochs](#aztec-fast-forward-epochs) + - [aztec generate-bls-keypair](#aztec-generate-bls-keypair) + - [aztec generate-bootnode-enr](#aztec-generate-bootnode-enr) + - [aztec generate-keys](#aztec-generate-keys) + - [aztec generate-l1-account](#aztec-generate-l1-account) + - [aztec generate-p2p-private-key](#aztec-generate-p2p-private-key) + - [aztec get-block](#aztec-get-block) + - [aztec get-current-min-fee](#aztec-get-current-min-fee) + - [aztec get-l1-addresses](#aztec-get-l1-addresses) + - [aztec get-l1-balance](#aztec-get-l1-balance) + - [aztec get-l1-to-l2-message-witness](#aztec-get-l1-to-l2-message-witness) + - [aztec get-logs](#aztec-get-logs) + - [aztec get-node-info](#aztec-get-node-info) + - [aztec init](#aztec-init) + - [aztec initiate-withdraw-by-attester](#aztec-initiate-withdraw-by-attester) + - [aztec initiate-withdraw-by-attester-batch](#aztec-initiate-withdraw-by-attester-batch) + - [aztec migrate-ha-db](#aztec-migrate-ha-db) + - [aztec migrate-ha-db down](#aztec-migrate-ha-db-down) + - [aztec migrate-ha-db up](#aztec-migrate-ha-db-up) + - [aztec new](#aztec-new) + - [aztec preload-crs](#aztec-preload-crs) + - [aztec profile](#aztec-profile) + - [aztec profile flamegraph](#aztec-profile-flamegraph) + - [aztec profile gates](#aztec-profile-gates) + - [aztec propose-with-lock](#aztec-propose-with-lock) + - [aztec prover](#aztec-prover) + - [aztec prover get-jobs](#aztec-prover-get-jobs) + - [aztec prover start-proof](#aztec-prover-start-proof) + - [aztec prune-rollup](#aztec-prune-rollup) + - [aztec remove-l1-validator](#aztec-remove-l1-validator) + - [aztec setup-protocol-contracts](#aztec-setup-protocol-contracts) + - [aztec sign-attester-exit](#aztec-sign-attester-exit) + - [aztec start](#aztec-start) + - [aztec test](#aztec-test) + - [aztec trigger-seed-snapshot](#aztec-trigger-seed-snapshot) + - [aztec update](#aztec-update) + - [aztec validator-keys|valKeys](#aztec-validator-keysvalkeys) + - [aztec vote-on-governance-proposal](#aztec-vote-on-governance-proposal) +## aztec + +Aztec command line interface + +**Usage:** +```bash +aztec [options] [command] +``` + +**Available Commands:** + +- `add-l1-validator [options]` - Adds a validator to the L1 rollup contract via a direct deposit. +- `advance-epoch [options]` - Use L1 cheat codes to warp time until the next epoch. +- `block-number [options]` - Gets the current Aztec L2 block number. +- `codegen [options] <noir-abi-path>` - Validates and generates an Aztec Contract ABI from Noir ABI. +- `compile [nargo-args...]` - Compile Aztec Noir contracts using nargo and postprocess them to generate transpiled artifacts and verification keys. All options are forwarded to nargo compile. +- `compute-genesis-values [options]` - Computes genesis values (VK tree root, protocol contracts hash, genesis archive root). +- `debug-rollup [options]` - Debugs the rollup contract. +- `decode-enr <enr>` - Decodes an ENR record +- `deploy-l1-contracts [options]` - Deploys all necessary Ethereum contracts for Aztec. +- `deploy-new-rollup [options]` - Deploys a new rollup contract and adds it to the registry (if you are the owner). +- `deposit-governance-tokens [options]` - Deposits governance tokens to the governance contract. +- `execute-governance-proposal [options]` - Executes a governance proposal. +- `fast-forward-epochs [options]` - Fast forwards the epoch of the L1 rollup contract. +- `generate-bls-keypair [options]` - Generate a BLS keypair with convenience flags +- `generate-bootnode-enr [options] <privateKey> <p2pIp> <p2pPort>` - Generates the encoded ENR record for a bootnode. +- `generate-keys [options]` - Generates encryption and signing private keys. +- `generate-l1-account [options]` - Generates a new private key for an account on L1. +- `generate-p2p-private-key` - Generates a LibP2P peer private key. +- `get-block [options] [blockNumber]` - Gets info for a given block or latest. +- `get-current-min-fee [options]` - Gets the current base fee. +- `get-l1-addresses [options]` - Gets the addresses of the L1 contracts. +- `get-l1-balance [options] <who>` - Gets the balance of an ERC token in L1 for the given Ethereum address. +- `get-l1-to-l2-message-witness [options]` - Gets a L1 to L2 message witness. +- `get-logs [options]` - Gets public logs for a contract and tag, optionally restricted by block range or tx hash. +- `get-node-info [options]` - Gets the information of an Aztec node from a PXE or directly from an Aztec node. +- `help [command]` - display help for command +- `init` - creates a new Aztec Noir workspace in the current directory. +- `initiate-withdraw-by-attester [options]` - Initiates a withdrawal signed by the position's attester. +- `initiate-withdraw-by-attester-batch [options]` - Relays a JSON array of attester-signed withdrawal authorizations. +- `migrate-ha-db` - Run validator-ha-signer database migrations +- `new <name>` - creates a new Aztec Noir workspace in its own directory (or creates a new contract-test crates pair and adds it to the current workspace if run in workspace). +- `preload-crs` - Preload the points data needed for proving and verifying +- `profile` - Profile compiled Aztec artifacts. +- `propose-with-lock [options]` - Makes a proposal to governance with a lock +- `prover` - Operate a prover node via its admin JSON-RPC endpoint +- `prune-rollup [options]` - Prunes the pending chain on the rollup contract. +- `remove-l1-validator [options]` - Removes a validator to the L1 rollup contract. +- `setup-protocol-contracts [options]` - Bootstrap the blockchain by initializing all the protocol contracts +- `sign-attester-exit [options]` - Signs an exit authorization locally and writes a JSON array for batch submission. +- `start [options]` - Starts Aztec modules. Options for each module can be set as key-value pairs (e.g. "option1=value1,option2=value2") or as environment variables. +- `test [options]` - starts a TXE and runs "nargo test" using it as the oracle resolver. +- `trigger-seed-snapshot [options]` - Triggers a seed snapshot for the next epoch. +- `update [options] [projectPath]` - Updates Nodejs and Noir dependencies +- `validator-keys|valKeys` - Manage validator keystores for node operators +- `vote-on-governance-proposal [options]` - Votes on a governance proposal. + +**Options:** + +- `-V --version` - output the version number +- `-h --help` - display help for command + +### Subcommands + +### aztec add-l1-validator + +Adds a validator to the L1 rollup contract via a direct deposit. + +**Usage:** +```bash +aztec add-l1-validator [options] +``` + +**Options:** + +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `--network <string>` - Network to execute against (env: NETWORK) +- `-pk, --private-key <string>` - The private key to use sending the transaction +- `-m, --mnemonic <string>` - The mnemonic to use sending the transaction (default: "test test test test test test test test test test test junk") +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--attester <address>` - ethereum address of the attester +- `--withdrawer <address>` - ethereum address of the withdrawer +- `--bls-secret-key <string>` - The BN254 scalar field element used as a secret key for BLS signatures. Will be associated with the attester address. +- `--move-with-latest-rollup` - Whether to move with the latest rollup (default: true) +- `--no-move-with-latest-rollup` - Keep the stake on this rollup after an upgrade +- `--rollup <string>` - Rollup contract address +- `-h, --help` - display help for command + +### aztec advance-epoch + +Use L1 cheat codes to warp time until the next epoch. + +**Usage:** +```bash +aztec advance-epoch [options] +``` + +**Options:** + +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-n, --node-url <string>` - URL of the Aztec node (default: "http://localhost:8080", env: AZTEC_NODE_URL) +- `-h, --help` - display help for command + +### aztec block-number + +Gets the current Aztec L2 block number. + +**Usage:** +```bash +aztec block-number [options] +``` + +**Options:** + +- `-n, --node-url <string>` - URL of the Aztec node (default: "http://localhost:8080", env: AZTEC_NODE_URL) +- `-h, --help` - display help for command + +### aztec codegen + +Validates and generates an Aztec Contract ABI from Noir ABI. + +**Usage:** +```bash +aztec codegen [options] <noir-abi-path> +``` + +**Options:** + +- `-o, --outdir <path>` - Output folder for the generated code. +- `-f, --force` - Force code generation even when the contract has not changed. +- `-h, --help` - display help for command + +### aztec compile + +Compile Aztec Noir contracts using nargo and postprocess them to generate transpiled artifacts and verification keys. All options are forwarded to nargo compile. + +**Usage:** +```bash +aztec compile [options] [nargo-args...] +``` + +**Options:** + +- `-h, --help` - display help for command +- `--package <PACKAGE>` - The name of the package to run the command on. By default run on the first one found moving up along the ancestors of the current directory +- `--workspace` - Run on all packages in the workspace +- `--force` - Force a full recompilation +- `--print-acir` - Display the ACIR for compiled circuit, including the Brillig bytecode +- `--deny-warnings` - Treat all warnings as errors +- `--silence-warnings` - Suppress warnings +- `--debug-comptime-in-file <DEBUG_COMPTIME_IN_FILE>` - Enable printing results of comptime evaluation: provide a path suffix for the module to debug, e.g. "`package_name/src/main.nr`" +- `--skip-underconstrained-check` - Flag to turn off the compiler check for under constrained values. Warning: This can improve compilation speed but can also lead to correctness errors. This check should always be run on production code +- `--skip-brillig-constraints-check` - Flag to turn off the compiler check for missing Brillig call constraints. Warning: This can improve compilation speed but can also lead to correctness errors. This check should always be run on production code +- `--inliner-aggressiveness <INLINER_AGGRESSIVENESS>` - Setting to decide on an inlining strategy for Brillig functions. A more aggressive inliner should generate larger programs but more optimized A less aggressive inliner should generate smaller programs [default: 9223372036854775807] +- `-Z, --unstable-features <UNSTABLE_FEATURES>` - Unstable features to enable for this current build. If non-empty, it disables unstable features required in crate manifests. +- `--no-unstable-features` - Disable any unstable features required in crate manifests +- `-h, --help` - Print help (see a summary with '-h') + +### aztec compute-genesis-values + +Computes genesis values (VK tree root, protocol contracts hash, genesis archive root). + +**Usage:** +```bash +aztec compute-genesis-values [options] +``` + +**Options:** + +- `--test-accounts <boolean>` - Include initial test accounts in genesis state (env: TEST_ACCOUNTS) +- `--sponsored-fpc <boolean>` - Include sponsored FPC contract in genesis state (env: SPONSORED_FPC) +- `-h, --help` - display help for command + +### aztec debug-rollup + +Debugs the rollup contract. + +**Usage:** +```bash +aztec debug-rollup [options] +``` + +**Options:** + +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--rollup <address>` - ethereum address of the rollup contract +- `-h, --help` - display help for command + +### aztec decode-enr + +Decodes and ENR record + +**Usage:** +```bash +aztec decode-enr [options] <enr> +``` + +**Options:** + +- `-h, --help` - display help for command + +### aztec deploy-l1-contracts + +Deploys all necessary Ethereum contracts for Aztec. + +**Usage:** +```bash +aztec deploy-l1-contracts [options] +``` + +**Options:** + +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-pk, --private-key <string>` - The private key to use for deployment +- `--validators <string>` - Comma separated list of validators +- `-m, --mnemonic <string>` - The mnemonic to use in deployment (default: "test test test test test test test test test test test junk") +- `-i, --mnemonic-index <number>` - The index of the mnemonic to use in deployment (default: 0) +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--json` - Output the contract addresses in JSON format +- `--test-accounts` - Populate genesis state with initial fee juice for test accounts +- `--sponsored-fpc` - Populate genesis state with a testing sponsored FPC contract +- `--real-verifier` - Deploy the real verifier (default: false) +- `--existing-token <address>` - Use an existing ERC20 for both fee and staking +- `-h, --help` - display help for command + +### aztec deploy-new-rollup + +Deploys a new rollup contract and adds it to the registry (if you are the owner). + +**Usage:** +```bash +aztec deploy-new-rollup [options] +``` + +**Options:** + +- `-r, --registry-address <string>` - The address of the registry contract +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-pk, --private-key <string>` - The private key to use for deployment +- `--validators <string>` - Comma separated list of validators +- `-m, --mnemonic <string>` - The mnemonic to use in deployment (default: "test test test test test test test test test test test junk") +- `-i, --mnemonic-index <number>` - The index of the mnemonic to use in deployment (default: 0) +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--json` - Output the contract addresses in JSON format +- `--test-accounts` - Populate genesis state with initial fee juice for test accounts +- `--sponsored-fpc` - Populate genesis state with a testing sponsored FPC contract +- `--real-verifier` - Deploy the real verifier (default: false) +- `-h, --help` - display help for command + +### aztec deposit-governance-tokens + +Deposits governance tokens to the governance contract. + +**Usage:** +```bash +aztec deposit-governance-tokens [options] +``` + +**Options:** + +- `-r, --registry-address <string>` - The address of the registry contract +- `--recipient <string>` - The recipient of the tokens +- `-a, --amount <string>` - The amount of tokens to deposit +- `--mint` - Mint the tokens on L1 (default: false) +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `-p, --private-key <string>` - The private key to use to deposit +- `-m, --mnemonic <string>` - The mnemonic to use to deposit (default: "test test test test test test test test test test test junk") +- `-i, --mnemonic-index <number>` - The index of the mnemonic to use to deposit (default: 0) +- `-h, --help` - display help for command + +### aztec execute-governance-proposal + +Executes a governance proposal. + +**Usage:** +```bash +aztec execute-governance-proposal [options] +``` + +**Options:** + +- `-p, --proposal-id <string>` - The ID of the proposal +- `-r, --registry-address <string>` - The address of the registry contract +- `--wait <boolean>` - Whether to wait until the proposal is executable +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `-pk, --private-key <string>` - The private key to use to vote +- `-m, --mnemonic <string>` - The mnemonic to use to vote (default: "test test test test test test test test test test test junk") +- `-i, --mnemonic-index <number>` - The index of the mnemonic to use to vote (default: 0) +- `-h, --help` - display help for command + +### aztec fast-forward-epochs + +*Help for this command is currently unavailable due to a technical issue with option serialization.* + +### aztec generate-bls-keypair + +Generate a BLS keypair with convenience flags + +**Usage:** +```bash +aztec generate-bls-keypair [options] +``` + +**Options:** + +- `--mnemonic <mnemonic>` - Mnemonic for BLS derivation +- `--ikm <hex>` - Initial keying material for BLS (alternative to mnemonic) +- `--bls-path <path>` - EIP-2334 path (default m/12381/3600/0/0/0) +- `--bls-key-derivation-v6` - Replace any BLS key whose estimated proof-of-possession gas exceeds the budget with the next deterministic candidate from the same mnemonic (BLS key derivation v6, budget 270000 gas). ETH keys never change. Pass it again to regenerate the same keys. +- `--bls-key-derivation-max-gas <gas>` - Stricter budget for the estimated proof-of-possession verification gas of each BLS key. The L1 limit (GSE proofOfPossessionGasLimit) is 300,000 gas from the v6 upgrade, and governance may change it; the default budget is 270000, that cap minus a 10% margin. Must be between 137790 and 270000. Without --bls-key-derivation-v6, an over-budget key is an error. Pass the same value again to regenerate the same keys. +- `--skip-bls-key-gas-check` - Keep the first derived BLS key even if its proof-of-possession check could exceed the L1 gas cap (for example, a key already registered). +- `--g2` - Derive on G2 subgroup +- `--compressed` - Output compressed public key +- `--json` - Print JSON output to stdout +- `--out <file>` - Write output to file +- `-h, --help` - display help for command + +### aztec generate-bootnode-enr + +Generates the encoded ENR record for a bootnode. + +**Usage:** +```bash +aztec generate-bootnode-enr [options] <privateKey> <p2pIp> <p2pPort> +``` + +**Options:** + +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `-h, --help` - display help for command + +### aztec generate-keys + +Generates and encryption and signing private key pair. + +**Usage:** +```bash +aztec generate-keys [options] +``` + +**Options:** + +- `--json` - Output the keys in JSON format +- `-h, --help` - display help for command + +### aztec generate-l1-account + +Generates a new private key for an account on L1. + +**Usage:** +```bash +aztec generate-l1-account [options] +``` + +**Options:** + +- `--json` - Output the private key in JSON format +- `-h, --help` - display help for command + +### aztec generate-p2p-private-key + +Generates a private key that can be used for running a node on a LibP2P network. + +**Usage:** +```bash +aztec generate-p2p-private-key [options] +``` + +**Options:** + +- `-h, --help` - display help for command + +### aztec get-block + +Gets info for a given block or latest. + +**Usage:** +```bash +aztec get-block [options] [blockNumber] +``` + +**Options:** + +- `-n, --node-url <string>` - URL of the Aztec node (default: "http://localhost:8080", env: AZTEC_NODE_URL) +- `-h, --help` - display help for command + +### aztec get-current-min-fee + +Gets the current base fee. + +**Usage:** +```bash +aztec get-current-min-fee [options] +``` + +**Options:** + +- `-n, --node-url <string>` - URL of the Aztec node (default: "http://localhost:8080", env: AZTEC_NODE_URL) +- `-h, --help` - display help for command + +### aztec get-l1-addresses + +Gets the addresses of the L1 contracts. + +**Usage:** +```bash +aztec get-l1-addresses [options] +``` + +**Options:** + +- `-r, --registry-address <string>` - The address of the registry contract +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-v, --rollup-version <number>` - The version of the rollup +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--json` - Output the addresses in JSON format +- `-h, --help` - display help for command + +### aztec get-l1-balance + +Gets the balance of an ERC token in L1 for the given Ethereum address. + +**Usage:** +```bash +aztec get-l1-balance [options] <who> +``` + +**Options:** + +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-t, --token <string>` - The address of the token to check the balance of +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--json` - Output the balance in JSON format +- `-h, --help` - display help for command + +### aztec get-l1-to-l2-message-witness + +Gets a L1 to L2 message witness. + +**Usage:** +```bash +aztec get-l1-to-l2-message-witness [options] +``` + +**Options:** + +- `-ca, --contract-address <address>` - Aztec address of the contract. +- `--message-hash <messageHash>` - The L1 to L2 message hash. +- `--secret <secret>` - The secret used to claim the L1 to L2 message +- `-n, --node-url <string>` - URL of the Aztec node (default: "http://localhost:8080", env: AZTEC_NODE_URL) +- `-h, --help` - display help for command + +### aztec get-logs + +Gets public logs for a contract and tag, optionally restricted by block range or tx hash. + +**Usage:** +```bash +aztec get-logs [options] +``` + +**Options:** + +- `-ca, --contract-address <address>` - Contract address that emitted the logs. +- `--tag <tag>` - Tag (Fr value) to filter logs by. +- `-tx, --tx-hash <txHash>` - A transaction hash to restrict the search to. +- `-fb, --from-block <blockNum>` - Initial block number for getting logs (defaults to 1). +- `-tb, --to-block <blockNum>` - Up to which block to fetch logs (defaults to latest). <blockNumber>-<txIndexWithinBlock>-<logIndexWithinTx> to resume pagination after. +- `-n, --node-url <string>` - URL of the Aztec node (default: "http://localhost:8080", env: AZTEC_NODE_URL) +- `--follow` - If set, will keep polling for new logs until interrupted. +- `-h, --help` - display help for command + +### aztec get-node-info + +Gets the information of an Aztec node from a PXE or directly from an Aztec node. + +**Usage:** +```bash +aztec get-node-info [options] +``` + +**Options:** + +- `--json` - Emit output as json +- `-n, --node-url <string>` - URL of the Aztec node (default: "http://localhost:8080", env: AZTEC_NODE_URL) +- `-h, --help` - display help for command + +### aztec init + +Aztec Init - Create a new Aztec Noir project in the current directory + +**Usage:** +```bash +aztec init +``` + +**Options:** + +- `-h, --help` - Print help + +### aztec initiate-withdraw-by-attester + +Initiates a withdrawal signed by the position's attester. + +**Usage:** +```bash +aztec initiate-withdraw-by-attester [options] +``` + +**Options:** + +- `-pk, --private-key <string>` - The attester private key; supply this or --mnemonic (env: PRIVATE_KEY) +- `-m, --mnemonic <string>` - Mnemonic for the attester account; a flag overrides the other credential from the environment, and PRIVATE_KEY wins when both come from the environment (env: MNEMONIC) +- `--account-index <number>` - Mnemonic account index, matching validator-keys generation (default: 0; requires --mnemonic) +- `--address-index <number>` - Mnemonic address index: validator-keys base address index + zero-based validator position; third validator = base + 2 (default: 0; requires --mnemonic) +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--attester <address>` - Attester address of the position to exit +- `--rollup <address>` - Rollup holding the position +- `-h, --help` - display help for command + +### aztec initiate-withdraw-by-attester-batch + +Relays a JSON array of attester-signed withdrawal authorizations. + +**Usage:** +```bash +aztec initiate-withdraw-by-attester-batch [options] +``` + +**Options:** + +- `-pk, --private-key <string>` - The relayer private key; supply this or --mnemonic (env: PRIVATE_KEY) +- `-m, --mnemonic <string>` - Mnemonic for the relayer account; a flag overrides the other credential from the environment, and PRIVATE_KEY wins when both come from the environment (env: MNEMONIC) +- `--account-index <number>` - Mnemonic account index, matching validator-keys generation (default: 0; requires --mnemonic) +- `--address-index <number>` - Mnemonic address index for the relayer paying for the transaction; attesters sign authorizations separately (default: 0; requires --mnemonic) +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--authorizations <path>` - JSON file containing attester, decimal deadline, and signature fields +- `--rollup <address>` - Rollup holding the positions +- `--up-to-limit` - Process the largest permitted prefix instead of reverting when the whole batch is too large +- `--dry-run` - Simulate against current chain state without sending; no relayer key is required +- `-h, --help` - display help for command + +### aztec migrate-ha-db + +Run validator-ha-signer database migrations + +**Usage:** +```bash +aztec migrate-ha-db [options] [command] +``` + +**Available Commands:** + +- `down [options]` - Rollback the last migration +- `help [command]` - display help for command +- `up [options]` - Apply pending migrations + +**Options:** + +- `-h --help` - display help for command + +#### Subcommands + +#### aztec migrate-ha-db down + +Rollback the last migration + +**Usage:** +```bash +aztec migrate-ha-db down [options] +``` + +**Options:** + +- `--database-url <string>` - PostgreSQL connection string +- `--verbose` - Enable verbose output (default: false) +- `-h, --help` - display help for command + +#### aztec migrate-ha-db up + +Apply pending migrations + +**Usage:** +```bash +aztec migrate-ha-db up [options] +``` + +**Options:** + +- `--database-url <string>` - PostgreSQL connection string +- `--verbose` - Enable verbose output (default: false) +- `-h, --help` - display help for command + +### aztec new + +Aztec New - Create a new Aztec Noir project or add a contract to an existing workspace + +**Usage:** +```bash +aztec new <NAME> +``` + +**Options:** + +- `-h, --help` - Print help + +### aztec preload-crs + +Preload the points data needed for proving and verifying + +**Usage:** +```bash +aztec preload-crs [options] +``` + +**Options:** + +- `-h, --help` - display help for command + +### aztec profile + +Profile compiled Aztec artifacts. + +**Usage:** +```bash +aztec profile [options] [command] +``` + +**Available Commands:** + +- `flamegraph <contract-artifact> <function>` - Generate a gate count flamegraph SVG for a contract function. +- `gates [options] [target-dir]` - Display gate counts for all compiled Aztec artifacts in a target directory. +- `help [command]` - display help for command + +**Options:** + +- `-h --help` - display help for command + +#### Subcommands + +#### aztec profile flamegraph + +Generate a gate count flamegraph SVG for a contract function. + +**Usage:** +```bash +aztec profile flamegraph [options] <contract-artifact> <function> +``` + +**Options:** + +- `-h, --help` - display help for command + +#### aztec profile gates + +Display gate counts for all compiled Aztec artifacts in a target directory. + +**Usage:** +```bash +aztec profile gates [options] [target-dir] +``` + +**Options:** + +- `--json` - Output gate counts as JSON instead of a table (default: false) +- `-h, --help` - display help for command + +### aztec propose-with-lock + +Makes a proposal to governance with a lock + +**Usage:** +```bash +aztec propose-with-lock [options] +``` + +**Options:** + +- `-r, --registry-address <string>` - The address of the registry contract +- `-p, --payload-address <string>` - The address of the payload contract +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `-pk, --private-key <string>` - The private key to use to propose +- `-m, --mnemonic <string>` - The mnemonic to use to propose (default: "test test test test test test test test test test test junk") +- `-i, --mnemonic-index <number>` - The index of the mnemonic to use to propose (default: 0) +- `--json` - Output the proposal ID in JSON format +- `-h, --help` - display help for command + +### aztec prover + +Operate a prover node via its admin JSON-RPC endpoint + +**Usage:** +```bash +aztec prover [options] [command] +``` + +**Available Commands:** + +- `get-jobs [options]` - Lists the prover node proving jobs +- `help [command]` - display help for command +- `start-proof [options]` - Schedules proving for the given epoch + +**Options:** + +- `-h --help` - display help for command + +#### Subcommands + +#### aztec prover get-jobs + +Lists the prover node proving jobs + +**Usage:** +```bash +aztec prover get-jobs [options] +``` + +**Options:** + +- `--admin-url <url>` - URL of the prover node admin JSON-RPC endpoint +- `--api-key <key>` - Admin API key +- `-h, --help` - display help for command + +#### aztec prover start-proof + +Schedules proving for the given epoch + +**Usage:** +```bash +aztec prover start-proof [options] +``` + +**Options:** + +- `--epoch <n>` - Epoch number to prove +- `--admin-url <url>` - URL of the prover node admin JSON-RPC endpoint +- `--api-key <key>` - Admin API key +- `-h, --help` - display help for command + +### aztec prune-rollup + +Prunes the pending chain on the rollup contract. + +**Usage:** +```bash +aztec prune-rollup [options] +``` + +**Options:** + +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-pk, --private-key <string>` - The private key to use for deployment +- `-m, --mnemonic <string>` - The mnemonic to use in deployment (default: "test test test test test test test test test test test junk") +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--rollup <address>` - ethereum address of the rollup contract +- `-h, --help` - display help for command + +### aztec remove-l1-validator + +Removes a validator to the L1 rollup contract. + +**Usage:** +```bash +aztec remove-l1-validator [options] +``` + +**Options:** + +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-pk, --private-key <string>` - The private key to use for deployment +- `-m, --mnemonic <string>` - The mnemonic to use in deployment (default: "test test test test test test test test test test test junk") +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--validator <address>` - ethereum address of the validator +- `--recipient <address>` - Address that receives the withdrawn stake; it cannot be changed once set +- `--rollup <address>` - ethereum address of the rollup contract +- `-h, --help` - display help for command + +### aztec setup-protocol-contracts + +Bootstrap the blockchain by initializing all the protocol contracts + +**Usage:** +```bash +aztec setup-protocol-contracts [options] +``` + +**Options:** + +- `-n, --node-url <string>` - URL of the Aztec node (default: "http://localhost:8080", env: AZTEC_NODE_URL) +- `--testAccounts` - Deploy funded test accounts. +- `--json` - Output the contract addresses in JSON format +- `-h, --help` - display help for command + +### aztec sign-attester-exit + +Signs an exit authorization locally and writes a JSON array for batch submission. + +**Usage:** +```bash +aztec sign-attester-exit [options] +``` + +**Options:** + +- `-pk, --private-key <string>` - The attester private key; supply this or --mnemonic (env: PRIVATE_KEY) +- `-m, --mnemonic <string>` - Mnemonic for the attester account; a flag overrides the other credential from the environment, and PRIVATE_KEY wins when both come from the environment (env: MNEMONIC) +- `--account-index <number>` - Mnemonic account index, matching validator-keys generation (default: 0; requires --mnemonic) +- `--address-index <number>` - Mnemonic address index: validator-keys base address index + zero-based validator position; third validator = base + 2 (default: 0; requires --mnemonic) +- `--l1-rpc-urls <string>` - Ethereum host URLs (comma separated); when given, the chain ID they report must match, and they supply it or the rollup when those are not given (env: ETHEREUM_HOSTS) +- `-c, --l1-chain-id <number>` - Chain ID to sign for; required unless --network or --l1-rpc-urls supplies it, and a value from L1_CHAIN_ID alone must be confirmed by one of them (env: L1_CHAIN_ID) +- `--network <string>` - Network to execute against (env: NETWORK) +- `--attester <address>` - Attester address of the position to exit +- `--rollup <address>` - Rollup holding the position; defaults to the canonical rollup of --network +- `--deadline <timestamp>` - Authorization expiry as Unix seconds +- `--output <path>` - JSON output file (must be new unless --append is used) +- `--append` - Append or create if missing, without validating existing authorizations; run initiate-withdraw-by-attester-batch --dry-run after the batch is complete +- `-h, --help` - display help for command + +### aztec start + +**MISC** + +- `--network <value>` + Network to run Aztec on + *Environment: `$NETWORK`* + +- `--enable-auto-shutdown` + Soft-shutdown the node when the canonical rollup is no longer compatible (protocol constants diverge), keeping the health server up so K8s probes keep passing. Only applies to nodes following the canonical rollup. + *Environment: `$ENABLE_AUTO_SHUTDOWN`* + +- `--sync-mode <value>` (default: `snapshot`) + Set sync mode to `full` to always sync via L1, `snapshot` to download a snapshot if there is no local data, `force-snapshot` to download even if there is local data. + *Environment: `$SYNC_MODE`* + +- `--snapshots-urls <value>` + Base URLs for snapshots index, comma-separated. + *Environment: `$SYNC_SNAPSHOTS_URLS`* + +- `--fisherman-mode` + Whether to run in fisherman mode. + *Environment: `$FISHERMAN_MODE`* + +- `--local-network` + Starts Aztec Local Network + +- `--local-network.l1Mnemonic <value>` (default: `test test test test test test test test test test test junk`) + Mnemonic for L1 accounts. Will be used + *Environment: `$MNEMONIC`* + +- `--local-network.testAccounts` (default: `true`) + Deploy test accounts on local network start + *Environment: `$TEST_ACCOUNTS`* + +**API** + +- `--port <value>` (default: `8080`) + Port to run the Aztec Services on + *Environment: `$AZTEC_PORT`* + +- `--admin-port <value>` (default: `8880`) + Port to run admin APIs of Aztec Services on + *Environment: `$AZTEC_ADMIN_PORT`* + +- `--admin-api-key-hash <value>` + SHA-256 hex hash of a pre-generated admin API key. When set, the node uses this hash for authentication instead of auto-generating a key. + *Environment: `$AZTEC_ADMIN_API_KEY_HASH`* + +- `--disable-admin-api-key` + Disable API key authentication on the admin RPC endpoint. By default, a key is auto-generated, displayed once, and its hash is persisted. + *Environment: `$AZTEC_DISABLE_ADMIN_API_KEY`* + +- `--reset-admin-api-key [value]` + Force-generate a new admin API key, replacing any previously persisted key hash. The new key is displayed once at startup. + *Environment: `$AZTEC_RESET_ADMIN_API_KEY`* + +- `--node-debug` + Expose debug endpoints (e.g. mineBlock) on the main RPC port + *Environment: `$AZTEC_NODE_DEBUG`* + +- `--api-prefix <value>` + Prefix for API routes on any service that is started + *Environment: `$API_PREFIX`* + +- `--rpcMaxBatchSize <value>` (default: `100`) + Maximum allowed batch size for JSON RPC batch requests. + *Environment: `$RPC_MAX_BATCH_SIZE`* + +- `--rpcMaxBodySize <value>` (default: `1mb`) + Maximum allowed batch size for JSON RPC batch requests. + *Environment: `$RPC_MAX_BODY_SIZE`* + +- `--rpc-http-keep-alive-timeout-ms <value>` (default: `5000`) + HTTP keep-alive timeout for JSON RPC connections in milliseconds. + *Environment: `$RPC_HTTP_KEEP_ALIVE_TIMEOUT_MS`* + +- `--rpc-http-headers-timeout-ms <value>` (default: `60000`) + Timeout for receiving complete HTTP headers on JSON RPC connections in milliseconds. + *Environment: `$RPC_HTTP_HEADERS_TIMEOUT_MS`* + +- `--rpc-cors-allowed-headers <value>` + Headers allowed in cross-origin JSON RPC requests, separated by commas. + *Environment: `$RPC_CORS_ALLOWED_HEADERS`* + +- `--rpc-cors-allowed-origins <value>` + Origins allowed to make credentialed cross-origin JSON RPC requests, separated by commas. + *Environment: `$RPC_CORS_ALLOWED_ORIGINS`* + +- `--rpc-cors-allow-any-origin` + Allow credentialed cross-origin JSON RPC requests from any origin. + *Environment: `$RPC_CORS_ALLOW_ANY_ORIGIN`* + +**ETHEREUM** + +- `--l1-chain-id <value>` + The chain ID of the ethereum host. + *Environment: `$L1_CHAIN_ID`* + +- `--l1-rpc-urls <value>` + List of URLs of Ethereum RPC nodes that services will connect to (comma separated). + *Environment: `$ETHEREUM_HOSTS`* + +- `--l1-consensus-host-urls <value>` + List of URLs of the Ethereum consensus nodes that services will connect to (comma separated) + *Environment: `$L1_CONSENSUS_HOST_URLS`* + +- `--l1-consensus-host-api-keys <value>` + List of API keys for the corresponding L1 consensus clients, if needed. Added to the end of the corresponding URL as "?key=<api-key>" unless a header is defined + *Environment: `$L1_CONSENSUS_HOST_API_KEYS`* + +- `--l1-consensus-host-api-key-headers <value>` + List of header names for the corresponding L1 consensus client API keys, if needed. Added to the corresponding request as "<api-key-header>: <api-key>" + *Environment: `$L1_CONSENSUS_HOST_API_KEY_HEADERS`* + +- `--registry-address <value>` + The deployed L1 registry contract address. + *Environment: `$REGISTRY_CONTRACT_ADDRESS`* + +- `--rollup-version <value>` + The version of the rollup. + *Environment: `$ROLLUP_VERSION`* + +**STORAGE** + +- `--data-directory <value>` + Optional dir to store data. If omitted will store in memory. + *Environment: `$DATA_DIRECTORY`* + +- `--data-store-map-size-kb <value>` (default: `134217728`) + The maximum possible size of a data store DB in KB. Can be overridden by component-specific options. + *Environment: `$DATA_STORE_MAP_SIZE_KB`* + +**WORLD STATE** + +- `--world-state-data-directory <value>` + Optional directory for the world state database + *Environment: `$WS_DATA_DIRECTORY`* + +- `--world-state-db-map-size-kb <value>` + The maximum possible size of the world state DB in KB. Overwrites the general dataStoreMapSizeKb. + *Environment: `$WS_DB_MAP_SIZE_KB`* + +- `--world-state-history-retention <value>` (default: `rolling:64`) + World-state history retention: rolling:N retains N finalized checkpoints, pinned:N retains checkpoint N onwards, archive retains all history. + *Environment: `$WS_HISTORY_RETENTION`* + +**AZTEC NODE** + +- `--node` + Starts Aztec Node with options + +**ARCHIVER** + +- `--archiver.blobSinkMapSizeKb <value>` + The maximum possible size of the blob sink DB in KB. Overwrites the general dataStoreMapSizeKb. + *Environment: `$BLOB_SINK_MAP_SIZE_KB`* + +- `--archiver.blobAllowEmptySources <value>` + Whether to allow having no blob sources configured during startup + *Environment: `$BLOB_ALLOW_EMPTY_SOURCES`* + +- `--archiver.blobFileStoreUrls <value>` + URLs for filestore blob archive, comma-separated. Tried in order until blobs are found. + *Environment: `$BLOB_FILE_STORE_URLS`* + +- `--archiver.blobFileStoreUploadUrl <value>` + URL for uploading blobs to filestore (s3://, gs://, file://) + *Environment: `$BLOB_FILE_STORE_UPLOAD_URL`* + +- `--archiver.blobHealthcheckUploadIntervalMinutes <value>` + Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour) + *Environment: `$BLOB_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES`* + +- `--archiver.blobPreferFilestores <value>` + Whether to prefer filestores over consensus clients when fetching blobs. Default: false. + *Environment: `$BLOB_PREFER_FILESTORES`* + +- `--archiver.blobFileStoreTimeoutMs <value>` + Timeout in ms for HTTP requests to the blob file store. Default: 10000 (10s). + *Environment: `$BLOB_FILE_STORE_TIMEOUT_MS`* + +- `--archiver.archiveApiUrl <value>` + The URL of the archive API + *Environment: `$BLOB_ARCHIVE_API_URL`* + +- `--archiver.archiverPollingIntervalMS <value>` (default: `500`) + The polling interval in ms for retrieving new L2 blocks and encrypted logs. + *Environment: `$ARCHIVER_POLLING_INTERVAL_MS`* + +- `--archiver.archiverBatchSize <value>` (default: `100`) + The number of L2 blocks the archiver will attempt to download at a time. + *Environment: `$ARCHIVER_BATCH_SIZE`* + +- `--archiver.archiverStoreMapSizeKb <value>` + The maximum possible size of the archiver DB in KB. Overwrites the general dataStoreMapSizeKb. + *Environment: `$ARCHIVER_STORE_MAP_SIZE_KB`* + +- `--archiver.blockDurationMs <value>` + Duration per block in milliseconds when building multiple blocks per slot. Used to derive orphan proposed block pruning timing. + *Environment: `$SEQ_BLOCK_DURATION_MS`* + +- `--archiver.checkpointProposalSyncGraceSeconds <value>` + Consensus grace in seconds for a received checkpoint proposal to materialize into local proposed state. + *Environment: `$CHECKPOINT_PROPOSAL_SYNC_GRACE_SECONDS`* + +- `--archiver.skipValidateCheckpointAttestations <value>` + Skip validating checkpoint attestations (for testing purposes only) + +- `--archiver.skipPromoteProposedCheckpointDuringL1Sync <value>` + Skip promoting proposed checkpoints during L1 sync (for testing purposes only) + +- `--archiver.maxAllowedEthClientDriftSeconds <value>` (default: `300`) + Maximum allowed drift in seconds between the Ethereum client and current time. + *Environment: `$MAX_ALLOWED_ETH_CLIENT_DRIFT_SECONDS`* + +- `--archiver.ethereumAllowNoDebugHosts <value>` + Whether to allow starting the archiver without debug/trace method support on Ethereum hosts + *Environment: `$ETHEREUM_ALLOW_NO_DEBUG_HOSTS`* + +- `--archiver.archiverSkipHistoricalLogsCheck <value>` + Skip the startup check that probes the L1 RPC for historical Rollup contract logs. Set to true to bypass the check when the connected RPC node is known to prune old logs. + *Environment: `$ARCHIVER_SKIP_HISTORICAL_LOGS_CHECK`* + +- `--archiver.orphanPruneNoProposalTolerance <value>` (default: `1`) + Local tolerance in seconds before pruning an orphan block when no checkpoint proposal was received. + *Environment: `$ARCHIVER_ORPHAN_PRUNE_NO_PROPOSAL_TOLERANCE`* + +- `--archiver.skipOrphanProposedBlockPruning <value>` + Skip pruning orphan proposed blocks that have no matching proposed checkpoint. + *Environment: `$ARCHIVER_SKIP_ORPHAN_PROPOSED_BLOCK_PRUNING`* + +- `--archiver.testPreloadStandardContracts <value>` + Preload the standard contracts (AuthRegistry, PublicChecks, HandshakeRegistry) into the contract store at block 0. For test environments only, and only safe when genesis seeds the matching registration/deployment nullifiers; otherwise a later onchain publish would collide with the block-0 preload. + *Environment: `$TEST_PRELOAD_STANDARD_CONTRACTS`* + +**SEQUENCER** + +- `--sequencer` + Starts Aztec Sequencer with options + +- `--sequencer.blockDurationMs <value>` (default: `3000`) + Duration per block in milliseconds, used to derive how many blocks fit in a slot. + *Environment: `$SEQ_BLOCK_DURATION_MS`* + +- `--sequencer.validatorPrivateKeys <value>` (default: `[Redacted]`) + List of private keys of the validators participating in attestation duties + *Environment: `$VALIDATOR_PRIVATE_KEYS`* + +- `--sequencer.validatorAddresses <value>` + List of addresses of the validators to use with remote signers + *Environment: `$VALIDATOR_ADDRESSES`* + +- `--sequencer.disableValidator <value>` + Do not run the validator + *Environment: `$VALIDATOR_DISABLED`* + +- `--sequencer.disabledValidators <value>` + Temporarily disable these specific validator addresses + +- `--sequencer.attestationPollingIntervalMs <value>` (default: `200`) + Interval between polling for new attestations + *Environment: `$VALIDATOR_ATTESTATIONS_POLLING_INTERVAL_MS`* + +- `--sequencer.alwaysReexecuteBlockProposals <value>` (default: `true`) + Whether to always reexecute block proposals, even for non-validator nodes (useful for monitoring network status). + +- `--sequencer.skipCheckpointProposalValidation <value>` + Skip checkpoint proposal validation, including the live Inbox endpoint check, and always attest. For testing only (default: false) + +- `--sequencer.skipPushProposedBlocksToArchiver <value>` + Skip pushing proposed blocks to archiver (test only) + +- `--sequencer.attestToEquivocatedProposals <value>` + Agree to attest to equivocated checkpoint proposals (for testing purposes only) + +- `--sequencer.skipProposalSlotValidation <value>` + Accept proposal validation regardless of slot timing (for testing only) + +- `--sequencer.validateMaxL2BlockGas <value>` + Maximum L2 block gas for validation. Proposals exceeding this limit are rejected. + *Environment: `$VALIDATOR_MAX_L2_BLOCK_GAS`* + +- `--sequencer.validateMaxDABlockGas <value>` + Maximum DA block gas for validation. Proposals exceeding this limit are rejected. + *Environment: `$VALIDATOR_MAX_DA_BLOCK_GAS`* + +- `--sequencer.validateMaxTxsPerBlock <value>` + Maximum transactions per block for validation. Proposals exceeding this limit are rejected. + *Environment: `$VALIDATOR_MAX_TX_PER_BLOCK`* + +- `--sequencer.validateMaxTxsPerCheckpoint <value>` + Maximum transactions per checkpoint for validation. Proposals exceeding this limit are rejected. + *Environment: `$VALIDATOR_MAX_TX_PER_CHECKPOINT`* + +- `--sequencer.nodeId <value>` + The unique identifier for this node + *Environment: `$VALIDATOR_HA_NODE_ID`* + +- `--sequencer.pollingIntervalMs <value>` (default: `100`) + The number of ms to wait between polls when a duty is being signed + *Environment: `$VALIDATOR_HA_POLLING_INTERVAL_MS`* + +- `--sequencer.peerSigningTimeoutMs <value>` (default: `3000`) + How long to wait for a peer node's in-progress signing before giving up + *Environment: `$VALIDATOR_HA_SIGNING_TIMEOUT_MS`* + +- `--sequencer.signerCallTimeoutMs <value>` (default: `30000`) + Hard upper bound in ms on a single signer call before it is aborted (default 30000) + *Environment: `$VALIDATOR_SIGNER_CALL_TIMEOUT_MS`* + +- `--sequencer.maxStuckDutiesAgeMs <value>` + The maximum age of a stuck duty in ms (defaults to 2x Aztec slot duration) + *Environment: `$VALIDATOR_HA_MAX_STUCK_DUTIES_AGE_MS`* + +- `--sequencer.cleanupOldDutiesAfterHours <value>` (default: `24`) + Duty retention in hours from signing start (must be positive; minimum two slots) + *Environment: `$VALIDATOR_HA_OLD_DUTIES_MAX_AGE_H`* + +- `--sequencer.signingProtectionMapSizeKb <value>` + Maximum size of the local signing-protection LMDB store in KB. Overwrites the general dataStoreMapSizeKb. + *Environment: `$SIGNING_PROTECTION_MAP_SIZE_KB`* + +- `--sequencer.allowEphemeralSigningProtection <value>` + Allow local signing protection to run against an ephemeral store when no data directory is set (dev/test only). Ephemeral protection does not survive restarts, so this is unsafe for production validators. + *Environment: `$VALIDATOR_ALLOW_EPHEMERAL_SIGNING_PROTECTION`* + +- `--sequencer.haSigningEnabled <value>` + Whether HA signing / slashing protection is enabled + *Environment: `$VALIDATOR_HA_SIGNING_ENABLED`* + +- `--sequencer.databaseUrl <value>` + PostgreSQL connection string for validator HA signer (format: postgresql://user:password@host:port/database) + *Environment: `$VALIDATOR_HA_DATABASE_URL`* + +- `--sequencer.poolMaxCount <value>` (default: `10`) + Maximum number of clients in the pool + *Environment: `$VALIDATOR_HA_POOL_MAX`* + +- `--sequencer.poolMinCount <value>` + Minimum number of clients in the pool + *Environment: `$VALIDATOR_HA_POOL_MIN`* + +- `--sequencer.poolIdleTimeoutMs <value>` (default: `10000`) + Idle timeout in milliseconds + *Environment: `$VALIDATOR_HA_POOL_IDLE_TIMEOUT_MS`* + +- `--sequencer.poolConnectionTimeoutMs <value>` + Connection timeout in milliseconds (0 means no timeout) + *Environment: `$VALIDATOR_HA_POOL_CONNECTION_TIMEOUT_MS`* + +- `--sequencer.sequencerPollingIntervalMS <value>` (default: `500`) + The number of ms to wait between polling for checking to build on the next slot. + *Environment: `$SEQ_POLLING_INTERVAL_MS`* + +- `--sequencer.maxTxsPerCheckpoint <value>` + The maximum number of txs across all blocks in a checkpoint. + *Environment: `$SEQ_MAX_TX_PER_CHECKPOINT`* + +- `--sequencer.minTxsPerBlock <value>` (default: `1`) + The minimum number of txs to include in a block. + *Environment: `$SEQ_MIN_TX_PER_BLOCK`* + +- `--sequencer.minValidTxsPerBlock <value>` + The minimum number of valid txs (after execution) to include in a block. If not set, falls back to minTxsPerBlock. + +- `--sequencer.publishTxsWithProposals <value>` + Whether to publish txs with proposals. + *Environment: `$SEQ_PUBLISH_TXS_WITH_PROPOSALS`* + +- `--sequencer.maxL2BlockGas <value>` + The maximum L2 block gas. + *Environment: `$SEQ_MAX_L2_BLOCK_GAS`* + +- `--sequencer.maxDABlockGas <value>` + The maximum DA block gas. + *Environment: `$SEQ_MAX_DA_BLOCK_GAS`* + +- `--sequencer.perBlockAllocationMultiplier <value>` (default: `1.2`) + Per-block gas budget multiplier for both L2 and DA gas. Budget per block is (checkpointLimit / maxBlocks) * multiplier. Values greater than one allow early blocks to use more than their even share, relying on checkpoint-level capping for later blocks. + *Environment: `$SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER`* + +- `--sequencer.perBlockDAAllocationMultiplier <value>` (default: `1.5`) + Per-block budget multiplier applied to DA gas and blob fields in place of perBlockAllocationMultiplier. Defaults higher than the general multiplier so the largest contract class deploy fits a single block. + *Environment: `$SEQ_PER_BLOCK_DA_ALLOCATION_MULTIPLIER`* + +- `--sequencer.redistributeCheckpointBudget <value>` (default: `true`) + Redistribute remaining checkpoint budget evenly across remaining blocks instead of allowing a single block to consume the entire remaining budget. + *Environment: `$SEQ_REDISTRIBUTE_CHECKPOINT_BUDGET`* + +- `--sequencer.coinbase <value>` + Recipient of block reward. + *Environment: `$COINBASE`* + +- `--sequencer.feeRecipient <value>` + Address to receive fees. + *Environment: `$FEE_RECIPIENT`* + +- `--sequencer.noirExecuteWorkingDirectory <value>` + The working directory to use for simulation/proving + *Environment: `$NOIR_EXECUTE_WORKING_DIRECTORY`* + +- `--sequencer.noirExecuteBinaryPath <value>` + The path to the noir-execute binary + *Environment: `$NOIR_EXECUTE_BINARY_PATH`* + +- `--sequencer.governanceProposerPayload <value>` + The address of the payload for the governanceProposer + *Environment: `$GOVERNANCE_PROPOSER_PAYLOAD_ADDRESS`* + +- `--sequencer.governanceProposerForcePayloadVote <value>` + Keep signalling the configured governance payload even if a proposal referencing it was already executed. + *Environment: `$GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE`* + +- `--sequencer.l1PublishingTime <value>` (default: `12`) + How much time in seconds to allow in the slot for publishing the L1 transaction. + *Environment: `$SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT`* + +- `--sequencer.secondsBeforeInvalidatingBlockAsCommitteeMember <value>` (default: `144`) + How many seconds to wait before trying to invalidate a block from the pending chain as a committee member (zero to never invalidate). The next proposer is expected to invalidate, so the committee acts as a fallback. + *Environment: `$SEQ_SECONDS_BEFORE_INVALIDATING_BLOCK_AS_COMMITTEE_MEMBER`* + +- `--sequencer.secondsBeforeInvalidatingBlockAsNonCommitteeMember <value>` (default: `432`) + How many seconds to wait before trying to invalidate a block from the pending chain as a non-committee member (zero to never invalidate). The next proposer is expected to invalidate, then the committee, so other sequencers act as a fallback. + *Environment: `$SEQ_SECONDS_BEFORE_INVALIDATING_BLOCK_AS_NON_COMMITTEE_MEMBER`* + +- `--sequencer.skipWaitForValidParentCheckpointOnL1 <value>` + Bypass the parent checkpoint validity check before submitting a pipelined checkpoint, allowing the proposer to publish even when the parent landed on L1 with invalid attestations (for testing only) + +- `--sequencer.broadcastInvalidBlockProposal <value>` + Broadcast invalid block proposals with corrupted state (for testing only) + +- `--sequencer.invalidBlockProposalIndexWithinCheckpoint <value>` + Broadcast an invalid block proposal only at this indexWithinCheckpoint (for testing only) + +- `--sequencer.broadcastInvalidCheckpointProposalOnly <value>` + Broadcast invalid checkpoint proposals while keeping the underlying block proposals valid (for testing only). When unset, the checkpoint follows broadcastInvalidBlockProposal. + +- `--sequencer.injectFakeAttestation <value>` + Inject a fake attestation (for testing only) + +- `--sequencer.injectHighSValueAttestation <value>` + Inject a malleable attestation with a high-s value (for testing only) + +- `--sequencer.injectUnrecoverableSignatureAttestation <value>` + Inject an attestation with an unrecoverable signature (for testing only) + +- `--sequencer.injectYParityAttestation <value>` + Inject a non-proposer attestation slot in yParity form in the packed L1 tuple (for testing only) + +- `--sequencer.republishAfterFakeAttestation <value>` + Publish the checkpoint with a fake attestation first, then invalidate it and publish it again with the collected attestations within the same slot (for testing only) + +- `--sequencer.shuffleAttestationOrdering <value>` + Shuffle attestation ordering to create invalid ordering (for testing only) + +- `--sequencer.expectedBlockProposalsPerSlot <value>` + Expected number of block proposals per slot for P2P peer scoring. 0 (default) disables block proposal scoring. Set to a positive value to enable. + *Environment: `$SEQ_EXPECTED_BLOCK_PROPOSALS_PER_SLOT`* + +- `--sequencer.checkpointProposalSyncGraceSeconds <value>` (default: `6`) + Consensus grace in seconds for a received checkpoint proposal to materialize into local proposed state. Defaults to twice the block duration. + *Environment: `$CHECKPOINT_PROPOSAL_SYNC_GRACE_SECONDS`* + +- `--sequencer.maxTxsPerBlock <value>` + The maximum number of txs to include in a block. + *Environment: `$SEQ_MAX_TX_PER_BLOCK`* + +- `--sequencer.attestationPropagationTime <value>` (default: `2`) + How many seconds it takes for proposals and attestations to travel across the p2p layer (one-way). + *Environment: `$SEQ_ATTESTATION_PROPAGATION_TIME`* + +- `--sequencer.checkpointProposalPrepareTime <value>` (default: `1`) + Local time in seconds between the last block build finishing and the checkpoint proposal being ready for p2p send. + *Environment: `$SEQ_CHECKPOINT_PROPOSAL_PREPARE_TIME`* + +- `--sequencer.minBlockDuration <value>` (default: `2`) + Minimum block-building time in seconds still worth allocating if the proposer starts late. + *Environment: `$SEQ_MIN_BLOCK_DURATION`* + +- `--sequencer.maxBlocksPerCheckpoint <value>` (default: `24`) + Maximum number of blocks the sequencer packs into a single checkpoint, and the maximum indexWithinCheckpoint accepted on inbound block proposals. + *Environment: `$MAX_BLOCKS_PER_CHECKPOINT`* + +- `--sequencer.buildCheckpointIfEmpty <value>` + Have sequencer build and publish an empty checkpoint if there are no txs + *Environment: `$SEQ_BUILD_CHECKPOINT_IF_EMPTY`* + +- `--sequencer.minBlocksForCheckpoint <value>` + Minimum number of blocks required for a checkpoint proposal (test only) + +- `--sequencer.skipPublishingCheckpointsPercent <value>` + Percent probability (0 - 100) of sequencer skipping checkpoint publishing (testing only) + *Environment: `$SEQ_SKIP_CHECKPOINT_PUBLISH_PERCENT`* + +- `--sequencer.skipBroadcastProposals <value>` + Skip broadcasting checkpoint and block proposals via gossipsub when proposer (for testing only) + +- `--sequencer.skipBroadcastCheckpointProposal <value>` + Skip broadcasting only the CheckpointProposal via gossipsub when proposer; the held last block is broadcast standalone instead so peers still receive it as a proposed-but-uncheckpointed tip (for testing only) + +- `--sequencer.pauseProposingForSlots <value>` + List of slots for which the sequencer will not produce a proposal (for testing only). Attestation paths are unaffected. + +- `--sequencer.minPeersToPropose <value>` (default: `1`) + Minimum number of connected p2p peers required to build and propose a checkpoint (zero to disable the check). Ignored when p2p is disabled by config. + *Environment: `$SEQ_MIN_PEERS_TO_PROPOSE`* + +- `--sequencer.allowUnsafeInboxCatchupCapacity <value>` + Allow starting with fewer block opportunities per checkpoint than a mandatory streaming-Inbox backlog needs (for testing only). + +- `--sequencer.txPublicSetupAllowListExtend <value>` + Additional entries to extend the default setup allow list. Format: I:address\:selector[:flags],C:classId\:selector[:flags]. Flags: os (onlySelf), rn (rejectNullMsgSender), cl=N (calldataLength), joined with +. + *Environment: `$TX_PUBLIC_SETUP_ALLOWLIST`* + +- `--sequencer.keyStoreDirectory <value>` + Location of key store directory + *Environment: `$KEY_STORE_DIRECTORY`* + +- `--sequencer.sequencerPublisherPrivateKeys <value>` + The private keys to be used by the sequencer publisher. + *Environment: `$SEQ_PUBLISHER_PRIVATE_KEYS`* + +- `--sequencer.sequencerPublisherAddresses <value>` + The addresses of the publishers to use with remote signers + *Environment: `$SEQ_PUBLISHER_ADDRESSES`* + +- `--sequencer.blobAllowEmptySources <value>` + Whether to allow having no blob sources configured during startup + *Environment: `$BLOB_ALLOW_EMPTY_SOURCES`* + +- `--sequencer.blobFileStoreUrls <value>` + URLs for filestore blob archive, comma-separated. Tried in order until blobs are found. + *Environment: `$BLOB_FILE_STORE_URLS`* + +- `--sequencer.blobFileStoreUploadUrl <value>` + URL for uploading blobs to filestore (s3://, gs://, file://) + *Environment: `$BLOB_FILE_STORE_UPLOAD_URL`* + +- `--sequencer.blobHealthcheckUploadIntervalMinutes <value>` + Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour) + *Environment: `$BLOB_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES`* + +- `--sequencer.blobPreferFilestores <value>` + Whether to prefer filestores over consensus clients when fetching blobs. Default: false. + *Environment: `$BLOB_PREFER_FILESTORES`* + +- `--sequencer.blobFileStoreTimeoutMs <value>` + Timeout in ms for HTTP requests to the blob file store. Default: 10000 (10s). + *Environment: `$BLOB_FILE_STORE_TIMEOUT_MS`* + +- `--sequencer.archiveApiUrl <value>` + The URL of the archive API + *Environment: `$BLOB_ARCHIVE_API_URL`* + +- `--sequencer.sequencerPublisherAllowInvalidStates <value>` (default: `true`) + True to use publishers in invalid states (timed out, cancelled, etc) if no other is available + *Environment: `$SEQ_PUBLISHER_ALLOW_INVALID_STATES`* + +- `--sequencer.sequencerPublisherForwarderAddress <value>` + Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only) + *Environment: `$SEQ_PUBLISHER_FORWARDER_ADDRESS`* + +- `--sequencer.sequencerPublisherPreviousL1BlockWaitTimeoutMs <value>` (default: `8000`) + How long to wait for the previous L1 block before sending scheduled publisher txs anyway, in milliseconds. + *Environment: `$SEQ_PUBLISHER_PREVIOUS_L1_BLOCK_WAIT_TIMEOUT_MS`* + +- `--sequencer.sequencerPublisherPreviousL1BlockWaitPollIntervalMs <value>` (default: `500`) + Poll interval while waiting for the previous L1 block before scheduled publisher txs, in milliseconds. + *Environment: `$SEQ_PUBLISHER_PREVIOUS_L1_BLOCK_WAIT_POLL_INTERVAL_MS`* + +- `--sequencer.l1TxFailedStore <value>` + Store for failed L1 transaction inputs (test networks only). Format: gs://bucket/path + *Environment: `$L1_TX_FAILED_STORE`* + +- `--sequencer.publisherFundingThreshold <value>` + Min ETH balance below which a publisher gets funded. Specified in ether (e.g. 0.1). Unset = funding disabled. + *Environment: `$PUBLISHER_FUNDING_THRESHOLD`* + +- `--sequencer.publisherFundingAmount <value>` + Amount of ETH to send when funding a publisher. Specified in ether (e.g. 0.5). Unset = funding disabled. + *Environment: `$PUBLISHER_FUNDING_AMOUNT`* + +- `--sequencer.publisherLowBalanceWarningMultiplier <value>` (default: `4`) + Warn when a publisher balance is below this multiple of the ETH required for a reference L1 send (a first-of-epoch proposal for sequencers, an epoch proof for provers) at current fees. Publishers that cannot afford a send are never used for it. + *Environment: `$PUBLISHER_LOW_BALANCE_WARNING_MULTIPLIER`* + +**PROVER NODE** + +- `--prover-node` + Starts Aztec Prover Node with options + +- `--proverNode.keyStoreDirectory <value>` + Location of key store directory + *Environment: `$KEY_STORE_DIRECTORY`* + +- `--proverNode.noirExecuteWorkingDirectory <value>` + The working directory to use for simulation/proving + *Environment: `$NOIR_EXECUTE_WORKING_DIRECTORY`* + +- `--proverNode.noirExecuteBinaryPath <value>` + The path to the noir-execute binary + *Environment: `$NOIR_EXECUTE_BINARY_PATH`* + +- `--proverNode.bbWorkingDirectory <value>` + The working directory to use for proving + *Environment: `$BB_WORKING_DIRECTORY`* + +- `--proverNode.bbBinaryPath <value>` + The path to the bb binary + *Environment: `$BB_BINARY_PATH`* + +- `--proverNode.bbSkipCleanup <value>` + Whether to skip cleanup of bb temporary files + *Environment: `$BB_SKIP_CLEANUP`* + +- `--proverNode.numConcurrentIVCVerifiers <value>` (default: `8`) + Max concurrent verifications for the RPC verifier (QueuedIVCVerifier). + *Environment: `$BB_NUM_IVC_VERIFIERS`* + +- `--proverNode.bbIVCConcurrency <value>` (default: `1`) + Thread count for the RPC IVC verifier. + *Environment: `$BB_IVC_CONCURRENCY`* + +- `--proverNode.bbChonkVerifyMaxBatch <value>` (default: `16`) + Upper bound on proofs per batch for the peer chonk batch verifier. Proofs are verified immediately as they arrive; this only caps how many can accumulate while a batch is already being processed. + *Environment: `$BB_CHONK_VERIFY_MAX_BATCH`* + +- `--proverNode.bbChonkVerifyConcurrency <value>` (default: `6`) + Thread count for the peer batch verifier parallel reduce. 0 = auto. + *Environment: `$BB_CHONK_VERIFY_BATCH_CONCURRENCY`* + +- `--proverNode.bbDebugOutputDir <value>` + When set, bb.js operations write input/output files and log equivalent CLI commands to this directory + *Environment: `$BB_DEBUG_OUTPUT_DIR`* + +- `--proverNode.nodeUrl <value>` + The URL to the Aztec node to take proving jobs from + *Environment: `$AZTEC_NODE_URL`* + +- `--proverNode.proverId <value>` + Hex value that identifies the prover. Defaults to the address used for submitting proofs if not set. + *Environment: `$PROVER_ID`* + +- `--proverNode.failedProofStore <value>` + Store for failed proof inputs. Google cloud storage is only supported at the moment. Set this value as gs://bucket-name/path/to/store. + *Environment: `$PROVER_FAILED_PROOF_STORE`* + +- `--proverNode.enqueueConcurrency <value>` (default: `50`) + Max concurrent jobs the orchestrator serializes and enqueues to the broker. + *Environment: `$PROVER_ENQUEUE_CONCURRENCY`* + +- `--proverNode.blobSinkMapSizeKb <value>` + The maximum possible size of the blob sink DB in KB. Overwrites the general dataStoreMapSizeKb. + *Environment: `$BLOB_SINK_MAP_SIZE_KB`* + +- `--proverNode.blobAllowEmptySources <value>` + Whether to allow having no blob sources configured during startup + *Environment: `$BLOB_ALLOW_EMPTY_SOURCES`* + +- `--proverNode.blobFileStoreUrls <value>` + URLs for filestore blob archive, comma-separated. Tried in order until blobs are found. + *Environment: `$BLOB_FILE_STORE_URLS`* + +- `--proverNode.blobFileStoreUploadUrl <value>` + URL for uploading blobs to filestore (s3://, gs://, file://) + *Environment: `$BLOB_FILE_STORE_UPLOAD_URL`* + +- `--proverNode.blobHealthcheckUploadIntervalMinutes <value>` + Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour) + *Environment: `$BLOB_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES`* + +- `--proverNode.blobPreferFilestores <value>` + Whether to prefer filestores over consensus clients when fetching blobs. Default: false. + *Environment: `$BLOB_PREFER_FILESTORES`* + +- `--proverNode.blobFileStoreTimeoutMs <value>` + Timeout in ms for HTTP requests to the blob file store. Default: 10000 (10s). + *Environment: `$BLOB_FILE_STORE_TIMEOUT_MS`* + +- `--proverNode.archiveApiUrl <value>` + The URL of the archive API + *Environment: `$BLOB_ARCHIVE_API_URL`* + +- `--proverNode.proverPublisherAllowInvalidStates <value>` (default: `true`) + True to use publishers in invalid states (timed out, cancelled, etc) if no other is available + *Environment: `$PROVER_PUBLISHER_ALLOW_INVALID_STATES`* + +- `--proverNode.publisherFundingThreshold <value>` + Min ETH balance below which a publisher gets funded. Specified in ether (e.g. 0.1). Unset = funding disabled. + *Environment: `$PUBLISHER_FUNDING_THRESHOLD`* + +- `--proverNode.publisherFundingAmount <value>` + Amount of ETH to send when funding a publisher. Specified in ether (e.g. 0.5). Unset = funding disabled. + *Environment: `$PUBLISHER_FUNDING_AMOUNT`* + +- `--proverNode.publisherLowBalanceWarningMultiplier <value>` (default: `4`) + Warn when a publisher balance is below this multiple of the ETH required for a reference L1 send (a first-of-epoch proposal for sequencers, an epoch proof for provers) at current fees. Publishers that cannot afford a send are never used for it. + *Environment: `$PUBLISHER_LOW_BALANCE_WARNING_MULTIPLIER`* + +- `--proverNode.proverPublisherPrivateKeys <value>` + The private keys to be used by the prover publisher. + *Environment: `$PROVER_PUBLISHER_PRIVATE_KEYS`* + +- `--proverNode.proverPublisherAddresses <value>` + The addresses of the publishers to use with remote signers + *Environment: `$PROVER_PUBLISHER_ADDRESSES`* + +- `--proverNode.proverNodeMaxPendingJobs <value>` (default: `10`) + The maximum number of pending jobs for the prover node + *Environment: `$PROVER_NODE_MAX_PENDING_JOBS`* + +- `--proverNode.proverNodePollingIntervalMs <value>` (default: `1000`) + The interval in milliseconds to poll for new jobs + *Environment: `$PROVER_NODE_POLLING_INTERVAL_MS`* + +- `--proverNode.proverNodeMaxParallelBlocksPerEpoch <value>` + The Maximum number of blocks to process in parallel while proving an epoch + *Environment: `$PROVER_NODE_MAX_PARALLEL_BLOCKS_PER_EPOCH`* + +- `--proverNode.proverNodeFailedEpochStore <value>` + File store where to upload node state when an epoch fails to be proven + *Environment: `$PROVER_NODE_FAILED_EPOCH_STORE`* + +- `--proverNode.proverNodeEpochProvingDelayMs <value>` + Optional delay in milliseconds to wait for late-arriving events (e.g. reorgs) to settle before starting top-tree proving for an epoch + +- `--proverNode.txGatheringIntervalMs <value>` (default: `1000`) + How often to check that tx data is available + *Environment: `$PROVER_NODE_TX_GATHERING_INTERVAL_MS`* + +- `--proverNode.txGatheringBatchSize <value>` (default: `10`) + How many transactions to gather from a node in a single request + *Environment: `$PROVER_NODE_TX_GATHERING_BATCH_SIZE`* + +- `--proverNode.txGatheringMaxParallelRequestsPerNode <value>` (default: `100`) + How many tx requests to make in parallel to each node + *Environment: `$PROVER_NODE_TX_GATHERING_MAX_PARALLEL_REQUESTS_PER_NODE`* + +- `--proverNode.txGatheringTimeoutMs <value>` (default: `120000`) + How long to wait for tx data to be available before giving up + *Environment: `$PROVER_NODE_TX_GATHERING_TIMEOUT_MS`* + +- `--proverNode.proverNodeDisableProofPublish <value>` + Whether the prover node skips publishing proofs to L1 + *Environment: `$PROVER_NODE_DISABLE_PROOF_PUBLISH`* + +- `--proverNode.proverNodeDisableFullEpochProving <value>` + Whether the prover node skips proving complete epochs on its own. It still proves partial epochs requested through the startProof admin API, e.g. by a sidecar that pays for early withdrawal proofs. + *Environment: `$PROVER_NODE_DISABLE_FULL_EPOCH_PROVING`* + +- `--proverNode.proofSubmissionTargetAddress <value>` + Optional L1 address the submitEpochRootProof tx is sent to. Must expose the identical submitEpochRootProof ABI and forward to the rollup. Defaults to the rollup address. + *Environment: `$PROVER_NODE_PROOF_SUBMISSION_TARGET_ADDRESS`* + +- `--proverNode.web3SignerUrl <value>` + URL of the Web3Signer instance + *Environment: `$WEB3_SIGNER_URL`* + +**PROVER BROKER** + +- `--prover-broker` + Starts Aztec proving job broker + +- `--proverBroker.proverBrokerJobTimeoutMs <value>` (default: `30000`) + Jobs are retried if not kept alive for this long + *Environment: `$PROVER_BROKER_JOB_TIMEOUT_MS`* + +- `--proverBroker.proverBrokerPollIntervalMs <value>` (default: `1000`) + The interval to check job health status + *Environment: `$PROVER_BROKER_POLL_INTERVAL_MS`* + +- `--proverBroker.proverBrokerJobMaxRetries <value>` (default: `3`) + If starting a prover broker locally, the max number of retries per proving job + *Environment: `$PROVER_BROKER_JOB_MAX_RETRIES`* + +- `--proverBroker.proverBrokerBatchSize <value>` (default: `100`) + The prover broker writes jobs to disk in batches + *Environment: `$PROVER_BROKER_BATCH_SIZE`* + +- `--proverBroker.proverBrokerBatchIntervalMs <value>` (default: `50`) + How often to flush batches to disk + *Environment: `$PROVER_BROKER_BATCH_INTERVAL_MS`* + +- `--proverBroker.proverBrokerMaxEpochsToKeepResultsFor <value>` (default: `1`) + The maximum number of epochs to keep results for + *Environment: `$PROVER_BROKER_MAX_EPOCHS_TO_KEEP_RESULTS_FOR`* + +- `--proverBroker.proverBrokerStoreMapSizeKb <value>` + The size of the prover broker's database. Will override the dataStoreMapSizeKb if set. + *Environment: `$PROVER_BROKER_STORE_MAP_SIZE_KB`* + +- `--proverBroker.proverBrokerDebugReplayEnabled <value>` + Enable debug replay mode for replaying proving jobs from stored inputs + *Environment: `$PROVER_BROKER_DEBUG_REPLAY_ENABLED`* + +**PROVER AGENT** + +- `--prover-agent` + Starts Aztec Prover Agent with options + +- `--proverAgent.proverAgentCount <value>` (default: `1`) + Whether this prover has a local prover agent + *Environment: `$PROVER_AGENT_COUNT`* + +- `--proverAgent.proverAgentPollIntervalMs <value>` (default: `1000`) + The interval agents poll for jobs at + *Environment: `$PROVER_AGENT_POLL_INTERVAL_MS`* + +- `--proverAgent.proverAgentProofTypes <value>` + The types of proofs the prover agent can generate + *Environment: `$PROVER_AGENT_PROOF_TYPES`* + +- `--proverAgent.proverBrokerUrl <value>` + The URL where this agent takes jobs from + *Environment: `$PROVER_BROKER_HOST`* + +- `--proverAgent.realProofs <value>` (default: `true`) + Whether to construct real proofs + *Environment: `$PROVER_REAL_PROOFS`* + +- `--proverAgent.proverTestDelayType <value>` (default: `fixed`) + The type of artificial delay to introduce + *Environment: `$PROVER_TEST_DELAY_TYPE`* + +- `--proverAgent.proverTestDelayMs <value>` + Artificial delay to introduce to all operations to the test prover. + *Environment: `$PROVER_TEST_DELAY_MS`* + +- `--proverAgent.proverTestDelayFactor <value>` (default: `1`) + If using realistic delays, what percentage of realistic times to apply. + *Environment: `$PROVER_TEST_DELAY_FACTOR`* + +- `--proverAgent.proverTestVerificationDelayMs <value>` (default: `10`) + The delay (ms) to inject during fake proof verification + *Environment: `$PROVER_TEST_VERIFICATION_DELAY_MS`* + +- `--proverAgent.cancelJobsOnStop <value>` + Whether to abort pending proving jobs when the orchestrator is cancelled. When false (default), jobs remain in the broker queue and can be reused on restart/reorg. + *Environment: `$PROVER_CANCEL_JOBS_ON_STOP`* + +- `--proverAgent.proofStore <value>` + Optional proof input store for the prover + *Environment: `$PROVER_PROOF_STORE`* + +- `--p2p-enabled [value]` + Enable P2P subsystem + *Environment: `$P2P_ENABLED`* + +- `--p2p.validateMaxTxsPerBlock <value>` + Maximum transactions per block for validation. Overrides maxTxsPerBlock for gossip validation when set. + *Environment: `$VALIDATOR_MAX_TX_PER_BLOCK`* + +- `--p2p.validateMaxTxsPerCheckpoint <value>` + Maximum transactions per checkpoint for validation. Used as fallback for maxTxsPerBlock when that is not set. + *Environment: `$VALIDATOR_MAX_TX_PER_CHECKPOINT`* + +- `--p2p.p2pDiscoveryDisabled <value>` + A flag dictating whether the P2P discovery system should be disabled. + *Environment: `$P2P_DISCOVERY_DISABLED`* + +- `--p2p.blockCheckIntervalMS <value>` (default: `100`) + The frequency in which to check for new L2 blocks. + *Environment: `$P2P_BLOCK_CHECK_INTERVAL_MS`* + +- `--p2p.debugDisableColocationPenalty <value>` + DEBUG: Disable colocation penalty - NEVER set to true in production + *Environment: `$DEBUG_P2P_DISABLE_COLOCATION_PENALTY`* + +- `--p2p.peerCheckIntervalMS <value>` (default: `30000`) + The frequency in which to check for new peers. + *Environment: `$P2P_PEER_CHECK_INTERVAL_MS`* + +- `--p2p.peerFailedBanTimeMs <value>` (default: `300000`) + How long to ban a peer after it fails maximum dial attempts. + *Environment: `$P2P_PEER_FAILED_BAN_TIME_MS`* + +- `--p2p.l2QueueSize <value>` (default: `1000`) + Size of queue of L2 blocks to store. + *Environment: `$P2P_L2_QUEUE_SIZE`* + +- `--p2p.listenAddress <value>` (default: `0.0.0.0`) + The listen address. ipv4 address. + *Environment: `$P2P_LISTEN_ADDR`* + +- `--p2p.p2pPort <value>` (default: `40400`) + The port for the P2P service. Defaults to 40400 + *Environment: `$P2P_PORT`* + +- `--p2p.p2pBroadcastPort <value>` + The port to broadcast the P2P service on (included in the node's ENR). Defaults to P2P_PORT. + *Environment: `$P2P_BROADCAST_PORT`* + +- `--p2p.p2pIp <value>` + The IP address for the P2P service. ipv4 address. + *Environment: `$P2P_IP`* + +- `--p2p.peerIdPrivateKey <value>` + An optional peer id private key. If blank, will generate a random key. + *Environment: `$PEER_ID_PRIVATE_KEY`* + +- `--p2p.peerIdPrivateKeyPath <value>` + An optional path to store generated peer id private keys. If blank, will default to storing any generated keys in the root of the data directory. + *Environment: `$PEER_ID_PRIVATE_KEY_PATH`* + +- `--p2p.bootstrapNodes <value>` + A list of bootstrap peer ENRs to connect to. Separated by commas. + *Environment: `$BOOTSTRAP_NODES`* + +- `--p2p.bootstrapNodeEnrVersionCheck <value>` + Whether to check the version of the bootstrap node ENR. + *Environment: `$P2P_BOOTSTRAP_NODE_ENR_VERSION_CHECK`* + +- `--p2p.bootstrapNodesAsFullPeers <value>` + Whether to consider our configured bootnodes as full peers + *Environment: `$P2P_BOOTSTRAP_NODES_AS_FULL_PEERS`* + +- `--p2p.maxPeerCount <value>` (default: `100`) + The maximum number of peers to connect to. + *Environment: `$P2P_MAX_PEERS`* + +- `--p2p.p2pHealthMinPeers <value>` + Minimum number of connected peers for p2p to be reported as healthy on the status endpoint. Zero never fails the check. + *Environment: `$P2P_HEALTH_MIN_PEERS`* + +- `--p2p.queryForIp <value>` + If announceUdpAddress or announceTcpAddress are not provided, query for the IP address of the machine. Default is false. + *Environment: `$P2P_QUERY_FOR_IP`* + +- `--p2p.publicIpServices <value>` (default: `https://api.ipify.org/,https://checkip.amazonaws.com/,https://ifconfig.me/ip,https://icanhazip.com/`) + Comma-separated HTTPS URLs that return plain-text public IPv4. Used when P2P_QUERY_FOR_IP is true and P2P_IP is unset. Tried in order until one succeeds. + *Environment: `$P2P_PUBLIC_IP_SERVICES`* + +- `--p2p.gossipsubInterval <value>` (default: `700`) + The interval of the gossipsub heartbeat to perform maintenance tasks. + *Environment: `$P2P_GOSSIPSUB_INTERVAL_MS`* + +- `--p2p.gossipsubD <value>` (default: `8`) + The D parameter for the gossipsub protocol. + *Environment: `$P2P_GOSSIPSUB_D`* + +- `--p2p.gossipsubDlo <value>` (default: `4`) + The Dlo parameter for the gossipsub protocol. + *Environment: `$P2P_GOSSIPSUB_DLO`* + +- `--p2p.gossipsubDhi <value>` (default: `12`) + The Dhi parameter for the gossipsub protocol. + *Environment: `$P2P_GOSSIPSUB_DHI`* + +- `--p2p.gossipsubDLazy <value>` (default: `8`) + The Dlazy parameter for the gossipsub protocol. + *Environment: `$P2P_GOSSIPSUB_DLAZY`* + +- `--p2p.gossipsubFloodPublish <value>` + Whether to flood publish messages. - For testing purposes only + *Environment: `$P2P_GOSSIPSUB_FLOOD_PUBLISH`* + +- `--p2p.gossipsubMcacheLength <value>` (default: `12`) + The number of gossipsub interval message cache windows to keep. + *Environment: `$P2P_GOSSIPSUB_MCACHE_LENGTH`* + +- `--p2p.gossipsubMcacheGossip <value>` (default: `3`) + How many message cache windows to include when gossiping with other peers. + *Environment: `$P2P_GOSSIPSUB_MCACHE_GOSSIP`* + +- `--p2p.gossipsubSeenTTL <value>` (default: `1200000`) + How long to keep message IDs in the seen cache. + *Environment: `$P2P_GOSSIPSUB_SEEN_TTL`* + +- `--p2p.maxGossipClockDisparityMs <value>` (default: `500`) + Maximum clock-disparity tolerance (ms) applied to both ends of proposal/attestation gossip receive windows. + *Environment: `$P2P_MAX_GOSSIP_CLOCK_DISPARITY_MS`* + +- `--p2p.gossipsubTxTopicWeight <value>` (default: `1`) + The weight of the tx topic for the gossipsub protocol. + *Environment: `$P2P_GOSSIPSUB_TX_TOPIC_WEIGHT`* + +- `--p2p.gossipsubTxInvalidMessageDeliveriesWeight <value>` (default: `-20`) + The weight of the tx invalid message deliveries for the gossipsub protocol. + *Environment: `$P2P_GOSSIPSUB_TX_INVALID_MESSAGE_DELIVERIES_WEIGHT`* + +- `--p2p.gossipsubTxInvalidMessageDeliveriesDecay <value>` (default: `0.5`) + Determines how quickly the penalty for invalid message deliveries decays over time. Between 0 and 1. + *Environment: `$P2P_GOSSIPSUB_TX_INVALID_MESSAGE_DELIVERIES_DECAY`* + +- `--p2p.peerPenaltyValues <value>` (default: `2,10,50`) + The values for the peer scoring system. Passed as a comma separated list of values in order: low, mid, high tolerance errors. + *Environment: `$P2P_PEER_PENALTY_VALUES`* + +- `--p2p.peerBanDurationSeconds <value>` (default: `86400`) + How long (in seconds) a peer is banned for once its score drops below the ban threshold. + *Environment: `$P2P_PEER_BAN_DURATION_SECONDS`* + +- `--p2p.doubleSpendSeverePeerPenaltyWindow <value>` (default: `30`) + +### aztec test + +*Help for this command is currently unavailable.* + +### aztec trigger-seed-snapshot + +Triggers a seed snapshot for the next epoch. + +**Usage:** +```bash +aztec trigger-seed-snapshot [options] +``` + +**Options:** + +- `-pk, --private-key <string>` - The private key to use for deployment +- `-m, --mnemonic <string>` - The mnemonic to use in deployment (default: "test test test test test test test test test test test junk") +- `--rollup <address>` - ethereum address of the rollup contract +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `-h, --help` - display help for command + +### aztec update + +Updates Nodejs and Noir dependencies + +**Usage:** +```bash +aztec update [options] [projectPath] +``` + +**Options:** + +- `--contract [paths...]` - Paths to contracts to update dependencies (default: []) +- `--aztec-version <semver>` - The version to update Aztec packages to. Defaults to latest (default: "latest") +- `-h, --help` - display help for command + +### aztec validator-keys|valKeys + +*This subcommand does not provide its own help information.* + +### aztec vote-on-governance-proposal + +Votes on a governance proposal. + +**Usage:** +```bash +aztec vote-on-governance-proposal [options] +``` + +**Options:** + +- `-p, --proposal-id <string>` - The ID of the proposal +- `-a, --vote-amount <string>` - The amount of tokens to vote +- `--in-favor <boolean>` - Whether to vote in favor of the proposal. Use "yea" for true, any other value for false. +- `--wait <boolean>` - Whether to wait until the proposal is active +- `-r, --registry-address <string>` - The address of the registry contract +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"], env: ETHEREUM_HOSTS) +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `-pk, --private-key <string>` - The private key to use to vote +- `-m, --mnemonic <string>` - The mnemonic to use to vote (default: "test test test test test test test test test test test junk") +- `-i, --mnemonic-index <number>` - The index of the mnemonic to use to vote (default: 0) +- `-h, --help` - display help for command diff --git a/docs/lines/v6/docs-developers/docs/cli/aztec_up_cli_reference.md b/docs/lines/v6/docs-developers/docs/cli/aztec_up_cli_reference.md new file mode 100644 index 00000000000..a8a64cfd4d2 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/cli/aztec_up_cli_reference.md @@ -0,0 +1,151 @@ +--- +title: Aztec Up CLI Reference +description: Comprehensive auto-generated reference for the Aztec Version Manager with all commands and options. +tags: [cli, reference, autogenerated, version-manager] +sidebar_position: 3 +--- + +# Aztec Up CLI Reference + +*This documentation is auto-generated from the `aztec-up` CLI help output.* + + +*Generated: Thu 22 Oct 2026 18:35:43 UTC* + +*Command: `aztec-up`* + +## Table of Contents + +- [aztec-up](#aztec-up) + - [aztec-up env](#aztec-up-env) + - [aztec-up install](#aztec-up-install) + - [aztec-up list](#aztec-up-list) + - [aztec-up prune](#aztec-up-prune) + - [aztec-up self-update](#aztec-up-self-update) + - [aztec-up uninstall](#aztec-up-uninstall) + - [aztec-up use](#aztec-up-use) +## aztec-up + +aztec-up - Aztec version manager + +**Usage:** +```bash +aztec-up [command] [options] +``` + +**Available Commands:** + +- `env` - Output PATH for .aztecrc version (for eval) +- `install <VERSION>` - Install a version and switch to it +- `list` - List installed versions +- `prune` - Remove all versions except the current one +- `self-update` - Update aztec-up itself to the latest version +- `uninstall <version>` - Remove an installed version +- `use [<version>]` - Switch to an installed version (or read from .aztecrc) + +**Options:** + +- `-h --help` - Show this help message + +**Examples:** +```bash +aztec-up install 0.85.0 Install a specific version +aztec-up install nightly Install the nightly version +aztec-up use 0.85.0 Switch to version 0.85.0 +aztec-up use Read version from .aztecrc and switch to it +aztec-up list Show all installed versions +aztec-up self-update Update aztec-up to latest +``` + +### Subcommands + +### aztec-up env + +Output PATH export for the version specified in .aztecrc + +**Usage:** +```bash +aztec-up env +``` + +**Options:** + +- `-h, --help` - Print help + +### aztec-up install + +Install a version of Aztec and switch to it + +**Usage:** +```bash +aztec-up install <VERSION> +``` + +**Options:** + +- `-h, --help` - Print help + +### aztec-up list + +List installed Aztec versions and available aliases + +**Usage:** +```bash +aztec-up list +``` + +**Options:** + +- `-h, --help` - Print help + +### aztec-up prune + +Remove all installed versions except the currently active one + +**Usage:** +```bash +aztec-up prune +``` + +**Options:** + +- `-h, --help` - Print help + +### aztec-up self-update + +Update aztec-up itself to the latest version + +**Usage:** +```bash +aztec-up self-update +``` + +**Options:** + +- `-h, --help` - Print help + +### aztec-up uninstall + +Remove an installed version of Aztec + +**Usage:** +```bash +aztec-up uninstall <VERSION> +``` + +**Options:** + +- `-h, --help` - Print help + +### aztec-up use + +Switch to an installed version of Aztec + +**Usage:** +```bash +aztec-up use [VERSION] +``` + +**Options:** + +- `-h, --help` - Print help diff --git a/docs/lines/v6/docs-developers/docs/cli/aztec_wallet_cli_reference.md b/docs/lines/v6/docs-developers/docs/cli/aztec_wallet_cli_reference.md new file mode 100644 index 00000000000..0c9b5b296c9 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/cli/aztec_wallet_cli_reference.md @@ -0,0 +1,415 @@ +--- +title: Aztec Wallet CLI Reference +description: Comprehensive auto-generated reference for the Aztec Wallet CLI with all commands and options. +tags: [cli, reference, autogenerated, wallet] +sidebar_position: 2 +--- + +# Aztec Wallet CLI Reference + +*This documentation is auto-generated from the `aztec-wallet` CLI help output.* + + +*Generated: Thu 29 Oct 2026 20:11:01 UTC* + +*Command: `aztec-wallet`* + +## Table of Contents + +- [aztec-wallet](#aztec-wallet) + - [aztec-wallet alias](#aztec-wallet-alias) + - [aztec-wallet authorize-action](#aztec-wallet-authorize-action) + - [aztec-wallet bridge-fee-juice](#aztec-wallet-bridge-fee-juice) + - [aztec-wallet create-account](#aztec-wallet-create-account) + - [aztec-wallet create-authwit](#aztec-wallet-create-authwit) + - [aztec-wallet create-secret](#aztec-wallet-create-secret) + - [aztec-wallet deploy](#aztec-wallet-deploy) + - [aztec-wallet deploy-account](#aztec-wallet-deploy-account) + - [aztec-wallet get-alias](#aztec-wallet-get-alias) + - [aztec-wallet get-fee-juice-balance](#aztec-wallet-get-fee-juice-balance) + - [aztec-wallet get-tx](#aztec-wallet-get-tx) + - [aztec-wallet import-test-accounts](#aztec-wallet-import-test-accounts) + - [aztec-wallet profile](#aztec-wallet-profile) + - [aztec-wallet register-contract](#aztec-wallet-register-contract) + - [aztec-wallet register-sender](#aztec-wallet-register-sender) + - [aztec-wallet send](#aztec-wallet-send) + - [aztec-wallet simulate](#aztec-wallet-simulate) +## aztec-wallet + +Aztec wallet + +**Usage:** +```bash +aztec-wallet [options] [command] +``` + +**Available Commands:** + +- `alias <type> <key> <value>` - Aliases information for easy reference. +- `authorize-action [options] <functionName> <caller>` - Authorizes a public call on the caller, so they can perform an action on behalf of the provided account +- `bridge-fee-juice [options] <amount> <recipient>` - Mints L1 Fee Juice and pushes them to L2. +- `create-account [options]` - Creates an aztec account that can be used for sending transactions. +- `create-authwit [options] <functionName> <caller>` - Creates an authorization witness that can be privately sent to a caller so they can perform an action on behalf of the provided account +- `create-secret [options]` - Creates an aliased secret to use in other commands +- `deploy [options] [artifact]` - Deploys a compiled Aztec.nr contract to Aztec. +- `deploy-account [options] <address>` - Deploys an already registered aztec account that can be used for sending transactions. +- `get-alias [alias]` - Shows stored aliases +- `get-fee-juice-balance [options] <address>` - Checks the Fee Juice balance for a given address. +- `get-tx [options] [txHash]` - Gets the status of the recent txs, or a detailed view if a specific transaction hash is provided +- `help [command]` - display help for command +- `import-test-accounts [options]` - Import test accounts from pxe. +- `profile [options] <functionName>` - Profiles a private function by counting the unconditional operations in its execution steps +- `register-contract [options] [address] [artifact]` - Registers a contract in this wallet's PXE +- `register-sender [options] [address]` - Registers a sender's address in the wallet, so the note synching process will look for notes sent by them +- `send [options] <functionName>` - Calls a function on an Aztec contract. +- `simulate [options] <functionName>` - Simulates the execution of a function on an Aztec contract. + +**Options:** + +- `-V --version` - output the version number +- `-d --data-dir <string>` - Storage directory for wallet data (default: "~/.aztec/wallet") +- `-p --prover <string>` - The type of prover the wallet uses (choices: "wasm", "native", "none", default: "native", env: PXE_PROVER) +- `-n --node-url <string>` - URL of the Aztec node to connect to (default: "http://localhost:8080", env: AZTEC_NODE_URL) +- `-h --help` - display help for command + +### Subcommands + +### aztec-wallet alias + +Aliases information for easy reference. + +**Usage:** +```bash +aztec-wallet alias [options] <type> <key> <value> +``` + +**Options:** + +- `-h, --help` - display help for command + +### aztec-wallet authorize-action + +Authorizes a public call on the caller, so they can perform an action on behalf of the provided account + +**Usage:** +```bash +aztec-wallet authorize-action [options] <functionName> <caller> +``` + +**Options:** + +- `--args [args...]` - Function arguments (default: []) +- `-ca, --contract-address <address>` - Aztec address of the contract. +- `-c, --contract-artifact <fileLocation>` - Path to a compiled Aztec contract's artifact in JSON format. If executed inside a nargo workspace, a package and contract name can be specified as package@contract +- `-f, --from <string>` - Alias or address of the account to simulate from +- `-h, --help` - display help for command + +### aztec-wallet bridge-fee-juice + +Mints L1 Fee Juice and pushes them to L2. + +**Usage:** +```bash +aztec-wallet bridge-fee-juice [options] <amount> <recipient> +``` + +**Options:** + +- `--l1-rpc-urls <string>` - List of Ethereum host URLs. Chain identifiers localhost and testnet can be used (comma separated) (default: ["http://localhost:8545"]) +- `-m, --mnemonic <string>` - The mnemonic to use for deriving the Ethereum address that will mint and bridge (default: "test test test test test test test test test test test junk") +- `--mint` - Mint the tokens on L1 (default: false) +- `--l1-private-key <string>` - The private key to the eth account bridging +- `-c, --l1-chain-id <number>` - Chain ID of the ethereum host (default: 31337, env: L1_CHAIN_ID) +- `--json` - Output the claim in JSON format +- `--no-wait` - Wait for the bridged funds to be available in L2, polling every 60 seconds +- `--interval <number>` - The polling interval in seconds for the bridged funds (default: "60") +- `-h, --help` - display help for command + +### aztec-wallet create-account + +Creates an aztec account that can be used for sending transactions. Registers the account on the PXE and deploys an account contract. Uses a Schnorr account which uses an immutable key for authentication. + +**Usage:** +```bash +aztec-wallet create-account [options] +``` + +**Options:** + +- `-f, --from <string>` - Alias or address of the account performing the deployment +- `--skip-initialization` - Skip initializing the account contract. Useful for publicly deploying an existing account. +- `--public-deploy` - Publishes the account contract instance (and the class, if needed). Needed if the contract contains public functions. +- `--register-class` - Register the contract class (useful for when the contract class has not been deployed yet). +- `-p, --public-key <string>` - Public key that identifies a private signing key stored outside of the wallet. Used for ECDSA SSH accounts over the secp256r1 curve. +- `-sk, --signing-key <string>` - Signing key for account. Uses random by default. (env: SIGNING_KEY) +- `-a, --alias <string>` - Alias for the account. Used for easy reference in subsequent commands. +- `-t, --type <string>` - Type of account to create (choices: "schnorr", "schnorr_initializerless", "ecdsasecp256r1", "ecdsasecp256r1ssh", "ecdsasecp256k1", default: "schnorr") +- `-s, --salt <hex string>` - Optional deployment salt as a hex string for generating the deployment address. Defaults to 0. +- `--register-only` - Just register the account on the Wallet. Do not deploy or initialize the account contract. +- `--json` - Emit output as json +- `--no-wait` - Skip waiting for the contract to be deployed. Print the hash of deployment transaction +- `--wait-for-status <status>` - Tx status to wait for: 'proposed' or 'checkpointed' (default: "proposed") +- `-v, --verbose` - Provide timings on all executed operations (synching, simulating, proving) (default: false) +- `--payment <options>` - Fee payment method and arguments. Parameters: method Valid values: "fee_juice", "fpc-public", "fpc-private", "fpc-sponsored" Default: fee_juice asset The asset used for fee payment. Required for "fpc-public" and "fpc-private". fpc The FPC contract that pays in fee juice. Not required for the "fee_juice" method. claim Whether to use a previously stored claim to bridge fee juice. claimSecret The secret to claim fee juice on L1. claimAmount The amount of fee juice to be claimed. messageLeafIndex The index of the claim in the l1toL2Message tree. +- `--gas-limits <da=100,l2=100,teardownDA=10,teardownL2=10>` - Gas limits for the tx. +- `--max-fees-per-gas <da=100,l2=100>` - Maximum fees per gas unit for DA and L2 computation. +- `--max-priority-fees-per-gas <da=0,l2=0>` - Maximum priority fees per gas unit for DA and L2 computation. +- `--estimate-gas-only` - Only report gas estimation for the tx, do not send it. +- `-h, --help` - display help for command + +### aztec-wallet create-authwit + +Creates an authorization witness that can be privately sent to a caller so they can perform an action on behalf of the provided account + +**Usage:** +```bash +aztec-wallet create-authwit [options] <functionName> <caller> +``` + +**Options:** + +- `--args [args...]` - Function arguments (default: []) +- `-ca, --contract-address <address>` - Aztec address of the contract. +- `-c, --contract-artifact <fileLocation>` - Path to a compiled Aztec contract's artifact in JSON format. If executed inside a nargo workspace, a package and contract name can be specified as package@contract +- `-f, --from <string>` - Alias or address of the account to simulate from +- `-a, --alias <string>` - Alias for the authorization witness. Used for easy reference in subsequent commands. +- `-h, --help` - display help for command + +### aztec-wallet create-secret + +Creates an aliased secret to use in other commands + +**Usage:** +```bash +aztec-wallet create-secret [options] +``` + +**Options:** + +- `-a, --alias <string>` - Key to alias the secret with +- `-h, --help` - display help for command + +### aztec-wallet deploy + +Deploys a compiled Aztec.nr contract to Aztec. + +**Usage:** +```bash +aztec-wallet deploy [options] [artifact] +``` + +**Options:** + +- `--init <string>` - The contract initializer function to call (default: "constructor") +- `--no-init` - Leave the contract uninitialized +- `-k, --public-key <string>` - Optional encryption public key for this address. Set this value only if this contract is expected to receive private notes, which will be encrypted using this public key. +- `-s, --salt <hex string>` - Optional deployment salt as a hex string for generating the deployment address. Defaults to random. +- `--universal` - Do not mix the sender address into the deployment. +- `--args [args...]` - Constructor arguments (default: []) +- `-f, --from <string>` - Alias or address of the account to deploy from +- `-a, --alias <string>` - Alias for the contract. Used for easy reference subsequent commands. +- `--json` - Emit output as json +- `--no-wait` - Skip waiting for the contract to be deployed. Print the hash of deployment transaction +- `--no-class-registration` - Don't register this contract class +- `--no-public-deployment` - Don't emit this contract's public bytecode +- `--timeout <number>` - The amount of time in seconds to wait for the deployment to post to L2 +- `--wait-for-status <status>` - Tx status to wait for: 'proposed' or 'checkpointed' (default: "proposed") +- `-v, --verbose` - Provide timings on all executed operations (synching, simulating, proving) (default: false) +- `--payment <options>` - Fee payment method and arguments. Parameters: method Valid values: "fee_juice", "fpc-public", "fpc-private", "fpc-sponsored" Default: fee_juice asset The asset used for fee payment. Required for "fpc-public" and "fpc-private". fpc The FPC contract that pays in fee juice. Not required for the "fee_juice" method. claim Whether to use a previously stored claim to bridge fee juice. claimSecret The secret to claim fee juice on L1. claimAmount The amount of fee juice to be claimed. messageLeafIndex The index of the claim in the l1toL2Message tree. +- `--gas-limits <da=100,l2=100,teardownDA=10,teardownL2=10>` - Gas limits for the tx. +- `--max-fees-per-gas <da=100,l2=100>` - Maximum fees per gas unit for DA and L2 computation. +- `--max-priority-fees-per-gas <da=0,l2=0>` - Maximum priority fees per gas unit for DA and L2 computation. +- `--estimate-gas-only` - Only report gas estimation for the tx, do not send it. +- `-h, --help` - display help for command + +### aztec-wallet deploy-account + +Deploys an already registered aztec account that can be used for sending transactions. + +**Usage:** +```bash +aztec-wallet deploy-account [options] <address> +``` + +**Options:** + +- `-f, --from <string>` - Alias or address of the account performing the deployment +- `--json` - Emit output as json +- `--no-wait` - Skip waiting for the contract to be deployed. Print the hash of deployment transaction +- `--register-class` - Register the contract class (useful for when the contract class has not been deployed yet). +- `--public-deploy` - Publishes the account contract instance (and the class, if needed). Needed if the contract contains public functions. +- `--skip-initialization` - Skip initializing the account contract. Useful for publicly deploying an existing account. +- `--wait-for-status <status>` - Tx status to wait for: 'proposed' or 'checkpointed' (default: "proposed") +- `-v, --verbose` - Provide timings on all executed operations (synching, simulating, proving) (default: false) +- `--payment <options>` - Fee payment method and arguments. Parameters: method Valid values: "fee_juice", "fpc-public", "fpc-private", "fpc-sponsored" Default: fee_juice asset The asset used for fee payment. Required for "fpc-public" and "fpc-private". fpc The FPC contract that pays in fee juice. Not required for the "fee_juice" method. claim Whether to use a previously stored claim to bridge fee juice. claimSecret The secret to claim fee juice on L1. claimAmount The amount of fee juice to be claimed. messageLeafIndex The index of the claim in the l1toL2Message tree. +- `--gas-limits <da=100,l2=100,teardownDA=10,teardownL2=10>` - Gas limits for the tx. +- `--max-fees-per-gas <da=100,l2=100>` - Maximum fees per gas unit for DA and L2 computation. +- `--max-priority-fees-per-gas <da=0,l2=0>` - Maximum priority fees per gas unit for DA and L2 computation. +- `--estimate-gas-only` - Only report gas estimation for the tx, do not send it. +- `-h, --help` - display help for command + +### aztec-wallet get-alias + +Shows stored aliases + +**Usage:** +```bash +aztec-wallet get-alias [options] [alias] +``` + +**Options:** + +- `-h, --help` - display help for command + +### aztec-wallet get-fee-juice-balance + +Checks the Fee Juice balance for a given address. + +**Usage:** +```bash +aztec-wallet get-fee-juice-balance [options] <address> +``` + +**Options:** + +- `--json` - Emit output as json +- `--exact` - Show exact balance with all 18 decimal places +- `-h, --help` - display help for command + +### aztec-wallet get-tx + +Gets the status of the recent txs, or a detailed view if a specific transaction hash is provided + +**Usage:** +```bash +aztec-wallet get-tx [options] [txHash] +``` + +**Options:** + +- `-p, --page <number>` - The page number to display (default: 1) +- `-s, --page-size <number>` - The number of transactions to display per page (default: 10) +- `-h, --help` - display help for command + +### aztec-wallet import-test-accounts + +Import test accounts from pxe. + +**Usage:** +```bash +aztec-wallet import-test-accounts [options] +``` + +**Options:** + +- `--json` - Emit output as json +- `-h, --help` - display help for command + +### aztec-wallet profile + +Profiles a private function by counting the unconditional operations in its execution steps + +**Usage:** +```bash +aztec-wallet profile [options] <functionName> +``` + +**Options:** + +- `--args [args...]` - Function arguments (default: []) +- `-ca, --contract-address <address>` - Aztec address of the contract. +- `-c, --contract-artifact <fileLocation>` - Path to a compiled Aztec contract's artifact in JSON format. If executed inside a nargo workspace, a package and contract name can be specified as package@contract +- `--debug-execution-steps-dir <address>` - Directory to write execution step artifacts for bb profiling/debugging. +- `-aw, --auth-witness <string,...>` - Authorization witness to use for the simulation +- `-f, --from <string>` - Alias or address of the account to simulate from +- `--payment <options>` - Fee payment method and arguments. Parameters: method Valid values: "fee_juice", "fpc-public", "fpc-private", "fpc-sponsored" Default: fee_juice asset The asset used for fee payment. Required for "fpc-public" and "fpc-private". fpc The FPC contract that pays in fee juice. Not required for the "fee_juice" method. claim Whether to use a previously stored claim to bridge fee juice. claimSecret The secret to claim fee juice on L1. claimAmount The amount of fee juice to be claimed. messageLeafIndex The index of the claim in the l1toL2Message tree. +- `--gas-limits <da=100,l2=100,teardownDA=10,teardownL2=10>` - Gas limits for the tx. +- `--max-fees-per-gas <da=100,l2=100>` - Maximum fees per gas unit for DA and L2 computation. +- `--max-priority-fees-per-gas <da=0,l2=0>` - Maximum priority fees per gas unit for DA and L2 computation. +- `--estimate-gas-only` - Only report gas estimation for the tx, do not send it. +- `-h, --help` - display help for command + +### aztec-wallet register-contract + +Registers a contract in this wallet's PXE + +**Usage:** +```bash +aztec-wallet register-contract [options] [address] [artifact] +``` + +**Options:** + +- `--init <string>` - The contract initializer function to call (default: "constructor") +- `-k, --public-key <string>` - Optional encryption public key for this address. Set this value only if this contract is expected to receive private notes, which will be encrypted using this public key. +- `-s, --salt <hex string>` - Optional deployment salt as a hex string for generating the deployment address. +- `--deployer <string>` - The address of the account that deployed the contract +- `--args [args...]` - Constructor arguments (default: []) +- `-a, --alias <string>` - Alias for the contact. Used for easy reference in subsequent commands. +- `-h, --help` - display help for command + +### aztec-wallet register-sender + +Registers a sender's address in the wallet, so the note synching process will look for notes sent by them + +**Usage:** +```bash +aztec-wallet register-sender [options] [address] +``` + +**Options:** + +- `-a, --alias <string>` - Alias for the sender. Used for easy reference in subsequent commands. +- `-h, --help` - display help for command + +### aztec-wallet send + +Calls a function on an Aztec contract. + +**Usage:** +```bash +aztec-wallet send [options] <functionName> +``` + +**Options:** + +- `--args [args...]` - Function arguments (default: []) +- `-c, --contract-artifact <fileLocation>` - Path to a compiled Aztec contract's artifact in JSON format. If executed inside a nargo workspace, a package and contract name can be specified as package@contract +- `-ca, --contract-address <address>` - Aztec address of the contract. +- `-a, --alias <string>` - Alias for the transaction hash. Used for easy reference in subsequent commands. +- `-aw, --auth-witness <string,...>` - Authorization witness to use for the transaction. If using multiple, pass a comma separated string +- `-f, --from <string>` - Alias or address of the account to send the transaction from +- `--no-wait` - Print transaction hash without waiting for it to be mined +- `--wait-for-status <status>` - Tx status to wait for: 'proposed' or 'checkpointed' (default: "proposed") +- `-v, --verbose` - Provide timings on all executed operations (synching, simulating, proving) (default: false) +- `--payment <options>` - Fee payment method and arguments. Parameters: method Valid values: "fee_juice", "fpc-public", "fpc-private", "fpc-sponsored" Default: fee_juice asset The asset used for fee payment. Required for "fpc-public" and "fpc-private". fpc The FPC contract that pays in fee juice. Not required for the "fee_juice" method. claim Whether to use a previously stored claim to bridge fee juice. claimSecret The secret to claim fee juice on L1. claimAmount The amount of fee juice to be claimed. messageLeafIndex The index of the claim in the l1toL2Message tree. +- `--gas-limits <da=100,l2=100,teardownDA=10,teardownL2=10>` - Gas limits for the tx. +- `--max-fees-per-gas <da=100,l2=100>` - Maximum fees per gas unit for DA and L2 computation. +- `--max-priority-fees-per-gas <da=0,l2=0>` - Maximum priority fees per gas unit for DA and L2 computation. +- `--estimate-gas-only` - Only report gas estimation for the tx, do not send it. +- `-h, --help` - display help for command + +### aztec-wallet simulate + +Simulates the execution of a function on an Aztec contract. + +**Usage:** +```bash +aztec-wallet simulate [options] <functionName> +``` + +**Options:** + +- `--args [args...]` - Function arguments (default: []) +- `-ca, --contract-address <address>` - Aztec address of the contract. +- `-c, --contract-artifact <fileLocation>` - Path to a compiled Aztec contract's artifact in JSON format. If executed inside a nargo workspace, a package and contract name can be specified as package@contract +- `-sk, --signing-key <string>` - The sender's signing key (env: SIGNING_KEY) +- `-aw, --auth-witness <string,...>` - Authorization witness to use for the simulation +- `-f, --from <string>` - Alias or address of the account to simulate from +- `-v, --verbose` - Provide timings on all executed operations (synching, simulating, proving) (default: false) +- `--payment <options>` - Fee payment method and arguments. Parameters: method Valid values: "fee_juice", "fpc-public", "fpc-private", "fpc-sponsored" Default: fee_juice asset The asset used for fee payment. Required for "fpc-public" and "fpc-private". fpc The FPC contract that pays in fee juice. Not required for the "fee_juice" method. claim Whether to use a previously stored claim to bridge fee juice. claimSecret The secret to claim fee juice on L1. claimAmount The amount of fee juice to be claimed. messageLeafIndex The index of the claim in the l1toL2Message tree. +- `--gas-limits <da=100,l2=100,teardownDA=10,teardownL2=10>` - Gas limits for the tx. +- `--max-fees-per-gas <da=100,l2=100>` - Maximum fees per gas unit for DA and L2 computation. +- `--max-priority-fees-per-gas <da=0,l2=0>` - Maximum priority fees per gas unit for DA and L2 computation. +- `--estimate-gas-only` - Only report gas estimation for the tx, do not send it. +- `-h, --help` - display help for command diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/_category_.json b/docs/lines/v6/docs-developers/docs/foundational-topics/_category_.json new file mode 100644 index 00000000000..8ab61c7b48a --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Foundational Topics", + "position": 0, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/accounts/_category_.json b/docs/lines/v6/docs-developers/docs/foundational-topics/accounts/_category_.json new file mode 100644 index 00000000000..1faf532854b --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/accounts/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Accounts", + "position": 0, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/accounts/index.md b/docs/lines/v6/docs-developers/docs/foundational-topics/accounts/index.md new file mode 100644 index 00000000000..43350ac322d --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/accounts/index.md @@ -0,0 +1,275 @@ +--- +title: Accounts +tags: [accounts] +description: Deep dive into Aztec's native account abstraction system - understanding how smart contract accounts work, their architecture, key management, and authorization mechanisms in a privacy-preserving blockchain. +references: ["labs/noir-projects/aztec-nr/aztec/src/authwit/*", "labs/noir-projects/noir-contracts/contracts/account/*", "labs/yarn-project/accounts/src/*", "labs/yarn-project/aztec.js/src/account/*", "labs/yarn-project/entrypoints/src/*"] +--- + +# Understanding Accounts in Aztec + +This page provides a comprehensive understanding of how accounts work in Aztec. We'll explore the architecture, implementation details, and the powerful features enabled by Aztec's native account abstraction. + +## What is Account Abstraction? + +Account abstraction fundamentally changes how we think about blockchain accounts. Instead of accounts being simple key pairs (like in Bitcoin or traditional Ethereum EOAs), accounts become programmable smart contracts that can define their own rules for authentication, authorization, and transaction execution. + +### Why Account Abstraction Matters + +Traditional blockchain accounts have significant limitations: + +- **Rigid authentication**: You lose your private key, you lose everything +- **Limited authorization**: Can't easily implement multi-signature schemes or time-locked transactions +- **Fixed fee payment**: Must pay fees in the native token from the same account +- **No customization**: Can't adapt to different security requirements or use cases + +Account abstraction solves these problems by making accounts programmable. This enables: + +- **Recovery mechanisms**: Social recovery, hardware wallet backups, time-delayed recovery +- **Flexible authentication**: Biometrics, passkeys, multi-factor authentication, custom signature schemes +- **Fee abstraction**: Pay fees in any token, or have someone else pay for you +- **Custom authorization**: Complex permission systems, spending limits, automated transactions + +## Aztec's Native Account Abstraction + +Unlike Ethereum where account abstraction is implemented at the application layer (ex. ERC-4337), Aztec has **native account abstraction** at the protocol level. This means: + +1. **Every account is a smart contract** - There are no externally owned accounts (EOAs) +2. **Unified experience** - All accounts have the same capabilities and flexibility +3. **Protocol-level support** - The entire network is designed around smart contract accounts +4. **Private by design** - Account abstraction works seamlessly with Aztec's privacy features + +### Breaking the DoS Attack Problem + +One of the biggest challenges in account abstraction is preventing denial-of-service (DoS) attacks. If accounts can have arbitrary validation logic, malicious actors could flood the network with transactions that are expensive to validate but ultimately invalid. + +Other account abstraction systems (like ERC-4337) solve this by restricting what validation logic can do - limiting opcodes, storage access, and gas. This works but limits flexibility. + +Aztec takes a different approach: validation happens client-side with ZK proofs, so the sequencer only verifies a constant-size proof regardless of validation complexity: + +```mermaid +graph LR + A[Transaction] --> B[Client validates] + B --> C[Generate ZK proof] + C --> D[Sequencer verifies proof] +``` + +With this approach: + +- **Client performs validation**: All complex logic runs on the user's device +- **Proof generation**: The client generates a succinct ZK proof that validation succeeded +- **Constant verification cost**: The sequencer only verifies the proof - a constant-time operation regardless of validation complexity + +This means we can have: + +- **Unlimited validation complexity** without affecting network performance +- **Free complex operations** like verifying 100 signatures or checking complex conditions +- **Better privacy** as validation logic isn't visible onchain + +## How Aztec Accounts Work + +### Account Architecture + +Every Aztec account is a smart contract with a specific structure. At its core, an account contract must: + +1. **Authenticate transactions** - Verify that the transaction is authorized by the account owner +2. **Execute calls** - Perform the requested operations (transfers, contract calls, etc.) +3. **Manage keys** - Handle the various keys used for privacy and authentication +4. **Handle fees** - Determine how transaction fees are paid + +### The Account Contract Structure + +Here's the essential structure of an Aztec account contract: + +```mermaid +graph TD + A[Transaction Request] --> B[Entrypoint Function] + B --> C{Authenticate} + C -->|Valid| D[Set Fee Payer] + C -->|Invalid| E[Reject Transaction] + D --> F[Execute App Calls] + F --> G{Cancellable?} + G -->|Yes| H[Emit Cancellation Nullifier] + G -->|No| I[Complete] + H --> I + + style C fill:#f9f,stroke:#333 + style E fill:#f66,stroke:#333 + style I fill:#6f6,stroke:#333 +``` + +The entrypoint function follows this pattern: + +1. **Authentication** - Verify the transaction is authorized (signatures, multisig, etc.) +2. **Fee Payer Setup** - Set the account as fee payer if using its own balance +3. **Application Execution** - Execute the requested function calls +4. **Cancellation Handling** - Optionally emit a nullifier for transaction cancellation + +### Address Derivation + +Aztec addresses are **deterministic** - they can be computed before deployment. An address is derived from: + +```text +Address = hash( + public_keys_hash, // All the account's public keys + partial_address // Contract deployment information +) +``` + +Where: + +- **public_keys_hash** = Combined hash of nullifier, incoming viewing, and other keys +- **partial_address** = Hash of the contract code and deployment parameters + +This deterministic addressing enables powerful features: + +- **Pre-funding**: Send funds to an address before the account is deployed +- **Counterfactual deployment**: Interact with an account as if it exists, deploy it later +- **Address recovery**: Recompute addresses from known keys + +#### Complete Address + +While an address alone is sufficient for receiving funds, spending notes requires a **complete address** which includes: + +- All the user's public keys (nullifier, incoming viewing, etc.) +- The partial address (contract deployment information) +- The contract address itself + +The complete address proves that the nullifier key inside the address is correct, enabling the user to spend their notes. + +## The Entrypoint Pattern + +The entrypoint is the gateway to your account. When someone wants to execute a transaction from your account, they call the entrypoint with a payload describing what to do. + +### Transaction Flow + +Here's how a transaction flows through an account: + +```mermaid +sequenceDiagram + participant User + participant Wallet + participant Account Contract + participant Target Contracts + + User->>Wallet: Request transaction (transfer, swap, etc.) + Wallet->>Wallet: Create payload & sign + Wallet->>Account Contract: Call entrypoint(payload, signature) + Account Contract->>Account Contract: Verify signature + Account Contract->>Target Contracts: Execute requested calls + Account Contract->>Account Contract: Pay fees + Account Contract->>Account Contract: Emit cancellation nullifier (if cancellable) + Account Contract-->>User: Transaction complete +``` + +### Non-Standard Entrypoints + +The beauty of account abstraction is that not every contract needs authentication. Some contracts can have **permissionless entrypoints**. + +For example, a lottery contract where anyone can trigger the payout: + +- No authentication required +- Anyone can call the function +- The contract itself handles the logic and constraints + +This pattern is useful for: + +- **Automated operations**: Keepers can trigger time-based actions +- **Public goods**: Anyone can advance the state of a protocol +- **Gasless transactions**: Users don't need to hold fee tokens + +## Account Lifecycle + +### 1. Pre-deployment (Counterfactual State) + +Before deployment, an account exists in a **counterfactual state**: + +- The address can be computed deterministically +- Can receive funds (notes can be encrypted to the address) +- Cannot send transactions (no code deployed) + +### 2. Deployment + +Deploying an account involves: + +1. Submitting the account contract code +2. Registering in the contract instance registry +3. Paying deployment fees (either self-funded or sponsored) + +See [Creating Accounts](../../aztec-js/how_to_create_account.md) for code examples. + +### 3. Initialization + +Accounts can be initialized for different purposes: + +- **Private-only**: Just needs initialization, no public deployment +- **Public interaction**: Requires both initialization and deployment + +The contract is initialized when one of the functions marked with the `#[initializer]` annotation has been invoked. Multiple functions in the contract can be marked as initializers. Contracts may have functions that skip the initialization check (marked with `#[noinitcheck]`). + +:::note +Account deployment and initialization are not required to receive notes. The user address is deterministically derived, so funds can be sent to an account that hasn't been deployed yet. +::: + +### 4. Active Use + +Once deployed and initialized, accounts can: + +- Send and receive private notes +- Interact with public and private functions +- Authorize actions via authentication witnesses +- Pay fees in various ways + +## Authentication Witnesses (AuthWit) + +Aztec replaces Ethereum's dangerous "infinite approval" pattern with **Authentication Witnesses** - a more secure authorization scheme where users sign specific actions rather than granting blanket permissions. + +Instead of approving unlimited token transfers, users authorize exact actions with precise parameters. This eliminates persistent security risks while enabling better UX through batched operations. + +For detailed information about how AuthWit works in both private and public contexts, see the [Authentication Witness documentation](../advanced/authwit.md). + +## Transaction Abstractions + +Aztec abstracts two critical components of transactions that are typically rigid in other blockchains: nonces and fees. + +### Nonce Abstraction + +Unlike Ethereum where nonces are sequential counters enforced by the protocol, Aztec lets account contracts implement their own replay protection. + +**Different nonce strategies possible:** + +| Strategy | How it Works | Benefits | +|----------|--------------|----------| +| **Sequential** (like Ethereum) | Must use nonces in order (1, 2, 3...) | Simple, predictable ordering | +| **Unordered** (like Bitcoin) | Any unused nonce is valid | Parallel transactions, no blocking | +| **Time-windowed** | Nonces valid only in specific time periods | Automatic expiration, batching | +| **Merkle-tree based** | Nonces from a pre-committed set | Privacy, batch pre-authorization | + +This enables: + +- **Parallel transactions**: No need to wait for one tx to complete before sending another +- **Custom cancellation**: Define your own rules for replacing/cancelling transactions +- **Flexible ordering**: Implement priority queues, batching, or time-based ordering + +### Fee Abstraction + +Unlike traditional blockchains where users must pay fees in the native token, Aztec accounts can implement custom fee payment logic: + +- **Pay with any token** through integrated swaps +- **Sponsored transactions** where applications pay for users +- **Meta-transactions** with relayer networks +- **Custom payment models** like subscriptions or paymasters + +This flexibility is crucial for user onboarding and enables gasless experiences. For detailed information about fee mechanics and payment options, see the [Fees documentation](../fees.md). + +## Account Contracts + +Aztec provides several account contract implementations: + +- **Schnorr Account** - Single-key account using Schnorr signatures (default) +- **ECDSA Account** - Single-key account using ECDSA signatures (secp256k1 or secp256r1) + +These implement the simple signature pattern where a single key controls the account. The flexibility of account abstraction also enables more complex patterns like multisig, social recovery, or session keys - these can be implemented as custom account contracts. + +## Summary + +Aztec's native account abstraction means every account is a smart contract with customizable authentication, fee payment, and authorization logic. Because validation happens client-side with ZK proofs, complex validation doesn't increase network costs - enabling patterns that aren't practical on other blockchains. diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/accounts/keys.md b/docs/lines/v6/docs-developers/docs/foundational-topics/accounts/keys.md new file mode 100644 index 00000000000..4563b760bd7 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/accounts/keys.md @@ -0,0 +1,230 @@ +--- +title: Keys +tags: [accounts, keys] +description: Understand the specialized key pairs used in Aztec accounts - nullifier keys, incoming viewing keys, and signing keys - and how they enable privacy, security, and flexible authentication. +references: ["labs/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr"] +--- + +## Account Keys in Aztec + +Unlike traditional blockchains where accounts use a single key pair, Aztec accounts use **multiple specialized key pairs**, each serving a distinct cryptographic purpose. This separation is fundamental to Aztec's privacy model and enables powerful security features that aren't possible with single-key systems. + +## Why Multiple Keys? + +The separation of keys in Aztec serves critical purposes: + +- **Privacy isolation**: Different keys for different operations prevent correlation attacks +- **Selective disclosure**: Share viewing access without compromising spending ability +- **Damage limitation**: If one key is compromised, others remain secure +- **Flexible authorization**: Choose any authentication method without affecting core protocol keys +- **Per-application security**: Keys can be scoped to specific contracts to minimize exposure + +This multi-key architecture is what enables Aztec to provide strong privacy guarantees while maintaining flexibility and security. + +## Key Types + +Each Aztec account uses multiple key pairs: + +| Key Type | Purpose | Protocol Managed | Rotatable | +|----------|---------|-----------------|-----------| +| **Nullifier Keys** (`Npk_m`) | Spending notes (destroying private state) | Yes | No | +| **Incoming Viewing Keys** (`Ivpk_m`) | Decrypting received notes | Yes | No | +| **Outgoing Viewing Keys** (`Ovpk_m`) | Reserved — not currently used | Yes | No | +| **Tagging Keys** (`Tpk_m`) | Reserved — not currently used | Yes | No | +| **Message-Signing Keys** (`Mspk_m`) | Reserved — slot for future protocol-level message signing | Yes | No | +| **Fallback Keys** (`Fbpk_m`) | Reserved — slot for future account-recovery / fallback flows | Yes | No | +| **Signing Keys** | Transaction authorization | No (app-defined) | Yes | + +Protocol keys are embedded into the protocol and cannot be changed once an account is created. The signing key is abstracted to the account contract developer, allowing complete flexibility in authentication methods. + +### Nullifier Keys + +**Purpose**: Spending notes (private state consumption) + +Nullifier keys enable spending private notes. When using a note (like spending a token), the spender must prove they have the right to nullify it - essentially marking it as "spent" without revealing which note is being spent. + +**How it works:** + +1. Each account has a master nullifier key pair (`Npk_m`, `nhk_m`) +2. For each application, an **app-siloed** key is derived: `nhk_app = hash(nhk_m, app_contract_address)` +3. To spend a note, compute its nullifier using the note hash and app-siloed key +4. The protocol verifies the app-siloed key comes from your master key and that your master public key is in your address + +This ensures only the rightful owner can spend notes, while the app-siloing provides additional security isolation between contracts. + +:::tip + +This last point could be confusing for most developers: how could a protocol verify a secret key is derived from another secret key without knowing it? Well, _you_ make that derivation, generating a ZK proof for it. The protocol just verifies that ZK proof! + +::: + +#### Accessing nullifier keys in code + +The nullifier hiding key (`nhk`) — sometimes referred to in older documentation as the "nullifier secret key" (`nsk`) — is the secret scalar used to compute nullifiers. You should **never** derive or construct this key manually. Use the framework-provided functions: + +| Context | Function | Import / Access | +|---------|----------|----------------| +| Private (constrained) | `context.request_nhk_app(owner_npk_m_hash)` | Called on `&mut PrivateContext` | +| Unconstrained | `get_nhk_app(owner_npk_m_hash)` | `use aztec::keys::getters::get_nhk_app` | +| TypeScript (master key) | `deriveMasterNullifierHidingSecretKey(secretKey)` | `import { deriveMasterNullifierHidingSecretKey } from '@aztec-labs/aztec.js/keys'` | +| TypeScript (app-siloed) | `computeAppNullifierHidingKey(nhkM, app)` | `import { computeAppNullifierHidingKey } from '@aztec-labs/aztec.js/keys'` | + +To get the owner's master nullifier public key hash (needed as input): + +```rust +let owner_npk_m_hash = get_public_keys(owner).npk_m_hash; +``` + +`PublicKeys` exposes the nullifier, outgoing-viewing, tagging, message-signing, and fallback keys as their hashes; only `ivpk_m` is held as a Grumpkin point (it is required as a point for address derivation and encrypt-to-address). + +:::warning +Do not compute nullifier keys by hand or derive custom blinding factors. The protocol kernel validates that `nhk_app` derives correctly from the master key — a hand-rolled value will fail verification. +::: + +### Incoming Viewing Keys + +**Purpose**: Receiving and decrypting private notes + +Incoming viewing keys enable private information to be shared with recipients. The sender uses the recipient's public viewing key (`Ivpk`) to encrypt notes, and the recipient uses their secret viewing key (`ivsk`) to decrypt them. + +**The encryption flow:** + +```mermaid +graph TB + A[Sender generates<br/>ephemeral key esk] --> B[Computes shared secret<br/>S = esk × RecipientAddress] + B --> C[Derives encryption key<br/>key = hash S] + C --> D[Encrypts note<br/>ciphertext = AES note, key] + D --> E[Sends: Epk, ciphertext] + + E -.->|transmitted| F[Recipient computes<br/>S = ivsk × Epk] + F --> G[Derives same key<br/>key = hash S] + G --> H[Decrypts note] +``` + +This uses elliptic curve Diffie-Hellman: both parties compute the same shared secret `S`, but only the recipient has the private key needed to decrypt. + +### Signing Keys + +**Purpose**: Transaction authorization (optional, application-defined) + +Unlike nullifier and incoming viewing keys which are protocol-mandated, signing keys are **completely abstracted** - thanks to [native account abstraction](./index.md), any authorization method can be implemented: + +- **Signature-based**: ECDSA, Schnorr, BLS, multi-signature +- **Biometric**: Face ID, fingerprint +- **Web2 credentials**: Google OAuth, passkeys +- **Custom logic**: Time locks, spending limits, multi-party authorization + +**Traditional signature approach:** + +When using signatures, the account contract validates the signature against a stored public key. Here's an example from the Schnorr account contract: + +#include_code is_valid_impl labs/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr rust + +The flexibility of signing key storage and rotation is entirely up to your account contract implementation. + +## Address Derivation + +Your Aztec address is deterministically computed from your public keys and account contract. This enables anyone to encrypt notes to your address without needing additional information. + +![Address derivation](/img/address_derivation.svg) + +:::note +The diagram above is being updated to reflect the new key-hashing scheme described below. Treat the text formulas as authoritative until a refresh lands. +::: + +```text +pre_address = hash(public_keys_hash, partial_address) + +where: + public_keys_hash = hash(npk_m_hash, ivpk_m_hash, ovpk_m_hash, tpk_m_hash, + mspk_m_hash, fbpk_m_hash) + ivpk_m_hash = hash(Ivpk_m.x, Ivpk_m.y) // computed in-circuit + npk_m_hash, ovpk_m_hash, tpk_m_hash, // computed off-circuit (PXE) + mspk_m_hash, fbpk_m_hash // computed off-circuit (PXE) + partial_address = hash(contract_class_id, salted_initialization_hash) + contract_class_id = hash(artifact_hash, fn_tree_root, public_bytecode_commitment) + salted_initialization_hash = hash(salt, constructor_hash, deployer_address, immutables_hash) +``` + +The final address is derived as `address = (pre_address * G + Ivpk_m).x` - only the x-coordinate of the resulting elliptic curve point. Note that `Ivpk_m` (the master incoming viewing key) is the only public key still represented as an elliptic curve point: address derivation needs it as a point so that anyone can encrypt to the address without further information. The other five master keys are exposed only as their hashes. + +This derivation ensures: + +- Your address is deterministic (can be computed before deployment) +- Keys and contract code are cryptographically bound to the address +- The address proves ownership of the nullifier key needed to spend notes + +:::note +The `Ovpk` (outgoing viewing key) and `Tpk` (tagging key) exist in the protocol's `PublicKeys` struct but are not currently used. They're reserved for future protocol upgrades. +::: + +:::note +The `Mspk` (master message-signing key) and `Fbpk` (master fallback key) are reserved for future protocol features (message signing and account recovery, respectively). Their public keys are derived per account and participate in the address derivation above. Unlike the other privacy keys, their **secret keys are held by the wallet, not the PXE**: the PXE receives only the corresponding public keys, since it is not trusted to hold these secrets. +::: + +## Key Management + +### Key Generation and Derivation + +Protocol keys (nullifier and incoming viewing) are automatically generated by the [Private Execution Environment (PXE)](../pxe/index.md) when creating an account. The PXE handles: + +- Initial key pair generation +- App-siloed key derivation +- Secure key storage and oracle access +- Key material never leaves the client + +All keys use elliptic curve cryptography on the Grumpkin curve: + +- Secret keys are scalars +- Public keys are elliptic curve points (secret × generator point) + +Signing keys are application-defined and managed by your account contract logic. + +### App-Siloed Keys + +Nullifier keys are **app-siloed** - scoped to each contract that uses them. This provides crucial security isolation: + +**How it works:** + +```text +nhk_app = hash(nhk_m, app_contract_address) +``` + +**Security benefits:** + +1. **Damage containment**: If a nullifier key for one app leaks, other apps remain secure +2. **Privacy preservation**: Activity in different apps cannot be correlated via nullifier keys + +### Key Rotation + +**Protocol keys (nullifier, incoming viewing):** Cannot be rotated. They are embedded in the address, which is immutable. If compromised, a new account must be deployed. + +**Signing keys:** Fully rotatable, depending on the account contract implementation. Options include: + +- Change keys on a schedule +- Rotate after suspicious activity +- Implement time-delayed rotation for security + +## Summary + +Aztec's multi-key architecture is fundamental to its privacy and security model: + +| Key Type | Purpose | App-Siloed | Rotatable | Managed By | +|----------|---------|------------|-----------|------------| +| **Nullifier** (`Npk_m`) | Spend notes | Yes | No | Protocol (PXE) | +| **Incoming Viewing** (`Ivpk_m`) | Decrypt received notes | No | No | Protocol (PXE) | +| **Outgoing Viewing** (`Ovpk_m`) | Reserved | N/A | No | Protocol (PXE) | +| **Tagging** (`Tpk_m`) | Reserved | N/A | No | Protocol (PXE) | +| **Message-Signing** (`Mspk_m`) | Reserved | N/A | No | Application (Wallet) | +| **Fallback** (`Fbpk_m`) | Reserved | N/A | No | Application (Wallet) | +| **Signing** | Transaction authorization | N/A | Yes | Account Contract (Wallet) | + +**Key takeaways:** + +- **Separation enables privacy**: Different keys for different operations prevent correlation and limit damage from compromise +- **App-siloing adds security**: Per-contract nullifier keys isolate risk +- **Flexibility in authorization**: Signing keys are completely abstracted - use any authentication method +- **Protocol keys are permanent**: Nullifier and viewing keys are embedded in your address and cannot be changed +- **Client-side security**: All key material is generated and managed in the PXE, never exposed to the network + +This architecture allows Aztec to provide strong privacy guarantees while maintaining the flexibility needed for various security models and use cases. diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/_category_.json b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/_category_.json new file mode 100644 index 00000000000..a7eeb8fdaac --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Advanced Topics", + "position": 9, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/authwit.md b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/authwit.md new file mode 100644 index 00000000000..99b56063cf8 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/authwit.md @@ -0,0 +1,200 @@ +--- +title: Authentication Witness (Authwit) +tags: [accounts, authwit] +sidebar_position: 2 +keywords: [authwit, authentication witness, accounts] +description: Learn about Aztec's Authentication Witness scheme that enables secure third-party actions on behalf of users, providing a privacy-preserving alternative to traditional token approvals. +references: ["labs/noir-projects/aztec-nr/aztec/src/authwit/*", "labs/noir-projects/aztec-nr/aztec/src/macros/authorization.nr", "labs/noir-projects/noir-contracts/contracts/standard/auth_registry_contract/*", "labs/yarn-project/aztec.js/src/authorization/*"] +--- + +Authentication Witness is a scheme for authenticating actions on Aztec, allowing users to authorize third-parties (protocols or other users) to execute actions on their behalf. + +import YouTubeEmbed from '@site/src/components/YouTubeEmbed'; + +For a video walkthrough of how authwits work, including both the private and public flows, watch this explainer (find more on the [video lessons](../../resources/video_lessons.mdx) page): + +<YouTubeEmbed videoId="VRZVOCdjGZ4" title="How Authorization Works on Aztec" /> + +## Summary + +- **Authwits authorize specific actions**, not blanket allowances like ERC20 approvals +- **Two-level hash structure**: inner hash (caller, selector, args) wrapped in message hash (consumer, chain_id, version, inner_hash) +- **Private authwits** are verified via static calls to the account contract, with witnesses provided through oracles +- **Public authwits** use a shared registry where authorizations are stored and consumed +- **Single-use enforcement** through nullifiers prevents replay attacks +- **Implementation**: Use the `#[authorize_once]` macro in your contracts (see [implementation guide](../../aztec-nr/framework-description/authentication_witnesses.md)) + +## Background + +In traditional EVM contracts, users authorize third-party actions through `approve` (setting allowances) or `permit` (signed approvals). Both approaches have drawbacks: infinite approvals create security risks, two-transaction flows hurt UX, and smart contract wallets struggle with signature-based permits. + +Aztec's private state model makes traditional approvals even more problematic. Even if you approve an allowance, the recipient can't spend your private tokens without knowing the note secrets. See [Hybrid State model](../state_management.md) and [keys](../accounts/keys.md) for more on private state. + +Authwits solve this by authorizing specific actions rather than blanket allowances, with verification happening through account contracts. + +## How authwits work + +Since private execution happens on the user's device, we can use oracles to provide authorization data mid-execution. The user provides a "witness" (proof of authorization) that the account contract validates. + +:::info Witness vs signature +We use "witness" instead of "signature" because authorization doesn't require a cryptographic signature. Depending on the account contract implementation, it could be a password or other mechanism. +::: + +### Hash structure + +Authwits use a two-level hash structure: + +**Inner hash** encodes the specific action being authorized: + +```rust +inner_hash = H(caller, selector, args_hash) +``` + +**Message hash** wraps the inner hash with context to prevent cross-chain replay attacks: + +```rust +message_hash = H(consumer, chain_id, version, inner_hash) +``` + +Where + +- `caller` is the address attempting the action (e.g., a DeFi contract) +- `selector` is the function selector being called +- `args_hash` is the hash of the function arguments +- `consumer` is the contract verifying the authorization (e.g., the token contract) +- `chain_id` and `version` prevent cross-chain replay attacks + +**Example:** Authorizing a DeFi contract to transfer tokens: + +```rust +inner_hash = H(defi, transfer_selector, H(alice_account, defi, 1000)); +message_hash = H(token, chain_id, version, inner_hash); +``` + +This reads as "defi is allowed to call the token's transfer function with arguments (alice_account, defi, 1000) on this specific chain". + +### Private authwit flow + +In private execution, the Token contract asks Alice's account contract to verify the authwit. The account contract requests the witness from Alice via an oracle, validates it, and returns the result. + +```mermaid +sequenceDiagram + participant Alice + participant Account as Alice's Account + participant Token + participant DeFi + + Alice->>Account: deposit(Token, 1000) + Account->>DeFi: deposit(Token, 1000) + DeFi->>Token: transfer(Alice, DeFi, 1000) + Token->>Account: verify authwit (static call) + Account->>Alice: request witness (oracle) + Alice-->>Account: provide witness + Account-->>Token: IS_VALID + Note over Token: emit nullifier + Token-->>DeFi: transfer complete + DeFi-->>Account: deposit complete +``` + +:::info Static calls for security +The authwit verification uses a static call to the account contract. This prevents the account from re-entering the flow and modifying state during verification. +::: + +### Public authwit flow + +In public execution, oracles aren't available since the sequencer runs the code. Instead, authorizations are stored in a shared registry before use. + +```mermaid +sequenceDiagram + participant Alice + participant Account as Alice's Account + participant Registry as Auth Registry + participant Token + participant DeFi + + Alice->>Account: set_authorized(message_hash, true) + Account->>Registry: set_authorized(message_hash, true) + Note over Registry: store authorization + + Alice->>Account: deposit(Token, 1000) + Account->>DeFi: deposit(Token, 1000) + DeFi->>Token: transfer(Alice, DeFi, 1000) + Token->>Registry: consume(Alice, inner_hash) + Note over Registry: verify & set to false + Registry-->>Token: IS_VALID + Token-->>DeFi: transfer complete +``` + +The registry approach has a gas optimization: if authorization is set and consumed in the same transaction, the state changes cancel out, saving gas. + +:::tip Why use Auth Registry for signatures in public? +ECDSA signature verification is not directly available in public functions due to AVM limitations. The public authwit flow above is the recommended pattern: verify signatures in private, store approvals in the Auth Registry, and consume them in public. See [AVM Cryptographic Compatibility](./circuits/avm_compatibility.md) for more details. +::: + +### Replay prevention + +Each authwit can only be used once. The consuming contract emits a nullifier for the action, preventing reuse. This is similar to how notes work. + +To allow the same action multiple times (e.g., repeated transfers of the same amount), include a nonce in the arguments: + +```rust +inner_hash = H(defi, transfer_selector, H(alice_account, defi, 1000, nonce)); +``` + +The account contract cannot emit the nullifier (it's called via static call), so the consuming contract handles this. The authwit library manages this automatically. + +### Cancelling authwits + +You can cancel an authwit before it's used by emitting its nullifier directly. This invalidates the authwit without executing the authorized action: + +```rust +fn cancel_authwit(inner_hash: Field) { + let on_behalf_of = self.msg_sender(); + let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); + self.context.push_nullifier_unsafe(nullifier); +} +``` + +## Differences from ERC20 approvals + +| Aspect | ERC20 Approve | Authwit | +|--------|---------------|---------| +| Scope | Blanket allowance | Specific action | +| User awareness | Often unclear amounts | Exact action visible | +| Revocation | Requires transaction | Can cancel with nullifier | +| Private state | Cannot work (need note secrets) | Works via account contract | + +:::note Private authwits and note secrets +While authwits authorize a contract to perform an action, spending private notes still requires knowledge of the note secrets. For private tokens, the note owner must be involved in the transaction—they cannot simply give another user an authwit and have that user spend the notes independently. +::: + +## Use cases + +Authwits work for any function requiring third-party authorization: + +- Token transfers and burns +- DeFi deposits and withdrawals +- Governance voting +- Bridge operations (public to private transfers) +- Any contract interaction requiring user approval + +## Implementation + +Use the `#[authorize_once]` macro to add authwit verification to your contract functions: + +```rust +#[authorize_once("from", "authwit_nonce")] +#[external("private")] +fn transfer_in_private( + from: AztecAddress, + to: AztecAddress, + amount: u128, + authwit_nonce: Field, +) { + // Transfer logic here +} +``` + +The macro handles authwit verification and nullifier emission automatically. + +For complete implementation details, see the [developer documentation](../../aztec-nr/framework-description/authentication_witnesses.md). diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/_category_.json b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/_category_.json new file mode 100644 index 00000000000..611f5ed6919 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Circuits", + "position": 3, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/avm_compatibility.md b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/avm_compatibility.md new file mode 100644 index 00000000000..16e98175965 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/avm_compatibility.md @@ -0,0 +1,69 @@ +--- +title: AVM Cryptographic Compatibility +sidebar_position: 3 +description: Which Noir cryptographic primitives work in public (AVM) functions vs private, and workarounds for unsupported operations. +tags: [protocol, circuits] +references: ["labs/yarn-project/simulator/docs/avm/avm-isa-quick-reference.md"] +--- + +Private and public functions in Aztec use different execution models. Private functions compile to ACIR circuits and have access to the full Noir standard library. Public functions compile to AVM bytecode via the transpiler, which supports only a specific set of cryptographic operations. + +## Compatibility Table + +The table below lists the low-level blackbox operations and whether they are available in the AVM. Higher-level Noir standard library functions (like `sha256::sha256_var`, `keccak256::keccak256`, `poseidon2::hash`, and `std::hash::pedersen_hash`) are built on these primitives and work in public functions when the underlying operations are supported. + +| Noir Primitive | Private (ACIR) | Public (AVM) | Notes | +|---|---|---|---| +| Poseidon2 Permutation | Supported | Supported | `POSEIDON2PERM` opcode | +| Pedersen Hash / Commitment | Supported | Supported | Lowered to `ECADD` and `MSM` operations | +| SHA-256 Compression | Supported | Supported | `SHA256COMPRESSION` opcode | +| Keccak f1600 | Supported | Supported | `KECCAKF1600` opcode | +| Embedded Curve Add | Supported | Supported | `ECADD` opcode (Grumpkin curve) | +| Multi-Scalar Multiplication | Supported | Supported | Lowered to `TORADIXBE` + `ECADD` operations | +| ToRadix | Supported | Supported | `TORADIXBE` opcode | +| ECDSA secp256k1 | Supported | **Not supported** | Transpiler panics | +| ECDSA secp256r1 | Supported | **Not supported** | Transpiler panics | +| AES-128 Encrypt | Supported | **Not supported** | Transpiler panics | +| Blake2s | Supported | **Not supported** | Transpiler panics | +| Blake3 | Supported | **Not supported** | Transpiler panics | + +## Why the Difference + +Private functions are compiled to ACIR (Abstract Circuit Intermediate Representation), which supports the full set of Noir standard library blackbox functions. These are evaluated as part of the zk-SNARK proof generation on the user's device. + +Public functions are compiled to AVM bytecode via the transpiler. The AVM has a fixed instruction set, and each supported cryptographic operation must either have a dedicated opcode or be reducible to a sequence of supported opcodes. For example, multi-scalar multiplication has no dedicated opcode but is lowered to `TORADIXBE` and `ECADD` instructions. Operations that cannot be mapped to supported opcodes cannot be transpiled. + +## What Error Will I See? + +If you use an unsupported blackbox function in a `#[external("public")]` function, the transpiler will panic at compile time with a message like: + +``` +Transpiler doesn't know how to process EcdsaSecp256k1 +``` + +where the final token is the name of the unsupported `BlackBoxOp` variant (e.g. `AES128Encrypt`, `Blake2s`, `Blake3`). + +## Signature Verification in Public: Workarounds + +Since ECDSA signature verification is not available in public functions, use the **Authentication Registry** pattern: + +1. Verify signatures in a **private** function (where all Noir primitives are available) +2. Store approval hashes in the **Auth Registry** (a shared public contract) +3. Consume the approvals in **public** functions + +This is exactly how public authwits work. See [Authentication Witnesses](../authwit.md) for the full pattern. + +:::tip Schnorr signatures +The [`noir-lang/schnorr`](https://github.com/noir-lang/schnorr) library implements Schnorr verification in pure Noir using embedded curve operations (ECADD, MSM), which are supported in the AVM. This means Schnorr verification may work in public functions. However, the standard Aztec account contracts only use Schnorr in private functions, and the recommended pattern remains verifying signatures in private via the Auth Registry. +::: + +## ISA Reference + +For the complete list of AVM opcodes, see the [AVM ISA Quick Reference](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/yarn-project/simulator/docs/avm/avm-isa-quick-reference.md). + +## Related Pages + +- [Public Execution (AVM)](./public_execution.md) – How the AVM executes public functions +- [Authentication Witnesses](../authwit.md) – The Auth Registry pattern for public authorization +- [Call Types](../../../foundational-topics/call_types.md) – How private and public functions interact +- [Private Kernel](./private_kernel.md) – How private functions are processed diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/index.md b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/index.md new file mode 100644 index 00000000000..13e456cc1f2 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/index.md @@ -0,0 +1,67 @@ +--- +title: Circuits +sidebar_position: 0 +tags: [protocol, circuits] +description: Explore Aztec's core protocol circuits that enforce privacy rules and transaction validity through zero-knowledge proofs, enabling private state and function execution. +--- + +Central to Aztec's operations are 'circuits' derived both from the core protocol and the developer-written Aztec.nr contracts. + +The core circuits enhance privacy by adding additional security checks and preserving transaction details - a characteristic Ethereum lacks. + +On this page, you’ll learn a bit more about these circuits and their integral role in promoting secure and efficient transactions within Aztec's privacy-preserving design. + +## Motivation + +In Aztec, circuits come from two sources: + +1. Core protocol circuits +2. User-written circuits (written as Aztec.nr Contracts and deployed to the network) + +This page focuses on the core protocol circuits. These circuits check that the rules of the protocol are being adhered to. + +When a function in an Ethereum smart contract is executed, the EVM performs checks to ensure that Ethereum's transaction rules are being adhered-to correctly. Stuff like: + +- "Does this tx have a valid signature?" +- "Does this contract address contain deployed code?" +- "Does this function exist in the requested contract?" +- "Is this function allowed to call this function?" +- "How much gas has been paid, and how much is left?" +- "Is this contract allowed to read/update this state variable?" +- "Perform the state read / state write" +- "Execute these opcodes" + +All of these checks have a computational cost, for which users are charged gas. + +Many existing L2s move this logic offchain, as a way of saving their users gas costs, and as a way of increasing tx throughput. + +zk-Rollups, in particular, move these checks offchain by encoding them in zk-S(N/T)ARK circuits. Rather than paying a committee of Ethereum validators to perform the above kinds of checks, L2 users instead pay a sequencer to execute these checks via the circuit(s) which encode them. The sequencer can then generate a zero-knowledge proof of having executed the circuit(s) correctly, which they can send to a rollup contract on Ethereum. The Ethereum validators then verify this zk-S(N/T)ARK. It often turns out to be much cheaper for users to pay the sequencer to do this, than to execute a smart contract on Ethereum directly. + +But there's a problem. + +Ethereum (and the EVM) doesn't have a notion of privacy. + +- There is no notion of a private state variable in the EVM. +- There is no notion of a private function in the EVM. + +So users cannot keep private state variables' values private from Ethereum validators, nor from existing (non-private) L2 sequencers. Nor can users keep the details of which function they've executed private from validators or sequencers. + +How does Aztec add privacy? + +Well, we just encode _extra_ checks in our zk-Rollup's zk-SNARK circuits! These extra checks introduce the notions of private state and private functions, and enforce privacy-preserving constraints on every transaction being sent to the network. + +In other words, since neither the EVM nor other rollups have rules for how to preserve privacy, we've written a new rollup which introduces such rules, and we've written circuits to enforce those rules! + +What kind of extra rules / checks does a rollup need, to enforce notions of private states and private functions? Stuff like: + +- "Perform state reads and writes using new tree structures which prevent tx linkability" (see [indexed merkle tree](../storage/indexed_merkle_tree.mdx). +- "Hide which function was just executed, by wrapping it in a zk-snark" +- "Hide all functions which were executed as part of this tx's stack trace, by wrapping the whole tx in a zk-snark" + +## Aztec core protocol circuits + +So what kinds of core protocol circuits does Aztec have? + +### Kernel, Rollup, and Squisher Circuits + +The specs of these have recently been updated. Eg for squisher circuits since Honk and Goblin Plonk schemes are still being improved! But we'll need some extra circuit(s) to squish a Honk proof (as produced by the Root Rollup Circuit) into a Standard Plonk or Fflonk proof, for cheap verification on Ethereum. diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/private_kernel.md b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/private_kernel.md new file mode 100644 index 00000000000..3f73cbf4d19 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/private_kernel.md @@ -0,0 +1,128 @@ +--- +title: Private Kernel Circuit +sidebar_position: 1 +tags: [protocol, circuits] +description: Learn about the Private Kernel Circuit, the only zero-knowledge circuit in Aztec that handles private data and ensures transaction privacy by executing on user devices. +references: ["labs/yarn-project/pxe/src/private_kernel/*"] +--- + +The private kernel circuit is executed by the user on their own device. This ensures private inputs remain private. + +:::note +This is the only core protocol circuit that truly requires the "zero-knowledge" property. Other circuits use SNARKs for succinct verification, but don't need to hide witness data. The private kernel must hide: the contract function executed, the user's address, and the function's inputs and outputs. +::: + +## Overview + +The private kernel processes all private function calls in a transaction, accumulating their side effects (note hashes, nullifiers, logs, messages) and validation requests. It runs recursively—once per private function call—building up a proof that all private execution was correct. + +The kernel validates: +- Proof of private function execution +- Correct call context (caller, address, arguments) +- Proper scoping of side effects to their originating contract +- Uniqueness and ordering of side effect counters + +## Kernel Phases + +The private kernel consists of five circuit types: + +### Init + +The entry point for private kernel execution. It processes the first private function call in a transaction and validates: +- The call matches the transaction request (origin, function, arguments) +- The function is marked as private +- No msg_sender exists (first call has no caller) + +### Inner + +Processes subsequent private function calls after init. Can chain multiple times as the call stack grows. It: +- Verifies the previous kernel proof +- Pops the next call from the private call stack +- Validates the call matches its request +- Appends new side effects to accumulated data + +### Reset + +Can be called one or more times at any point after init and before tail/tail-to-public. Optimizes the accumulated data by: + +- Squashing transient note hash/nullifier pairs (where a note is created and nullified within the same transaction) along with their associated logs +- Validating note hash and nullifier read requests against the state +- Validating key validation requests + +This circuit reduces the data size before finalization. + +### Tail + +The final circuit for **private-only** transactions (no public function calls). It: +- Sorts remaining side effects +- Converts accumulated data to rollup format +- Produces output ready for rollup aggregation + +### Tail to Public + +The bridge circuit for transactions with both private and public execution. It: +- Splits side effects into non-revertible and revertible arrays +- Prepares data for public execution +- Handles the transition from private to public phases + +## Data Flow + +As the kernel processes each private call, it accumulates: + +| Data Type | Description | +|-----------|-------------| +| Note hashes | Commitments to new private notes | +| Nullifiers | Markers that invalidate notes or provide uniqueness | +| L2 to L1 messages | Cross-chain messages to Ethereum | +| Private logs | Encrypted event data | +| Public call requests | Queued public function calls | + +Each item is scoped with the contract address that emitted it, ensuring proper attribution. + +## Performance Impact + +The kernel circuits add significant overhead to every transaction. Understanding this overhead is important when designing contracts and profiling performance. + +### Gate counts per phase + +Consider a typical transaction where a user calls a single contract function. This actually involves **two** private function calls — the account entrypoint (e.g. `SchnorrAccount:entrypoint`) and your contract function — so the kernel processes both: + +```text +Account entrypoint: 22,000 gates (1st private call → processed by init) +Your function: 14,000 gates (2nd private call → processed by inner) +private_kernel_init: ~46,000 gates +private_kernel_inner: ~101,000 gates +private_kernel_reset: ~200,000 gates +private_kernel_tail: ~44,000 gates +───────────────────────────────────── +Total transaction: ~427,000 gates +``` + +The init circuit handles the first private function call (the account entrypoint), and inner handles each subsequent one. The exact kernel gate counts vary depending on the transaction's complexity (number of note hashes, nullifiers, read requests, etc.), but the key takeaway is: **kernel overhead is substantial and scales with the number of private function calls**. + +Each additional private function call in a transaction adds at least one more kernel inner circuit (~101k gates). This means that architectural decisions — like whether to inline logic into one function vs. splitting across multiple function calls — can have a significant impact on total proving time. + +:::tip Design tip +When profiling shows high gate counts, consider whether you can reduce the number of distinct private function calls in your transaction. For example, inlining a verification step into the calling function saves an entire kernel fold (~101k gates), even if it slightly increases the calling function's own gate count. +::: + +:::info Large circuits do not prevent transaction inclusion + +A contract with a high gate count is **not** uncallable. The only effect of a large circuit is that it takes longer to prove on the client. Your transaction will still be included in a block as long as it is valid by the time the proof is submitted. + +In practice, there are only two edge cases where slow proving could cause issues: + +1. **Transaction expiry**: If proving takes so long (e.g. over an hour) that the transaction becomes invalid by the time you're done. This is unlikely for most use cases. +2. **Fee volatility**: If network fees increase rapidly while you're proving, your transaction may be underpriced by the time it's broadcast. This is similar to signing an Ethereum transaction and waiting before submitting it. You can mitigate this by overpaying for fees if rapid inclusion is a priority. + +One of the major benefits of Aztec is that private computation is proven client-side: you can do as much computation as you want in private functions — the network cost is the same regardless. +::: + +For tools to measure these costs, see the [profiling guide](../../../aztec-nr/framework-description/advanced/how_to_profile_transactions.md). + +## Related Pages + +- [Transactions](../../transactions.md) - How private kernel fits into transaction execution +- [Public Execution](./public_execution.md) - How public functions are executed by the AVM +- [Profiling Transactions](../../../aztec-nr/framework-description/advanced/how_to_profile_transactions.md) - Measuring gate counts and identifying bottlenecks +- [Writing Efficient Contracts](../../../aztec-nr/framework-description/advanced/writing_efficient_contracts.md) - Optimization strategies diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/public_execution.md b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/public_execution.md new file mode 100644 index 00000000000..14ccb2bb7c5 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/public_execution.md @@ -0,0 +1,87 @@ +--- +title: Public Execution (AVM) +sidebar_position: 2 +tags: [protocol, circuits] +description: Learn how the Aztec Virtual Machine (AVM) executes public functions and manages public state transitions. +references: ["labs/yarn-project/simulator/src/public/*"] +--- + +Public function execution in Aztec is handled by the **Aztec Virtual Machine (AVM)**. Unlike private execution (which runs on user devices), public execution runs on the sequencer's infrastructure where access to current state is required. + +:::note +Unlike the private kernel which runs recursively for each private call, **there is no "public kernel" circuit**. The AVM executes all public functions for a transaction in a single proof. The term "public kernel" is sometimes used colloquially to refer to the AVM's role in public execution. +::: + +## Overview + +The AVM processes public call requests that were queued during private execution. It operates on the current state of the public data tree, note hash tree, and nullifier tree—state that only the sequencer knows at execution time. + +For transactions containing public functions, the execution flow is: + +1. **Private Kernel** - Processes private functions, queues public call requests +2. **Hiding Kernel** - Bridges private output to public phase +3. **AVM** - Executes all public functions, produces accumulated data +4. **Rollup Circuits** - Validates proofs and includes in block + +## Supported Cryptographic Operations + +The AVM supports Poseidon2, Pedersen, SHA-256, Keccak, and Grumpkin curve operations (embedded curve add, multi-scalar multiplication). ECDSA signature verification, AES-128, Blake2s, and Blake3 are not available in public functions. + +:::warning +If your contract uses unsupported Noir blackbox functions in a public function, transpilation will fail at compile time. See [AVM Cryptographic Compatibility](./avm_compatibility.md) for the full compatibility table and workarounds. +::: + +## Execution Phases + +The AVM executes public functions in three distinct phases: + +| Phase | Revertible | Purpose | +| ------------- | ---------- | ----------------------------------------------- | +| **Setup** | No | Non-revertible initialization (fee preparation) | +| **App Logic** | Yes | Main application logic | +| **Teardown** | Yes | Fee payment finalization | + +This phased approach enables atomic fee payment even if the main transaction logic reverts. The setup phase cannot be reverted, ensuring the sequencer receives payment. + +## Inputs and Outputs + +### Inputs from Private Execution + +The AVM receives from the private phase: + +- **Public call requests**: Setup, app logic, and teardown function calls +- **Non-revertible accumulated data**: Note hashes, nullifiers, L2-L1 messages from setup phase +- **Revertible accumulated data**: Note hashes, nullifiers, L2-L1 messages that can be reverted +- **Gas settings**: Limits for execution and teardown +- **Fee payer**: Address responsible for transaction fees + +### Outputs + +After execution, the AVM produces: + +| Output | Description | +| ------------------ | ------------------------------------------ | +| Note hashes | Combined private + public note commitments | +| Nullifiers | Combined private + public nullifiers | +| L2-L1 messages | Cross-chain messages to Ethereum | +| Public logs | Event data from public execution | +| Public data writes | State updates to the public data tree | +| End tree snapshots | Final state of all trees after execution | +| Transaction fee | Computed fee based on gas consumed | +| Reverted flag | Whether app logic phase reverted | + +## State Transitions + +The AVM validates state transitions by tracking tree snapshots: + +- **Start snapshots**: Tree roots before public execution +- **End snapshots**: Tree roots after all public functions complete + +These snapshots are validated in the rollup circuits to ensure continuity across transactions in a block. + +## Related Pages + +- [AVM Cryptographic Compatibility](./avm_compatibility.md) – Which Noir primitives work in public functions +- [Private Kernel](./private_kernel.md) – How private functions are processed +- [Call Types](../../call_types.md) – How private and public functions interact +- [State Management](../../state_management.md) – How public and private state works diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/rollup_circuits.md b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/rollup_circuits.md new file mode 100644 index 00000000000..9f0227f410a --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/circuits/rollup_circuits.md @@ -0,0 +1,103 @@ +--- +title: Rollup Circuits +sidebar_position: 4 +tags: [protocol, circuits] +description: Learn how Rollup Circuits compress transactions into a single proof using a hierarchical tree topology for efficient verification on Ethereum. +--- + +The rollup circuits compress thousands of transactions into a single SNARK proof for verification on Ethereum. They aggregate proofs from private kernel and AVM execution, validate state transitions, and produce the final epoch proof submitted to L1. + +:::note +The rollup circuits use a "binary tree of proofs" topology. This allows proof generation to be parallelized across prover instances—each layer of the tree can be computed in parallel, or subtrees can be distributed to different provers. +::: + +## Circuit Hierarchy + +Rollup circuits operate at four levels, each producing outputs consumed by the next: + +| Level | Circuits | Input | Output | +|-------|----------|-------|--------| +| **Transaction** | TX Base (Private/Public), TX Merge | Kernel proofs | Transaction rollup data | +| **Block** | Block Root, Block Merge | Transaction rollups | Block rollup data | +| **Checkpoint** | Checkpoint Root, Checkpoint Merge | Block rollups | Checkpoint data | +| **Epoch** | Root Rollup | Checkpoint rollups | Final epoch proof | + +## Transaction Level + +### TX Base Rollups + +Process individual transactions from kernel proofs: + +- **TX Base Private** - Processes transactions with only private execution. Validates the private kernel proof, updates tree snapshots (note hash, nullifier), and accumulates fees and mana usage. +- **TX Base Public** - Processes transactions that include public (AVM) execution. Validates the AVM proof, which has already performed tree updates and fee/mana accumulation during public execution. + +### TX Merge Rollup + +Merges pairs of transaction rollup proofs in binary fashion. Can chain recursively to aggregate many transactions into a single proof. Validates proof correctness and consecutive transaction ordering. + +## Block Level + +### Block Root Rollups + +Transition from transaction-level to block-level outputs. Several variants handle different scenarios: + +- **Block Root First** - First block of a checkpoint (validates parity root and L1-to-L2 tree) +- **Block Root** - Subsequent blocks in a checkpoint +- **Block Root Single TX** - Optimized variant for single-transaction blocks +- **Block Root Empty TX First** - Handles empty blocks + +These circuits update the archive tree, compute block headers, and accumulate L2-to-L1 message hashes. + +### Block Merge Rollup + +Merges pairs of block rollup proofs within a checkpoint. Validates archive continuity and state consistency between blocks. + +## Checkpoint Level + +### Checkpoint Root Rollups + +Transition from block-level to checkpoint-level outputs: + +- **Checkpoint Root** - Standard checkpoint containing multiple blocks +- **Checkpoint Root Single Block** - Optimized for single-block checkpoints + +These circuits validate previous block headers, compute blob commitments, and accumulate fee recipients. + +### Checkpoint Merge Rollup + +Merges pairs of checkpoint proofs. Validates checkpoint continuity and blob accumulator consistency. + +### Checkpoint Padding + +A special circuit for epochs with only one checkpoint. Provides an empty right child for the binary tree structure. + +## Epoch Level (Root Rollup) + +The final circuit that completes an epoch proof. It: + +- Merges two checkpoint rollup proofs +- Validates epoch-level blob batching challenges +- Produces the final `RootRollupPublicInputs` for L1 submission + +The root rollup output includes: + +- Previous and new archive roots +- Checkpoint header hashes +- Accumulated fees across all checkpoints +- Final blob public inputs for data availability + +## Flexible Tree Topology + +The architecture supports asymmetric "wonky trees" for efficiency: + +- Transactions can be grouped variably into blocks +- Not all branches need the same depth +- Single-element optimizations reduce proof overhead +- Padding circuits handle partial epochs + +This flexibility allows sequencers to optimize proving costs based on actual workload. + +## Related Pages + +- [Private Kernel](./private_kernel.md) - How private function proofs are generated +- [Public Execution](./public_execution.md) - How the AVM produces public execution proofs diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/_category_.json b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/_category_.json new file mode 100644 index 00000000000..537ab73d1a0 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Advanced Storage Concepts", + "position": 1, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/indexed_merkle_tree.mdx b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/indexed_merkle_tree.mdx new file mode 100644 index 00000000000..be20b95399e --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/indexed_merkle_tree.mdx @@ -0,0 +1,396 @@ +--- +title: Indexed Merkle Tree (Nullifier Tree) +tags: [storage, concepts, advanced] +sidebar_position: 2 +description: Learn about indexed merkle trees, an efficient data structure for nullifier trees that enables fast non-membership proofs and batch insertions in Aztec. +--- + +import Image from "@theme/IdealImage"; + +## Prerequisites + +This page assumes familiarity with: + +- Merkle trees and membership proofs +- The UTXO model for private state +- Zero-knowledge proof concepts (circuits, constraints) + +## Overview + +This page covers indexed merkle trees and how they improve nullifier tree performance in circuits, including: + +- Why nullifier trees are necessary +- How indexed merkle trees work +- Membership exclusion proofs +- Batch insertions +- Tradeoffs + +This content was presented to the [Privacy + Scaling Explorations team at the Ethereum Foundation](https://pse.dev/). + +<iframe + width="100%" + height="720" + src="https://www.youtube-nocookie.com/embed/x_0ZhUKtWSs?si=TmguEhgz4Gu07Dac" + title="YouTube video player" + frameborder="0" + allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" + allowfullscreen +></iframe> + +## Primer on Nullifier Trees + +Privacy in public blockchains requires a UTXO model. State is stored in encrypted UTXOs in merkle trees. Since updating state directly leaks information, we simulate updates by "destroying" old UTXOs and creating new ones, resulting in an append-only merkle tree. + +A classic merkle tree: + +<Image + img={require("@site/static/img/indexed-merkle-tree/normal-merkle-tree.png")} + alt="Classic merkle tree structure showing leaf nodes hashed up to a root" +/> + +To destroy state, a "nullifier" tree stores deterministic values linked to notes in the append-only tree. This is typically implemented as a sparse Merkle Tree. + +A sparse merkle tree (not every leaf stores a value): + +<Image + img={require("@site/static/img/indexed-merkle-tree/sparse-merkle-tree.png")} + alt="Sparse merkle tree with empty leaves represented as zeros" +/> + +To spend or modify a note in the private state tree, you must create a nullifier and prove it does not already exist in the nullifier tree. Since nullifier trees are modeled as sparse merkle trees, non-membership checks are conceptually trivial. + +Data is stored at the leaf index corresponding to its value. For example, in a sparse tree containing $2^{256}$ values, to prove non-membership of value $2^{128}$: + +- Prove $tree\_values[2^{128}] = 0$ via a merkle membership proof (value does not exist) +- Conversely, prove $tree\_values[2^{128}] == 1$ to show the item exists + +## Problems introduced by using Sparse Merkle Trees for Nullifier Trees + +While sparse Merkle Trees offer a simple solution, they have significant drawbacks. A sparse nullifier tree must have an index for $e \in \mathbb{F}_p$, which for the bn254 curve requires a depth of 254. + +A tree of depth 254 means 254 hashes per membership proof. Each nullifier insertion requires a non-membership check followed by an insertion—two trips from leaf to root. This results in $254 \times 2$ hashes per insertion. Since the tree is sparse, insertions are random and must be sequential, so hash count scales linearly with nullifier count. This causes constraint counts in rollup circuits to grow rapidly, leading to long proving times. + +## Indexed Merkle Tree Constructions + +[This paper](https://eprint.iacr.org/2021/1263.pdf) (page 6) introduces indexed merkle trees, which enable efficient non-membership proofs. Each node stores a value $v \in \mathbb{F}_p$ and pointers to the leaf with the next higher value: + +$$ +\textsf{leaf} = \{v, i_{\textsf{next}}, v_{\textsf{next}}\}. +$$ + +Based on the tree's insertion rules, no leaves exist between the range $(v, v_{\textsf{next}})$. + +The merkle tree forms a linked list of increasing values. Once inserted, a leaf's pointers can change but its nullifier value cannot. + +Since leaves are no longer positioned at $(index == value)$, a deep tree is unnecessary—32 levels suffice. This improves insertions by approximately 8x $(256 / 32)$. + +_The node that provides the non-membership check is called a "low nullifier"._ + +The insertion protocol: + +1. Look for a nullifier's corresponding low_nullifier where: + + $$ + low\_nullifier_{\textsf{next\_value}} > new\_nullifier + $$ + + > if $new\_nullifier$ is the largest use the leaf: + + $$ + low\_nullifier_{\textsf{next\_value}} == 0 + $$ + +2. Perform membership check of the low nullifier. +3. Perform a range check on the low nullifier's value and next_value fields: + +$$ +\begin{aligned} +new\_nullifier &> low\_nullifier_{\textsf{value}} \: \&\& \\ +&( new\_nullifier < low\_nullifier_{\textsf{next\_value}} \: || \: low\_nullifier_{\textsf{next\_value}} == 0 ) +\end{aligned} +$$ + +4. Update the low nullifier pointers + + $$ + low\_nullifier_{\textsf{next\_index}} = new\_insertion\_index + $$ + + $$ + low\_nullifier_{\textsf{next\_value}} = new\_nullifier + $$ + +5. Perform insertion of new updated low nullifier (yields new root) +6. Update pointers on new leaf. Note: low_nullifier is from before update in step 4 + +$$ +new\_nullifier\_leaf_{\textsf{value}} = new\_nullifier +$$ + +$$ +new\_nullifier\_leaf_{\textsf{next\_value}} = low\_nullifier_{\textsf{next\_value}} +$$ + +$$ +new\_nullifier\_leaf_{\textsf{next\_index}} = low\_nullifier_{\textsf{next\_index}} +$$ + +7. Perform insertion of new leaf (yields new root) + +### Insertion constraints + +- `3n` hashes of 2 field elements (where `n` is the height of the tree) +- `3` hashes of 3 field elements +- `2` range checks +- A handful of equality constraints + +### Special cases + +At step 3, $low\_nullifier_{\textsf{next\_value}}$ can be 0. When a value is the maximum, it points to zero instead of a larger value (which does not exist). This closes the loop so insertions always occur within a ring, preventing splits. + +### Insertion example + +1. Initial state + +<Image + img={require("@site/static/img/indexed-merkle-tree/index-merkle-tree-1.png")} + alt="Indexed merkle tree initial state with single zero node" +/> + +2. Add a new value $v=30$ + +<Image + img={require("@site/static/img/indexed-merkle-tree/index-merkle-tree-2.png")} + alt="Indexed merkle tree after inserting value 30" +/> + +3. Add a new value $v=10$ + +<Image + img={require("@site/static/img/indexed-merkle-tree/index-merkle-tree-3.png")} + alt="Indexed merkle tree after inserting value 10" +/> + +4. Add a new value $v=20$ + +<Image + img={require("@site/static/img/indexed-merkle-tree/index-merkle-tree-4.png")} + alt="Indexed merkle tree after inserting value 20" +/> + +5. Add a new value $v=50$ + +<Image + img={require("@site/static/img/indexed-merkle-tree/index-merkle-tree-5.png")} + alt="Indexed merkle tree after inserting value 50" +/> + +The diagrams show how pointers update between each insertion. + +Note: The first node (value 0) is pre-populated, so the first insertion goes into the second index. + +### Non-membership proof + +To prove value `20` does not exist, reveal the leaf that "steps over" `20`—the leaf whose value is less than `20` and whose next value is greater than `20`. This is the `low_nullifier`. + +- Hash the low nullifier: $low\_nullifier = h(10, 1, 30)$ +- Prove the low leaf exists in the tree: `n` hashes +- Check the new value would have belonged in the range given by the low leaf: `2` range checks + - If ($low\_nullifier_{\textsf{next\_index}} == 0$): + - Special case, the low leaf is at the very end, so the new_value must be higher than all values in the tree: + - $assert(low\_nullifier_{\textsf{value}} < new\_value_{\textsf{value}})$ + - Else: + - $assert(low\_nullifier_{\textsf{value}} < new\_value_{\textsf{value}})$ + - $assert(low\_nullifier_{\textsf{next\_value}} > new\_value_{\textsf{value}})$ + +This provides significant performance improvement, but since the tree is not sparse, we can also perform batch insertions. + +## Batch insertions + +Since nullifiers are inserted deterministically (append-only), we can insert entire subtrees rather than appending nodes individually. However, for every node inserted, low nullifier pointers must be updated. This adds complexity when the low nullifier exists within the subtree being inserted. All impacted low nullifiers must be updated before the subtree insertion. + +Batch insertion in an append-only merkle tree: + +1. Prove the target subtree consists of all empty values +2. Calculate the root of an empty subtree and perform an inclusion proof for this empty root +3. Recreate the subtree within the circuit +4. Use the same sibling path to get the new root after subtree insertion + +In the following example, a subtree of size 4 is inserted. The subtree is greyed out as "pending". + +**Legend**: + +- Green: New Inserted Value +- Orange: Low Nullifier + +**Example** + +1. Prepare to insert subtree $[35,50,60,15]$ + +<Image + img={require("@site/static/img/indexed-merkle-tree/subtree-insert-1.png")} + alt="Preparing to insert subtree with values 35, 50, 60, 15" +/> + +2. Update low nullifier for new nullifier $35$ + +<Image + img={require("@site/static/img/indexed-merkle-tree/subtree-insert-2.png")} + alt="Updating low nullifier for value 35" +/> + +3. Update low nullifier for new nullifier $50$ (the low nullifier exists within the pending insertion subtree) + +<Image + img={require("@site/static/img/indexed-merkle-tree/subtree-insert-3.png")} + alt="Updating low nullifier for value 50 from pending subtree" +/> + +4. Update low nullifier for new nullifier $60$ + +<Image + img={require("@site/static/img/indexed-merkle-tree/subtree-insert-4.png")} + alt="Updating low nullifier for value 60" +/> + +5. Update low nullifier for new nullifier $15$ + +<Image + img={require("@site/static/img/indexed-merkle-tree/subtree-insert-5.png")} + alt="Updating low nullifier for value 15" +/> + +6. Update pointers for new nullifier $15$ + +<Image + img={require("@site/static/img/indexed-merkle-tree/subtree-insert-6.png")} + alt="Updating pointers for value 15" +/> + +7. Insert subtree + +<Image + img={require("@site/static/img/indexed-merkle-tree/subtree-insert-7.png")} + alt="Final state after subtree insertion" +/> + +### Performance gains from subtree insertion + +Sparse nullifier tree insertions require 1 non-membership check (254 hashes) and 1 insertion (254 hashes). For 4 values: 2032 hashes. + +In a depth-32 indexed tree, each subtree insertion costs 1 non-membership check (32 hashes) and 1 pointer update (32 hashes) per value, plus subtree construction (~67 hashes). Total: 327 hashes—a significant efficiency gain. + +_Range check constraint costs are negligible compared to hash costs._ + +## Performing subtree insertions in a circuit context + +A challenge arises when the low nullifier for a value exists within the subtree being inserted. In this case, you cannot perform a non-membership check against the tree root because the leaf needed for non-membership has not yet been inserted. These are called "pending" insertions. + +**Circuit Inputs** + +- `new_nullifiers`: `fr[]` +- `low_nullifier_leaf_preimages`: `tuple of {value: fr, next_index: fr, next_value: fr}` +- `low_nullifier_membership_witnesses`: A sibling path and a leaf index of low nullifier +- `current_nullifier_tree_root`: Current root of the nullifier tree +- `next_insertion_index`: `fr`, the tip of the nullifier tree +- `subtree_insertion_sibling_path`: A sibling path to check the subtree against the root + +If the low nullifier does not yet exist in the tree, membership checks fail and no non-membership proof can be produced. To handle this, the circuit must track all values pending insertion: + +- If `low_nullifier_membership_witness` is invalid (all zeros or leaf index of -1), this indicates a pending low nullifier read request +- Loop through all pending insertions to find one with value lower than the nullifier being inserted +- If no matching pending insertion is found, the circuit is invalid + +Pseudocode with pending insertion handling: + +```cpp + +auto empty_subtree_hash = SOME_CONSTANT_EMPTY_SUBTREE; +auto pending_insertion_subtree = []; +auto insertion_index = inputs.next_insertion_index; +auto root = inputs.current_nullifier_tree_root; + +// Check nothing exists where we would insert our subtree +assert(membership_check(root, empty_subtree_hash, insertion_index >> subtree_depth, inputs.subtree_insertion_sibling_path)); + +for (i in len(new_nullifiers)) { + auto new_nullifier = inputs.new_nullifiers[i]; + auto low_nullifier_leaf_preimage = inputs.low_nullifier_leaf_preimages[i]; + auto low_nullifier_membership_witness = inputs.low_nullifier_membership_witnesses[i]; + + if (low_nullifier_membership_witness is garbage) { + bool matched = false; + + // Search for the low nullifier within our pending insertion subtree + for (j in range(0, i)) { + auto pending_nullifier = pending_insertion_subtree[j]; + + if (pending_nullifier.is_garbage()) continue; + if (pending_nullifier[j].value < new_nullifier && (pending_nullifier[j].next_value > new_nullifier || pending_nullifier[j].next_value == 0)) { + + // Found matching low nullifier + matched = true; + + // Update pointers + auto new_nullifier_leaf = { + .value = new_nullifier, + .next_index = pending_nullifier.next_index, + .next_value = pending_nullifier.next_value + } + + // Update pending subtree + pending_nullifier.next_index = insertion_index; + pending_nullifier.next_value = new_nullifier; + + pending_insertion_subtree.push(new_nullifier_leaf); + break; + } + } + + // could not find a matching low nullifier in the pending insertion subtree + assert(matched); + + } else { + // Membership check for low nullifier + assert(perform_membership_check(root, hash(low_nullifier_leaf_preimage), low_nullifier_membership_witness)); + + // Range check low nullifier against new nullifier + assert(new_nullifier < low_nullifier_leaf_preimage.next_value || low_nullifier_leaf.next_value == 0); + assert(new_nullifier > low_nullifier_leaf_preimage.value); + + // Update new nullifier pointers + auto new_nullifier_leaf = { + .value = new_nullifier, + .next_index = low_nullifier_preimage.next_index, + .next_value = low_nullifier_preimage.next_value + }; + + // Update low nullifier pointers + low_nullifier_preimage.next_index = next_insertion_index; + low_nullifier_preimage.next_value = new_nullifier; + + // Update state vals for next iteration + root = update_low_nullifier(low_nullifier, low_nullifier_membership_witness); + + pending_insertion_subtree.push(new_nullifier_leaf); + + } + + next_insertion_index += 1; +} + +// insert subtree +root = insert_subtree(root, inputs.next_insertion_index >> subtree_depth, pending_insertion_subtree); + +``` + +## Drawbacks + +While indexed merkle trees provide significant circuit performance improvements, they increase computation and storage requirements for nodes. Finding the "low nullifier" for a non-membership proof requires searching existing nodes—a naive implementation uses brute force. Performance improves if nodes maintain a sorted data structure of existing nullifiers, though this increases storage footprint. + +## Related resources + +- [State Management](../../state_management.md) - How public and private state work, including nullifiers +- [Circuits](../circuits/index.md) - Core protocol circuits where indexed merkle trees are used +- [Note Discovery](note_discovery.md) - How private notes are discovered and managed +- [Original paper](https://eprint.iacr.org/2021/1263.pdf) - Academic reference for indexed merkle trees diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/note_discovery.md b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/note_discovery.md new file mode 100644 index 00000000000..6d26bcfb75b --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/note_discovery.md @@ -0,0 +1,133 @@ +--- +title: Note Discovery +tags: [storage, concepts, advanced, notes] +description: Understand how Aztec's note tagging system allows users to efficiently discover and decrypt notes that belong to them without relying on brute force or offchain communication. +sidebar_position: 3 +references: ["labs/noir-projects/aztec-nr/aztec/src/messages/discovery/*", "labs/noir-projects/aztec-nr/aztec/src/messages/delivery/*", "labs/yarn-project/pxe/src/tagging/*", "labs/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/*"] +--- + +Note discovery refers to the process of a user identifying and decrypting [notes](../../state_management.md#notes) that belong to them. + +## Alternative approaches + +Other protocols have explored different note discovery mechanisms. **Brute force** approaches download all notes and trial-decrypt each one, but this becomes prohibitively expensive as networks grow. **Offchain communication** has the sender share note content directly with recipients, avoiding onchain costs but introducing reliance on side channels. Aztec apps can use offchain communication if they wish, but the default mechanism is note tagging. + +## Note tagging + +Aztec uses note tagging as its default discovery mechanism. When creating a note, the sender _tags_ the log with a value that only the sender and recipient can identify. This allows recipients to efficiently query for relevant logs without downloading and attempting to decrypt everything. + +### How it works + +#### Every log has a tag + +In Aztec, each emitted log is an array of fields, e.g. `[tag, x, y, z]`. The first field is a _tag_ used to index and identify logs. The Aztec node indexes logs by their tag and exposes an API (`getPrivateLogsByTags()`) that retrieves logs matching specific tags. + +#### Tag derivation + +Every tag is derived the same way: `poseidon2(secret, index)`. + +What varies is how the sender and recipient come to share `secret`. This is the [tagging secret strategy](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy), chosen by the wallet by default, though a contract can [override it at delivery](../../../aztec-nr/framework-description/note_delivery.md#overriding-the-strategy-from-the-contract). + +There are four strategies, described below. They differ only in how `secret` is established. Once `secret` exists, the tag is derived from it the same way for all four. + +##### Arbitrary secret + +Two parties that already share a secret point out of band can use it directly. PXE app-siloes the point to the contract (`poseidon2(point.x, point.y, contract)`) and then folds in the recipient (`poseidon2(appSecret, recipient)`) to make the secret directional, so tags from Alice to Bob differ from tags from Bob to Alice. The recipient registers the secret point with their PXE so it can scan for the resulting tags. This leaves no onchain trace, but nothing onchain backs the secret, so it cannot be used for [constrained delivery](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy). + +##### Address-derived secret + +An address-derived secret is the same, except the shared secret point is computed instead of supplied. The sender and recipient each derive it via Diffie-Hellman on the Grumpkin curve from their own [incoming viewing secret key](../../accounts/keys.md#incoming-viewing-keys) (`ivsk`) and the other party's address point: `S = (preaddress + ivsk) × AddressPoint`. The app-siloing and directional fold are then identical. Because the point comes from the parties' addresses, the recipient registers the sender's address with their PXE (rather than a secret point) so it can compute the tags. Like an arbitrary secret, it cannot back [constrained delivery](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy). + +##### Non-interactive handshake + +To establish a handshake, the sender publishes an ephemeral public key onchain, encrypted to the recipient, under a log tagged with the recipient's address. During sync the recipient scans for handshakes addressed to them, decrypts the ephemeral key, and derives the shared secret via Diffie-Hellman against their own `ivsk`. Because that secret is already derived against the recipient, it is app-siloed to the contract but left bare, with no directional fold. + +This lets the recipient discover messages from a sender they never registered, at the cost of publishing onchain that a handshake was made with them. It is the default for reaching a new external recipient, and unlike an address-derived or arbitrary secret it can back [constrained delivery](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy). + +##### Interactive handshake + +An interactive handshake derives the same kind of shared secret from an ephemeral key, but the ephemeral key is never published: instead of announcing it onchain, the recipient must be reachable at send time to sign it. The registry requests that signature through the custom request oracle and verifies it in-circuit, so the send fails if the recipient does not answer. The recipient learns the ephemeral key while signing and registers the resulting secret with their PXE so it can scan for the tags. As with a non-interactive handshake, the secret is app-siloed to the contract but left bare, with no directional fold. + +This reveals nothing onchain about the recipient, and like a non-interactive handshake it can back [constrained delivery](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy). Because it requires the recipient's cooperation at send time, it is never the default: a wallet or contract opts in explicitly. + +##### Deriving the tag from the secret + +Whichever strategy produced it, the resulting app-siloed tagging secret is turned into a tag the same way: + +```mermaid +flowchart LR + G["Tagging Secret"] --> H{{"poseidon2(secret, index)"}} + H --> I[Tag] + I --> J{{"silo(contract, tag)"}} + J --> K[Siloed Tag] +``` + +The tagging secret is hashed with an index, a counter that increments for each log the sender emits to this recipient in this contract: `poseidon2(secret, index)`. When the log is emitted, the protocol kernel **siloes** the resulting tag with the contract address before it appears onchain. This siloed tag is what the node stores and indexes. Both the sender and recipient can independently compute the siloed tags and use them to query the node. + +#### The sender in note tagging + +The "sender" in note tagging is **not necessarily the transaction sender**. It's the **sender for tags**, which the wallet supplies as a default (typically the originating account address). Contracts can override this at message delivery by using `with_sender`, for both constrained and unconstrained delivery, e.g. `MessageDelivery::onchain_constrained().with_sender(address)`. + +#### Registering known senders + +To discover notes from a particular sender via an [address-derived secret](#address-derived-secret), the recipient's PXE must know the sender's address in advance so it can compute the shared tagging secret. Register senders using the wallet API: + +```typescript +// Register a sender so your PXE can discover notes from them +await wallet.registerSender(senderAddress); +``` + +Notes sent to yourself are always discoverable — the PXE automatically adds all local accounts as implicit senders. + +### The sync process + +The `#[aztec]` macro automatically injects an unconstrained `sync_state` utility function into every contract. This function is invoked by the PXE during note syncing to orchestrate discovery via oracles; manual execution is forbidden by the PXE to prevent inconsistencies. The process works as follows: + +1. **Fetch tagged logs**: The contract calls the `fetchTaggedLogs` oracle. The PXE computes tags for every secret it can use for this recipient, queries the node for matching logs, and returns them to the contract. + +2. **Decrypt**: For each log, the contract strips the tag and attempts AES-128 decryption using a symmetric key derived from the recipient's private key (via ECDH). Logs that don't decrypt are silently discarded (they were not intended for this recipient). + +3. **Parse message type**: Successfully decrypted messages are dispatched by type — private notes, partial notes, or private events. + +4. **Nonce discovery** (for notes): To confirm a decrypted note is valid, the system must match it against the unique note hashes emitted in the same transaction. It iterates the note hashes in the transaction, computes candidate nonces using `compute_note_hash_nonce(first_nullifier, note_index)` (a domain-separated Poseidon2 hash), and checks whether recomputing the unique note hash with each candidate nonce produces a match. A match confirms the note was emitted in this transaction and provides the nonce needed to later nullify it. (Note hash tree inclusion is validated separately.) + +5. **Store**: Validated notes are added to the PXE database, making them available for use in future transactions. + +Developers don't need to implement any of this manually — the `#[aztec]` macro handles it. However, since the discovery logic lives in contract code (called via oracles to the PXE), users can customize or replace the discovery mechanism to suit their needs. + +#### The sliding window algorithm + +The PXE doesn't scan all possible tag indexes — it uses a window-based approach to efficiently find new logs: + +- It tracks the **highest aged index**: the highest tag index seen in a block at least 24 hours old (`MAX_TX_LIFETIME`). Once a block is this old, no new transactions can reference it as an anchor, so no new logs can appear at or below that index. +- It tracks the **highest finalized index**: the highest tag index seen in any finalized block. +- It scans from the aged index to 20 indexes beyond the finalized index, covering both recent and in-flight logs. + +This means there's a practical limit on how many logs a single sender can emit to the same recipient in the same contract within a short time period. For most applications this limit is not a concern. + +### Limitations and solutions + +#### You cannot receive address-derived tagged notes from an unknown sender + +When the tag's secret is [address-derived](#address-derived-secret), you cannot compute it without knowing the sender's address, so you cannot discover those notes from a sender you haven't registered. This is a limitation of address-derived tagging, not of tagging in general. + +There are three broad families of solutions to this problem: + +**a) Brute force search** - Scan every single log and test if it decrypts. This has obvious performance issues as the network grows and becomes prohibitively expensive. + +**b) Tagging with known sender** - You know who will send you messages and search for those specifically. This is very fast and allows you to remove senders who spam you. However, it cannot be constrained, i.e., it cannot guarantee that the recipient will find the message. It also requires registering each sender's address in advance with `wallet.registerSender(address)`, so you must learn that address first. + +**c) Tagging with a handshake** - The sender and recipient establish a handshake to agree on a tagging secret, after which regular tagging works: the recipient can discover messages without having registered the sender in advance. A non-interactive handshake is published onchain, so it needs nothing from the recipient, but it reveals that someone did a handshake with them. An interactive handshake reveals nothing onchain; in exchange, the recipient must sign it at send time. By default the wallet determines the type of handshake to use, though a contract can override the choice at delivery (see [tagging secret strategy](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy)). + +See the [Note Delivery](../../../aztec-nr/framework-description/note_delivery.md) documentation for more details on how the sender is used when delivering notes. + +## Advanced cryptography techniques + +Beyond the tagging system described above, there are more advanced cryptographic techniques for note discovery: + +- **Oblivious message retrieval (OMR)**: Allows retrieving messages without the server knowing which messages were accessed +- **Private information retrieval (PIR)**: Enables querying a database without revealing which records you're interested in + +These techniques would solve a privacy leak that exists with the current tagging system: when your PXE queries an Aztec node for logs with specific tags, the node can observe your IP address and correlate it with which tags (and therefore which transactions) you're interested in. Even though the logs are encrypted, this network-level metadata can leak information about your activity. + +OMR and PIR would eliminate this issue by allowing you to retrieve your logs without the node knowing which ones you requested. However, these methods are currently impractical in production due to computational costs. They represent a long-term goal for achieving stronger privacy guarantees. diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/storage_slots.md b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/storage_slots.md new file mode 100644 index 00000000000..d7071ad3879 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/advanced/storage/storage_slots.md @@ -0,0 +1,70 @@ +--- +title: Storage Slots +tags: [storage, concepts, advanced] +sidebar_position: 1 +description: Understand how storage slots work in Aztec for both public and private state, including siloing mechanisms and note hash commitments. +references: ["labs/noir-projects/aztec-nr/aztec/src/state_vars/*"] +--- + +Storage slots in Aztec serve a similar purpose to Ethereum—they identify where contract state is stored. However, Aztec handles public and private state differently to maintain privacy guarantees while preventing conflicts between contracts. + +:::note +In the formulas below, `H()` represents a hash function (specifically poseidon2 in the protocol). +::: + +## Public State Slots + +As described in [State Model](../../state_management.md), Aztec public state behaves similarly to public state on Ethereum from a developer's perspective. Behind the scenes, however, the storage is managed differently. Public state uses a single large sparse tree, so we silo slots by hashing them with the contract address: + +```rust +siloed_storage_slot = H(contract_address, storage_slot) +``` + +You can think of `storage_slot` as the logical position in contract storage, while `siloed_storage_slot` identifies the actual position in the global tree. This siloing is performed by the [kernel circuits](../circuits/private_kernel.md). + +For structs and arrays, logical storage slots are computed similarly to Ethereum (e.g., a struct with 3 fields uses 3 consecutive logical slots). However, since siloed slots are hashes, the actual tree positions are not consecutive. + +## Private State Slots + +Private storage works differently. As described in [State Model](../../state_management.md), private state is stored as encrypted logs with corresponding commitments in the note hash tree—an append-only structure where each leaf is a note hash. Notes are never updated or deleted; instead, a nullifier is emitted to invalidate a note. This append-only design prevents information leakage that would occur if we updated specific storage slots, even with encrypted values. + +Because of this, storage slots don't exist in the traditional sense for private state. The note hash tree leaves are simply commitments to note content. + +Nevertheless, the concept of a storage slot remains useful for application logic. It allows us to reason about distinct pieces of data—for example, ensuring that one account's balance cannot be confused with another's, or with the total supply. + +### How Storage Slots Work in Private State + +Storage slots are included as part of the note hash computation, logically linking all notes that belong to the same slot. For a token balance, this means the balance equals the sum of all non-nullified notes sharing the same storage slot—similar to how a physical wallet's balance is the sum of the bills inside it. + +The note hash computation includes the storage slot along with other note data (owner, randomness, and note-specific values). This happens in the application circuit. The private state variable wrappers in Aztec.nr (`PrivateSet`, `PrivateMutable`, etc.) handle this automatically. + +When reading notes, the application circuit constrains which storage slot the notes must belong to, ensuring notes from different slots cannot be mixed. + +### Contract Address Siloing + +To ensure contracts can only modify their own storage, the kernel circuit performs a second siloing step: + +```rust +siloed_note_hash = H(contract_address, note_hash) +``` + +This forces all note hashes to be scoped to the contract that created them. The kernel then makes each note hash unique by incorporating a nonce derived from the transaction: + +```rust +unique_note_hash = H(note_nonce, siloed_note_hash) +``` + +This `unique_note_hash` is what gets inserted into the note hash tree. + +:::info +Nullifiers are also siloed by contract address at the kernel level to prevent collisions across contracts. +::: + +### Privacy Implications + +With this design, knowing a storage slot is not sufficient to determine what data it contains—unlike public state where the slot directly maps to a value. The note hash tree only contains commitments, and the storage slot is just one component mixed into those commitments. This is a key property that enables private state in Aztec. + +## Further Reading + +- [State Model](../../state_management.md) - Overview of public and private state in Aztec +- [Private Kernel Circuits](../circuits/private_kernel.md) - How siloing is enforced at the protocol level diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/call_types.md b/docs/lines/v6/docs-developers/docs/foundational-topics/call_types.md new file mode 100644 index 00000000000..a8bda3875c4 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/call_types.md @@ -0,0 +1,234 @@ +--- +title: Call Types +sidebar_position: 6 +tags: [calls, contracts, execution] +description: Understand the different types of contract calls in Aztec, including private and public execution modes, and how they compare to Ethereum's call types. +references: ["labs/noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr", "labs/noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr", "labs/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr", "labs/noir-projects/noir-contracts/contracts/fees/fpc_contract/src/main.nr", "labs/yarn-project/end-to-end/src/automine/card_game.test.ts", "labs/yarn-project/end-to-end/src/automine/token/crowdfunding_and_claim.test.ts"] + +--- + +## What is a Call + +We say that a smart contract is called when one of its functions is invoked and its code is run. This means there'll be: + +- a caller +- arguments +- return values +- a call status (successful or failed) + +There are multiple types of calls, and some of the naming can make things **very** confusing. This page lists the different call types and execution modes, pointing out key differences between them. + +import YouTubeEmbed from '@site/src/components/YouTubeEmbed'; + +A key property of Aztec calls is that contracts can call each other privately, keeping even the call stack itself private. This two-minute explainer covers the idea before we get into the details (find more on the [video lessons](../resources/video_lessons.mdx) page): + +<YouTubeEmbed videoId="idxRuGQnQKs" title="What is Private Composability? An Aztec Explainer" /> + +## Ethereum Call Types + +Aztec's design is heavily influenced by Ethereum, and many APIs and concepts are similar. This section provides background on Ethereum call types for context. If you're already familiar with Ethereum, you can skip to [Aztec Call Types](#aztec-call-types). + +<details> +<summary>Ethereum background (click to expand)</summary> + +Broadly speaking, Ethereum contracts can be thought of as executing as a result of three different things: running certain EVM opcodes, running Solidity code (which compiles to EVM opcodes), or via the node JSON-RPC interface (e.g. when executing transactions). + +### EVM + +Certain opcodes allow contracts to make calls to other contracts, each with different semantics. We're particularly interested in `CALL` and `STATICCALL`, and how those relate to contract programming languages and client APIs. + +#### `CALL` + +This is the most common and basic type of call. It grants execution control to the caller until it eventually returns. No special semantics are in play here. Most Ethereum transactions spend the majority of their time in `CALL` contexts. + +#### `STATICCALL` + +This behaves almost exactly the same as `CALL`, with one key difference: any state-changing operations are forbidden and will immediately cause the call to fail. This includes writing to storage, emitting logs, or deploying new contracts. This call is used to query state on an external contract, e.g. to get data from a price oracle, check for access control permissions, etc. + +#### Others + +The `CREATE` and `CREATE2` opcodes (for contract deployment) also result in something similar to a `CALL` context, but all that's special about them has to do with how deployments work. `DELEGATECALL` (and `CALLCODE`) are somewhat complicated to understand but don't have any Aztec equivalents, so they are not worth covering. + +### Solidity + +Solidity (and other contract programming languages such as Vyper) compile down to EVM opcodes, but it is useful to understand how they map language concepts to the different call types. + +#### Mutating External Functions + +These are functions marked `payable` (which can receive ETH, which is a state change) or with no mutability declaration (sometimes called `nonpayable`). When one of these functions is called on a contract, the `CALL` opcode is emitted, meaning the callee can perform state changes, make further `CALL`s, etc. + +It is also possible to call such a function with `STATICCALL` manually (e.g. using assembly), but the execution will fail as soon as a state-changing opcode is executed. + +#### `view` + +An external function marked `view` will not be able to mutate state (write to storage, etc.), it can only _view_ the state. Solidity will emit the `STATICCALL` opcode when calling these functions, since its restrictions provide added safety to the caller (e.g. no risk of reentrancy). + +Note that it is entirely possible to use `CALL` to call a `view` function, and the result will be the exact same as if `STATICCALL` had been used. The reason why `STATICCALL` exists is so that _untrusted or unknown_ contracts can be called while still being able to reason about correctness. From the [EIP](https://eips.ethereum.org/EIPS/eip-214): + +> '`STATICCALL` adds a way to call other contracts and restrict what they can do in the simplest way. It can be safely assumed that the state of all accounts is the same before and after a static call.' + +### JSON-RPC + +From outside the EVM, calls to contracts are made via [JSON-RPC](https://ethereum.org/en/developers/docs/apis/json-rpc/) methods, typically from some client library that is aware of contract ABIs, such as [ethers.js](https://docs.ethers.org/v5) or [viem](https://viem.sh/). + +#### `eth_sendTransaction` + +This method is how transactions are sent to a node to get them to be broadcast and eventually included in a block. The specified `to` address will be called in a `CALL` context, with some notable properties: + +- there are no return values, even if the contract function invoked does return some data +- there is no explicit caller: it is instead derived from a provided signature + +Some client libraries choose to automatically issue `eth_sendTransaction` when calling functions from a contract ABI that are not marked as `view` - [ethers is a good example](https://docs.ethers.org/v5/getting-started/#getting-started--writing). Notably, this means that any return value is lost and not available to the calling client - the library typically returns a transaction receipt instead. If the return value is required, the only option is to simulate the call using `eth_call`. + +Note that it is possible to call non state-changing functions (i.e. `view`) with `eth_sendTransaction` - this is always meaningless. What transactions do is change the blockchain state, so all calling such a function achieves is for the caller to lose funds by paying for gas fees. The sole purpose of a `view` function is to return data, and `eth_sendTransaction` does not make the return value available. + +#### `eth_call` + +This method is the largest culprit of confusion around calls, but unfortunately requires understanding of all previous concepts in order to be explained. Its name is also quite unhelpful. + +What `eth_call` does is simulate a transaction (a call to a contract) given the current blockchain state. The behavior will be the exact same as `eth_sendTransaction`, except: + +- no actual transaction will be created +- while gas _will_ be measured, there'll be no transaction fees of any kind +- no signature is required: the `from` address is passed directly, and can be set to any value (even if the private key is unknown, or if they are contract addresses!) +- the return value of the called contract is available + +`eth_call` is typically used for one of the following: + +- query blockchain data, e.g. read token balances +- preview the state changes produced by a transaction, e.g. the transaction cost, token balance changes, etc + +Because some libraries ([such as ethers](https://docs.ethers.org/v5/getting-started/#getting-started--reading)) automatically use `eth_call` for `view` functions (which when called via Solidity result in the `STATICCALL` opcode), these concepts can be hard to tell apart. The following bears repeating: **an `eth_call`'s call context is the same as `eth_sendTransaction`, and it is a `CALL` context, not `STATICCALL`.** + +</details> + +## Aztec Call Types + +While Ethereum contracts are defined by bytecode that runs on the EVM, Aztec contracts have multiple modes of execution depending on the function that is invoked. This section covers the main ways contracts can be interacted with, drawing analogies to Ethereum call types where applicable. + +### Quick Reference + +| Execution Mode | Annotation | Runs On | State Access | Use Case | +|----------------|------------|---------|--------------|----------| +| **Private** | `#[external("private")]` | User's device | Private state (notes) | Confidential transactions, private transfers | +| **Public** | `#[external("public")]` | Sequencer | Public state | Token balances, access control checks | +| **Utility** | `#[external("utility")]` | Offchain client | Both (unconstrained) | Read-only queries, frontend data fetching | + +### Private Execution + +Contract functions marked with `#[external("private")]` can only be called privately, and as such 'run' in the user's device. Since they're circuits, their 'execution' is actually the generation of a zk-SNARK proof that'll later be sent to the sequencer for verification. + +#### Private Calls + +Private functions from other contracts can be called either regularly or statically by using `self.call()` and `self.view()`. They will also be 'executed' (i.e. proved) in the user's device, and `self.view()` will fail if any state changes are attempted (like the EVM's `STATICCALL`). + +#include_code private_call /labs/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr rust + +Unlike the EVM however, private execution doesn't revert in the traditional way: in case of error (e.g. a failed assertion, a state changing operation in a static context, etc.) the proof generation simply fails and no transaction request is generated, spending no network gas or user funds. + +#### Public Calls + +Since public execution can only be performed by the sequencer, public functions cannot be executed in a private context. It is possible however to _enqueue_ a public function call during private execution, requesting the sequencer to run it during inclusion of the transaction. It will be [executed in public](#public-execution) normally, including the possibility to enqueue static public calls. + +Since the public call is made asynchronously, any return values or side effects are not available during private execution. If the public function fails once executed, the entire transaction is reverted including state changes caused by the private part, such as new notes or nullifiers. Note that this does result in gas being spent, like in the case of the EVM. + +#include_code enqueue_public /labs/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr rust + +It is also possible to create public functions that can _only_ be invoked by privately enqueueing a call from the same contract, which can be very useful to update public state after private execution (e.g. update a token's supply after privately minting). This is achieved by annotating functions with `#[only_self]`. + +A common pattern is to enqueue public calls to check some validity condition on public state, e.g. that a deadline has not expired or that some public value is set. + +#include_code enqueueing /labs/noir-projects/aztec-nr/aztec/src/public_checks.nr rust + +Note that this reveals what public function is being called on what contract, and perhaps more importantly which contract enqueued the call during private execution. +To prevent this you can enqueue a call to a public function using `self.enqueue_incognito` that behaves the same as `self.enqueue` but conceals the message sender. + +To address this, we have introduced a `PublicChecks` contract that can be used to perform common checks, such as verifying the timestamp or block number. +By having these checks on a contract shared between apps the privacy set increases. + +An example of how a deadline can be checked using the `PublicChecks` contract follows: + +#include_code call-check-deadline /labs/noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr rust + +`privately_check_timestamp` and `privately_check_block_number` are helper functions around the call to the `PublicChecks` contract: + +#include_code helper_public_checks_functions /labs/noir-projects/aztec-nr/aztec/src/public_checks.nr rust + +This is what the implementation of the check timestamp functionality looks like: + +#include_code check_timestamp /labs/noir-projects/noir-contracts/contracts/standard/public_checks_contract/src/main.nr rust + +:::note +The `PublicChecks` contract is not part of the [aztec-nr repository](https://github.com/aztec-labs-eng/aztec-nr). +To add it as a dependency, point to the aztec-packages repository: + +```toml +[dependencies] +public_checks = { git = "https://github.com/aztec-labs-eng/aztec-node/", tag = "#include_aztec_version", directory = "noir-projects/noir-contracts/contracts/standard/public_checks_contract" } +``` + +::: + +Even with the public checks contract, achieving good privacy is hard. +For example, if the value being checked against is unique and stored in the contract's public storage, it's then simple to find private transactions that are using that value in the enqueued public reads, and therefore link them to this contract. +For this reason it is encouraged to try to avoid public function calls and instead privately read [Delayed Public Mutable](../aztec-nr/framework-description/state_variables.md#delayedpublicmutable) state when possible. + +### Public Execution + +Contract functions marked with `#[external("public")]` can only be called publicly, and are executed by the sequencer. The computation model is very similar to the EVM: all state, parameters, etc. are known to the entire network, and no data is private. Static execution like the EVM's `STATICCALL` is possible too, with similar semantics (state can be accessed but not modified, etc.). + +:::note +The AVM supports a subset of Noir's cryptographic operations. Signature verification (ECDSA) is not available in public functions. See [AVM Cryptographic Compatibility](./advanced/circuits/avm_compatibility.md) for details. +::: + +Since private calls are always run in a user's device, it is not possible to perform any private execution from a public context. A reasonably good mental model for public execution is that of an EVM in which some work has already been done privately, and all that is known about it is its correctness and side-effects (new notes and nullifiers, enqueued public calls, etc.). A reverted public execution will also revert the private side-effects. + +Public functions in other contracts can be called both regularly and statically, just like on the EVM. + +#include_code public_call /labs/noir-projects/noir-contracts/contracts/fees/fpc_contract/src/main.nr rust + +:::note +Public functions can be called either directly in a public context (as shown above), or asynchronously by enqueuing from a private context (as shown in the [Public Calls](#public-calls) section). +::: + +### Utility + +Contract functions marked with `#[external("utility")]` are never proven as part of a transaction, even when called from a private function. They are invoked by applications that interact with contracts for: + +- **State queries**: Reading from both private and public state via an offchain client +- **Local state management**: Modifying contract-related PXE state (e.g., processing logs in Aztec.nr) + +Utility functions can also be called from other utility functions, and from private functions as unconstrained code. Calls that cross a contract boundary require wallet authorization. See [utility calls](../aztec-nr/framework-description/calling_contracts.md#utility-calls) for how to make them. + +Since utility execution is unconstrained and relies heavily on oracle calls, no guarantees are made on the correctness of results. However, you can verify that the bytecode being executed is correct, since a contract's address includes a commitment to all of its utility functions. + +### aztec.js + +There are two main ways to execute an Aztec contract function using the `aztec.js` library, with close similarities to their [JSON-RPC counterparts](#json-rpc). + +#### `simulate` + +This is used to get a result out of an execution, either private or public. It creates no transaction and spends no gas. The mental model is fairly close to that of [`eth_call`](#eth_call), in that it can be used to call any type of function, simulate its execution and get a result out of it. `simulate` is also the only way to run [utility functions](#utility). + +#include_code simulate_function docs/examples/ts/aztecjs_connection/index.ts typescript + +:::warning +No correctness is guaranteed on the result of `simulate`! Correct execution is entirely optional and left up to the client that handles this request. +::: + +#### `send` + +This creates a transaction, generates proofs for private execution, broadcasts the transaction to the network, and returns a receipt. This is how transactions are sent, getting them to be included in blocks and spending gas. It is similar to [`eth_sendTransaction`](#eth_sendtransaction), except it also performs work on the user's device, namely the production of the proof for the private part of the transaction. + +#include_code send_tx labs/yarn-project/end-to-end/src/automine/card_game.test.ts typescript + +You can also use `send` to check for execution failures in testing contexts by expecting the transaction to throw: + +#include_code local-tx-fails /labs/yarn-project/end-to-end/src/automine/token/crowdfunding_and_claim.test.ts typescript + +## Next Steps + +- [State Management](./state_management.md) - Learn how private and public state works in Aztec +- [Transactions](./transactions.md) - Understand the transaction lifecycle +- [Contract Creation](./contract_creation.md) - Deploy and interact with contracts +- [Declaring Storage](../aztec-nr/framework-description/state_variables.md) - Define storage in your contracts diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/contract_creation.md b/docs/lines/v6/docs-developers/docs/foundational-topics/contract_creation.md new file mode 100644 index 00000000000..57b2db0ad9c --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/contract_creation.md @@ -0,0 +1,135 @@ +--- +title: Contract Deployment +sidebar_position: 1 +tags: [contracts, protocol] +description: Learn how contract classes and instances are created and deployed on the Aztec network. +references: ["labs/noir-projects/aztec-nr/aztec/src/history/deployment.nr", "labs/yarn-project/stdlib/src/contract/*"] +--- + +In the Aztec protocol, contracts are created as _instances_ of contract _classes_. Unlike Ethereum where deployment is binary (deployed or not), Aztec contracts progress through multiple states before they are fully operational. + +## Contract Lifecycle Overview + +Aztec contracts go through these states: + +1. **Contract Class Registration** - Publishing the contract bytecode to the network +2. **Contract Instance Creation** - Computing a deterministic address from class, salt, and initialization parameters +3. **Initialization** - Running the constructor to set up initial state +4. **Public Deployment** - Broadcasting the instance to the network for public function calls +5. **Private Function Broadcasting** - Sharing private function artifacts offchain (optional) + +Not every contract needs every state. A private-only contract can skip class registration and public deployment entirely. See the [Contract Deployment Quick Reference](../aztec-nr/contract_readiness_states.md) for a practical guide on which steps your contract needs. + +## Contract Classes + +A contract class is a collection of state variable declarations, and related private, public and utility functions. Contract classes don't have state, they just define code (storage structure and function logic). A contract class cannot be called; only a contract instance can be called. + +### Key Benefits of Contract Classes + +Contract classes simplify code reuse by making implementations a first-class citizen in the protocol. With a single class registration, multiple contract instances can be deployed that reference it, reducing deployment costs. Classes also facilitate upgradability by decoupling state from code, making it easier for an instance to switch to different code while retaining its state. + +### Structure of a Contract Class + +A contract class includes: + +- `artifact_hash`: Hash of the contract artifact +- `private_functions_root`: Merkle root of the private functions tree +- `packed_public_bytecode`: Packed bytecode representation of the AVM bytecode for all public functions + +The specification of the artifact hash is not enforced by the protocol. It should include commitments to utility functions code and compilation metadata. It is intended to be used by clients to verify that an offchain fetched artifact matches a registered class. + +### Contract Class Registration + +A contract class is published by calling a private `publish` function in a canonical `ContractClassRegistry` contract, which emits a registration nullifier. This process guarantees that the public bytecode for a contract class is publicly available, which is required for deploying contract instances. + +Contract class registration can be skipped if there are no public functions, and the contract will still be usable privately. However, if you have public functions, you must either register the class before deployment or skip public deployment entirely (only private functions will be callable). + +## Contract Instances + +A deployed contract is effectively an instance of a contract class. It always references a contract class, which determines what code it executes when called. A contract instance has both private and public state, as well as an address that serves as its identifier. + +### Structure of a Contract Instance + +A contract instance includes: + +- `salt`: User-generated pseudorandom value for uniqueness +- `deployer`: Optional address of the contract deployer. Zero for universal deployment +- `original_contract_class_id`: Identifier of the contract class the instance was deployed with. Updating the instance to a new class via the ContractInstanceRegistry does not change this value, since it is part of the address preimage +- `initialization_hash`: Hash of the selector and arguments to the constructor +- `immutables_hash`: Hash of the contract's compile-time immutable state +- `public_keys`: Public keys participating in address derivation (nullifier, incoming viewing, outgoing viewing, tagging, message-signing, and fallback keys). Only the incoming viewing key is held as an elliptic curve point; the other five are held as their `hash_public_key` digests. + +### Instance Address + +The address of a contract instance is computed as the hash of the elements in its structure. This computation is deterministic, allowing users to precompute the expected deployment address of their contract, including account contracts. + +### Contract Initialization vs. Public Deployment + +Aztec makes an important distinction between initialization and public deployment: + +1. **Initialization**: A contract instance is considered initialized once it emits an initialization nullifier, meaning it can only be initialized once. The default state for any address is uninitialized. A user who knows the preimage of the address can still issue a private call into a function in the contract, as long as that function doesn't assert that the contract has been initialized. +2. **Public Deployment**: A contract instance is considered publicly deployed when it has been broadcast to the network via the `publish_for_public_execution` function in the canonical `ContractInstanceRegistry` contract, which emits a deployment nullifier. All public function calls to an undeployed address fail, since the contract class is not known to the network. + +### Initialization + +Contract constructors are not enshrined in the protocol, but handled at the application circuit level. Constructors are methods used for initializing a contract, either private or public, and contract classes may declare more than a single constructor. They can be declared by the `#[initializer]` macro. You can read more about how to use them on the [defining initializer functions](../aztec-nr/framework-description/functions/how_to_define_functions.md#define-initializer-functions) page. + +A contract must ensure: + +- It is initialized at most once +- It is initialized using the method and arguments defined in its address preimage +- It is initialized by its deployer (if non-zero) +- Functions dependent on initialization cannot be invoked until the contract is initialized + +Functions in a contract may skip the initialization check. + +## Verification of Executed Code + +When a function is called on a contract instance, the protocol circuits verify that the executed code matches what was registered. For private functions, the circuit checks that the function's verification key hash exists in the `private_functions_root` of the contract class. For public functions, the AVM verifies that the bytecode matches the registered `packed_public_bytecode`. This verification ensures that contracts execute the exact code that was published during class registration. + +## Genesis Contracts + +The `ContractInstanceRegistry` and `ContractClassRegistry` contracts are protocol contracts that exist from the genesis of the Aztec Network at predefined addresses. They are necessary for deploying other contracts to the network. + +## Private Function Broadcasting + +Private function artifacts can be shared offchain so others can call your private functions. This is optional—callers who already have the artifacts don't need them broadcast. This step is only necessary when you want external parties to interact with your contract's private functions without having obtained the artifacts through other means. + +## Proving Contract States + +Your contract can verify the deployment or initialization state of other contracts. This is useful for: + +- Ensuring a dependency contract is deployed before interacting +- Access control based on contract state +- Conditional logic based on initialization + +```rust +use aztec::history::deployment::{ + assert_contract_bytecode_was_not_published_by, + assert_contract_bytecode_was_published_by, + assert_contract_was_initialized_by, + assert_contract_was_not_initialized_by, +}; + +// Prove a contract's bytecode was published by a given block +assert_contract_bytecode_was_published_by(block_header, contract_address); + +// Prove a contract's bytecode was NOT published by a given block +assert_contract_bytecode_was_not_published_by(block_header, contract_address); + +// Prove a contract was initialized by a given block +// (init_hash is the contract's initialization hash, obtainable via get_contract_instance) +assert_contract_was_initialized_by(block_header, contract_address, init_hash); + +// Prove a contract was NOT initialized by a given block +assert_contract_was_not_initialized_by(block_header, contract_address, init_hash); +``` + +These functions prove inclusion or non-inclusion of the corresponding nullifiers in the nullifier tree at a given block. + +## Further reading + +- [Contract Deployment Quick Reference](../aztec-nr/contract_readiness_states.md) - Practical guide for which deployment steps your contract needs +- [Deploying Contracts](../aztec-js/how_to_deploy_contract.md) - Deploy contracts using TypeScript +- [DApp Development Tutorial](../tutorials/js_tutorials/aztecjs-getting-started.md) - Build a complete application +- [Communicating Cross-Chain](../aztec-nr/framework-description/ethereum_aztec_messaging.md) - Portal contracts and L1/L2 messaging diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/_category_.json b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/_category_.json new file mode 100644 index 00000000000..0f5423420d3 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Aztec<>Ethereum Messaging", + "position": 10, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/data_structures.md b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/data_structures.md new file mode 100644 index 00000000000..16e6471c89a --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/data_structures.md @@ -0,0 +1,38 @@ +--- +title: Data Structures +description: Learn about the data structures used in Aztec portals for L1-L2 communication. +--- + +This page documents the Solidity structs used for L1-L2 message passing in the Aztec protocol. + +**Source**: [DataStructures.sol](https://github.com/AztecProtocol/aztec-packages/blob/#include_foundation_version/l1-contracts/src/core/libraries/DataStructures.sol) + +## `L1Actor` + +An entity on L1, specifying the address and the chainId. Used when specifying a sender or recipient on L1. + +#include_code l1_actor docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity + +## `L2Actor` + +An entity on L2, specifying the Aztec address and the protocol version. Used when specifying a sender or recipient on L2. + +#include_code l2_actor docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity + +## `L1ToL2Msg` + +A message sent from L1 to L2. The `secretHash` field contains the hash of a secret pre-image that must be known to consume the message on L2. Use [`computeSecretHash`](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/yarn-project/stdlib/src/hash/hash.ts) to compute it from a secret. + +#include_code l1_to_l2_msg docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity + +## `L2ToL1Msg` + +A message sent from L2 to L1. + +#include_code l2_to_l1_msg docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity + +## See also + +- [Inbox](./inbox.md) - L1 contract for sending messages to L2 +- [Outbox](./outbox.md) - L1 contract for consuming messages from L2 +- [Portal messaging overview](./index.md) - How L1-L2 messaging works diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/inbox.md b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/inbox.md new file mode 100644 index 00000000000..9654d4dee3b --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/inbox.md @@ -0,0 +1,44 @@ +--- +title: Inbox +description: Learn about the inbox mechanism in Aztec portals for receiving messages from L1. +tags: [portals, contracts] +--- + +The `Inbox` is a contract deployed on L1 that handles message passing from L1 to L2. + +**Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/#include_foundation_version/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/#include_foundation_version/l1-contracts/src/core/messagebridge/Inbox.sol). + +## `sendL2Message()` + +Sends a message from L1 to L2. + +#include_code send_l1_to_l2_message docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol solidity + +| Name | Type | Description | +| ----------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Recipient | [`L2Actor`](./data_structures.md#l2actor) | The recipient of the message. The recipient's version **MUST** match the inbox version and the actor must be an Aztec contract that is **attached** to the contract making this call. If the recipient is not attached to the caller, the message cannot be consumed by it. | +| Content | `field` (~254 bits) | The content of the message. This is the data that will be passed to the recipient. The content is limited to a single field for rollup purposes. If the content is small enough it can be passed directly, otherwise it should be hashed and the hash passed along (you can use our [`Hash`](https://github.com/AztecProtocol/aztec-packages/blob/#include_foundation_version/l1-contracts/src/core/libraries/crypto/Hash.sol) utilities with `sha256ToField` functions). | +| Secret Hash | `field` (~254 bits) | A hash of a secret used when consuming the message on L2. Keep this preimage secret to make the consumption private. To consume the message the caller must know the pre-image (the value that was hashed). Use [`computeSecretHash`](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/yarn-project/stdlib/src/hash/hash.ts) to compute it from a secret. | +| ReturnValue | `(bytes32, uint256)` | The message hash (used as an identifier) and the leaf index in the tree. | + +#### Edge cases + +- Will revert with `Inbox__ActorTooLarge(bytes32 actor)` if the recipient actor is larger than the field size (~254 bits). +- Will revert with `Inbox__VersionMismatch(uint256 expected, uint256 actual)` if the recipient version doesn't match the inbox version. +- Will revert with `Inbox__ContentTooLarge(bytes32 content)` if the content is larger than the field size (~254 bits). +- Will revert with `Inbox__SecretHashTooLarge(bytes32 secretHash)` if the secret hash is larger than the field size (~254 bits). + +## View functions + +These functions allow you to query the current state of the Inbox. + +| Function | Returns | Description | +| -------------------------- | ----------------- | ------------------------------------------------ | +| `getState()` | `InboxState` | Returns the current inbox state (rolling hash, total messages inserted). | +| `getTotalMessagesInserted()` | `uint64` | Returns the total number of messages inserted into the inbox. | +| `getFeeAssetPortal()` | `address` | Returns the address of the Fee Juice portal. | + +## Related pages + +- [Outbox](./outbox.md) - L2 to L1 message passing +- [Data Structures](./data_structures.md) - Message and actor type definitions diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/index.md b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/index.md new file mode 100644 index 00000000000..31478762d80 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/index.md @@ -0,0 +1,150 @@ +--- +id: portals +title: L1 <--> L2 communication (Portals) +description: A conceptual introduction to Portals and how Aztec communicates with L1 (Ethereum) +keywords: [portals] +tags: [portals, protocol, ethereum] +sidebar_position: 12 +references: ["labs/noir-projects/aztec-nr/aztec/src/messaging.nr"] +--- + +# L1-L2 Communication (Portals) + +import Image from "@theme/IdealImage"; + +In Aztec, _portals_ facilitate communication between L1 and L2. Unlike typical L2 solutions that rely on synchronous communication, Aztec's private-by-design architecture and the way transactions are processed (kernel proofs built on historical data) make direct calls between L1 and L2 impossible while maintaining privacy. Portals solve this by acting as bridges for asynchronous message passing, transmitting messages from public functions in L1 to private functions in L2 and vice versa. + +## Objective + +The goal is to set up a minimal-complexity mechanism, that will allow a base-layer (L1) and the Aztec Network (L2) to communicate arbitrary messages such that: + +- L2 functions can `call` L1 functions. +- L1 functions can `call` L2 functions. +- Messages have minimal impact on rollup block size. + +## High Level Overview + +This document will contain communication abstractions that we use to support interaction between _private_ functions, _public_ functions and Layer 1 portal contracts. + +Fundamental restrictions for Aztec: + +- L1 and L2 have very different execution environments. Operations that are cheap on L1 are often expensive on L2 and vice versa. For example, `keccak256` is cheap on L1 but very expensive on L2. +- _Private_ function calls are fully "prepared" and proven by the user, which provides the kernel proof along with commitments and nullifiers to the sequencer. +- _Public_ functions altering public state (updatable storage) must be executed at the current "head" of the chain, which only the sequencer can ensure, so these must be executed separately to the _private_ functions. +- _Private_ and _public_ functions within Aztec are therefore ordered such that _private_ functions are executed first, then _public_ functions. +- Messages are consumables, and can only be consumed by the recipient. See [Message Boxes](#message-boxes) for more information. + +With the aforementioned restrictions taken into account, cross-chain messages can be operated in a similar manner to when _public_ functions must transmit information to _private_ functions. In such a scenario, a "message" is created and conveyed to the recipient for future use. It is worth noting that any call made between different domains (_private, public, cross-chain_) is unilateral in nature. In other words, the caller is unaware of the outcome of the initiated call until told when some later rollup is executed (if at all). This can be regarded as message passing, providing us with a consistent mental model across all domains, which is convenient. + +As an illustration, suppose a private function adds a cross-chain call. In such a case, the private function would not have knowledge of the result of the cross-chain call within the same rollup (since it has yet to be executed). + +Similarly to the ordering of private and public functions, we can also reap the benefits of intentionally ordering messages between L1 and L2. When a message is sent from L1 to L2, it has been "emitted" by an action in the past (an L1 interaction), allowing us to add it to the list of consumables at the "beginning" of the block execution. This practical approach means that a message could be consumed in the same block it is included. In a sophisticated setup, rollup $n$ could send an L2 to L1 message that is then consumed on L1, and the response is added already in $n+1$. However, messages going from L2 to L1 will be added as they are emitted. + +:::info +Because everything is unilateral and async, application developers must explicitly handle failure cases so users can gracefully recover. Token bridges are a prime example: it would be very inconvenient if funds are locked on one domain but never minted or unlocked on the other. +::: + +## Components + +### Portal + +A "portal" refers to the part of an application residing on L1, which is associated with a particular L2 address (the confidential part of the application). It could be a contract or even an EOA on L1. + +### Message Boxes + +In a logical sense, a Message Box functions as a one-way message passing mechanism with two ends, one residing on each side of the divide, i.e., one component on L1 and another on L2. Essentially, these boxes are utilized to transmit messages between L1 and L2 via the rollup contract. The boxes can be envisaged as multi-sets that enable the same message to be inserted numerous times, a feature that is necessary to accommodate scenarios where, for instance, "deposit 10 eth to A" is required multiple times. The diagram below provides a detailed illustration of how one can perceive a message box in a logical context. + +<Image img={require("@site/static/img/com-abs-5.png")} /> + +- Here, a `sender` will insert a message into the `pending` set, the specific constraints of the actions depend on the implementation domain, but for now, say that anyone can insert into the pending set. +- At some point, a rollup will be executed, in this step messages are "moved" from pending on Domain A, to ready on Domain B. Note that consuming the message is "pulling & deleting" (or nullifying). The action is atomic, so a message that is consumed from the pending set MUST be added to the ready set, or the state transition should fail. A further constraint is that the `sender` and `recipient` version fields must match the version of their respective inbox/outbox contracts. +- When the message has been added to the ready set, the `recipient` can consume the message as part of a function call. + +A difference when compared to other cross-chain setups, is that Aztec is "pulling" messages, and that the message doesn't need to be calldata for a function call. For other rollups, execution is happening FROM the "message bridge", which then calls the L1 contract. For Aztec, you call the L1 contract, and it should then consume messages from the message box. + +Why pull instead of push? Privacy. Pushing would require full calldata, which would publicly expose inputs to private functions since L1 → L2 transaction calldata is committed on L1. + +By instead pulling, we can have the "message" be something that is derived from the arguments instead. This way, a private function to perform second half of a deposit, leaks the "value" deposited and "who" made the deposit (as this is done on L1), but the new owner can be hidden on L2. + +To support messages in both directions we require two of these message boxes (one in each direction). However, due to the limitations of each domain, the message box for sending messages into the rollup and sending messages out are not fully symmetrical. In reality, the setup looks closer to the following: + +<Image img={require("@site/static/img/com-abs-6.png")} /> + +:::info +The L2 -> L1 pending messages set only exist logically, as it is practically unnecessary. For anything to happen to the L2 state (e.g., update the pending messages), the state will be updated on L1, meaning that we could just as well insert the messages directly into the ready set. +::: + +### Rollup Contract + +The rollup contract has a few very important responsibilities. The contract must keep track of the _L2 rollup state root_, perform _state transitions_ and ensure that the data is available for anyone else to synchronize to the current state. + +To ensure that _state transitions_ are performed correctly, the contract will derive public inputs for the **rollup circuit** based on the input data, and then use a _verifier_ contract to validate that inputs correctly transition the current state to the next. All data needed for the public inputs to the circuit must be from the rollup block, ensuring that the block is available. For a valid proof, the _rollup state root_ is updated and it will emit an _event_ to make it easy for anyone to find the data. + +As part of _state transitions_ where cross-chain messages are included, the contract must "move" messages along the way, e.g., from "pending" to "ready". + +### Kernel Circuit + +For L2 to L1 messages, the kernel circuit's public inputs contain a dynamic array of messages, limited to `MAX_L2_TO_L1_MSGS_PER_TX` to ensure transactions can always be included. The circuit scopes each message to the contract address that emitted it, ensuring the sender cannot be spoofed. + +When consuming L1 to L2 messages, user contracts call `process_l1_to_l2_message()` which verifies the message exists in the L1 to L2 message tree and creates a nullifier to prevent double-consumption. The kernel circuit accumulates these nullifiers in its public inputs. + +### Rollup Circuit + +The rollup circuit must ensure that, provided two states $S$ and $S'$ and the rollup block $B$, applying $B$ to $S$ using the transition function must give us $S'$, e.g., $T(S, B) \mapsto S'$. If this is not the case, the constraints are not satisfied. + +For cross-chain messages, this means inserting and nullifying L1 → L2 messages in the trees and publishing L2 → L1 messages on chain. + +### Messages + +While a message could theoretically be arbitrarily long, we want to limit the cost of the insertion on L1 as much as possible. Therefore, we allow the users to send 32 bytes of "content" between L1 and L2. If 32 suffices, no packing required. If the 32 is too "small" for the message directly, the sender should simply pass along a `sha256(content)` instead of the content directly (note that this hash should fit in a field element which is ~254 bits. More info on this below). The content can then either be emitted as an event on L2 or kept by the sender, who should then be the only entity that can "unpack" the message. +In this manner, there is some way to "unpack" the content on the receiving domain. + +The message that is passed along requires the `sender/recipient` pair to be communicated as well (we need to know who should receive the message and be able to check). By having the pending messages be a contract on L1, we can ensure that the `sender = msg.sender` and let only `content` and `recipient` be provided by the caller. We only store the commitment (`sha256(LxToLyMsg)`) on chain or in the trees, so we only need to update a single storage slot per message. + +See the [Data Structures](./data_structures.md) page for the full message structure definitions (`L1Actor`, `L2Actor`, `L1ToL2Msg`, `L2ToL1Msg`). + +:::info +The `bytes32` elements for `content` and `secretHash` hold values that must fit in a field element (~ 254 bits). +::: + +:::info +The nullifier computation should include the index of the message in the message tree to ensure that it is possible to send duplicate messages (e.g., 2 x deposit of 500 dai to the same account). + +To make it possible to hide when a specific message is consumed, the `L1ToL2Msg` is extended with a `secretHash` field, where the `secretPreimage` is used as part of the nullifier computation. This way, it is not possible for someone just seeing the `L1ToL2Msg` on L1 to know when it is consumed on L2. +::: + +## Combined Architecture + +The following diagram shows the overall architecture, combining the earlier sections. + +<Image img={require("@site/static/img/com-abs-7.png")} /> + +## Using the L1 contract interfaces in your own project + +Portal contracts import Aztec's L1 interfaces (`IRegistry`, `IInbox`, `IOutbox`, `IRollup`, and supporting libraries). These ship in the [`@aztec-foundation/l1-artifacts`](https://www.npmjs.com/package/@aztec-foundation/l1-artifacts) npm package as a self-contained Foundry project under `l1-contracts/`, versioned to match each Aztec release: + +```bash +npm install @aztec-foundation/l1-artifacts@<aztec-version> +``` + +In a Foundry project, add remappings so `@aztec/*` imports (and the bundled OpenZeppelin copy) resolve into the package: + +```toml +# foundry.toml +remappings = [ + "@aztec/=node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/", + "@oz/=node_modules/@aztec-foundation/l1-artifacts/l1-contracts/lib/openzeppelin-contracts/contracts/", + "@aztec-blob-lib/=node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/rollup/" +] +``` + +Then import interfaces as `@aztec/core/interfaces/IRollup.sol`, `@aztec/governance/interfaces/IRegistry.sol`, and so on. Note that `forge install` cannot fetch the package: it only clones whole git repositories, and the L1 contract sources include generated files that exist only in built distributions, so npm is the supported channel. + +In a Hardhat project, import with full package paths (`@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/IRollup.sol`), or configure equivalent aliases pointing at `node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src`. + +## See also + +- [Communicating Cross-Chain](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) - Practical guide with code examples for L1-L2 messaging +- [Data Structures](./data_structures.md) - Message and actor type definitions +- [Inbox](./inbox.md) - L1 contract for sending messages to L2 +- [Outbox](./outbox.md) - L1 contract for consuming messages from L2 diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/outbox.md b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/outbox.md new file mode 100644 index 00000000000..4cf579946ce --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/outbox.md @@ -0,0 +1,89 @@ +--- +title: Outbox +description: Learn about the outbox mechanism in Aztec portals for sending messages to L1. +tags: [portals, contracts] +--- + +The `Outbox` is a contract deployed on L1 that handles message passing from L2 to L1. Portal contracts call `consume()` to receive and process messages that were sent from L2 contracts. The Rollup contract inserts message roots via `insert()` as proofs land. A proof can cover a prefix of an epoch's checkpoints (a partial proof), so an epoch can have several roots, one per number of checkpoints covered. + +**Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/#include_foundation_version/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/#include_foundation_version/l1-contracts/src/core/messagebridge/Outbox.sol). + +## `insert()` + +Inserts the root of a merkle tree containing all of the L2 to L1 messages in an epoch, after a proof covering the first `_numCheckpointsInEpoch` checkpoints of that epoch lands. This function is only callable by the Rollup contract. + +#include_code outbox_insert docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity + +| Name | Type | Description | +| ------------------------ | --------- | ------------------------------------------------------------------------------------------------------------ | +| `_epoch` | `Epoch` | The epoch in which the L2 to L1 messages reside | +| `_numCheckpointsInEpoch` | `uint256` | The number of checkpoints the inserting proof covered in this epoch (in `[1, MAX_CHECKPOINTS_PER_EPOCH]`) | +| `_root` | `bytes32` | The merkle root of the tree where all the L2 to L1 messages are leaves | + +### Edge cases + +- Will revert with `Outbox__Unauthorized()` if `msg.sender != ROLLUP_CONTRACT`. + +## `consume()` + +Allows a recipient to consume a message from the `Outbox`. + +#include_code outbox_consume docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity + +| Name | Type | Description | +| ------------------------ | ----------- | ---------------------------------------------------------------------------------------------------- | +| `_message` | `L2ToL1Msg` | The L2 to L1 message to consume | +| `_epoch` | `Epoch` | The epoch that contains the message to consume | +| `_numCheckpointsInEpoch` | `uint256` | The number of checkpoints in the partial proof whose root this consume verifies against | +| `_leafIndex` | `uint256` | The index inside the merkle tree where the message is located | +| `_path` | `bytes32[]` | The sibling path used to prove inclusion of the message (built against the tree for that proof depth) | + +### Edge cases + +- Will revert with `Outbox__PathTooLong()` if the path length is >= 256. +- Will revert with `Outbox__LeafIndexOutOfBounds(uint256 leafIndex, uint256 pathLength)` if the leaf index exceeds the tree capacity for the given path length. +- Will revert with `Outbox__VersionMismatch(uint256 expected, uint256 actual)` if the message version does not match the Outbox version. +- Will revert with `Outbox__InvalidRecipient(address expected, address actual)` if `msg.sender != _message.recipient.actor`. +- Will revert with `Outbox__InvalidChainId()` if `block.chainid != _message.recipient.chainId`. +- Will revert with `Outbox__NothingToConsumeAtEpoch(Epoch epoch)` if the root for the epoch has not been set. +- Will revert with `Outbox__AlreadyNullified(Epoch epoch, uint256 leafIndex)` if the message has already been consumed. +- Will revert with `MerkleLib__InvalidIndexForPathLength()` if the leaf index has bits set beyond the tree height. +- Will revert with `MerkleLib__InvalidRoot(bytes32 expected, bytes32 actual, bytes32 leaf, uint256 leafIndex)` if the merkle proof verification fails. + +## `hasMessageBeenConsumedAtEpoch()` + +Checks if an L2 to L1 message in a specific epoch has been consumed. + +#include_code outbox_has_message_been_consumed_at_epoch_and_index docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity + +| Name | Type | Description | +| --------- | --------- | ------------------------------------------------------------ | +| `_epoch` | `Epoch` | The epoch that contains the message to check | +| `_leafId` | `uint256` | The unique id of the message leaf | + +### Edge cases + +- This function does not throw. Out-of-bounds access is considered valid, but will always return false. + +## `getRootData()` + +Returns the merkle root for a given epoch and partial-proof depth. Returns `bytes32(0)` if no proof covering that number of checkpoints has been inserted. + +```solidity +function getRootData(Epoch _epoch, uint256 _numCheckpointsInEpoch) external view returns (bytes32); +``` + +| Name | Type | Description | +| ------------------------ | --------- | -------------------------------------------------------------------- | +| `_epoch` | `Epoch` | The epoch to fetch the root data for | +| `_numCheckpointsInEpoch` | `uint256` | The number of checkpoints in the partial proof whose root to fetch | + +**Returns**: The merkle root of the L2 to L1 message tree for that epoch and proof depth, or `bytes32(0)` if not proven. + +There is also `getRoots(Epoch _epoch)`, which returns every root stored for an epoch: slot `i` of the returned array holds the root for `numCheckpointsInEpoch = i + 1`. + +## Related pages + +- [Inbox](./inbox.md) - L1 to L2 message passing +- [Data Structures](./data_structures.md) - Message struct definitions +- [L1-L2 Communication (Portals)](./index.md) - Overview of cross-chain messaging diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/registry.md b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/registry.md new file mode 100644 index 00000000000..9203e29dc22 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/registry.md @@ -0,0 +1,54 @@ +--- +title: Registry +description: Learn about the portal registry and how it manages L1-L2 contract mappings. +tags: [portals, contracts] +--- + +The Registry is a contract deployed on L1 that tracks canonical and historical rollup instances. It allows you to query the current rollup contract and look up prior deployments by version. + +**Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/#include_foundation_version/l1-contracts/src/governance/interfaces/IRegistry.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/#include_foundation_version/l1-contracts/src/governance/Registry.sol). + +## `numberOfVersions()` + +Retrieves the number of versions that have been deployed. + +#include_code registry_number_of_versions docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/governance/interfaces/IRegistry.sol solidity + +| Name | Description | +| ----------- | ---------------------------------------------- | +| ReturnValue | The number of versions that have been deployed | + +## `getCanonicalRollup()` + +Retrieves the current rollup contract. + +#include_code registry_get_canonical_rollup docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/governance/interfaces/IRegistry.sol solidity + +| Name | Description | +| ----------- | ------------------ | +| ReturnValue | The current rollup | + +## `getRollup(uint256 _version)` + +Retrieves the rollup contract for a specific version. + +#include_code registry_get_rollup docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/governance/interfaces/IRegistry.sol solidity + +| Name | Description | +| ----------- | ------------------------------------ | +| `_version` | The version identifier of the rollup | +| ReturnValue | The rollup for the specified version | + +## Other view functions + +| Function | Returns | Description | +| ------------------------ | -------------------- | ------------------------------------------------ | +| `getVersion(uint256)` | `uint256` | Returns the version number stored at the given index in the historical versions list | +| `getGovernance()` | `address` | Returns the governance contract address (owner) | +| `getRewardDistributor()` | `IRewardDistributor` | Returns the reward distributor contract | + +## Related pages + +- [Inbox](./inbox.md) - L1 to L2 message passing +- [Outbox](./outbox.md) - L2 to L1 message passing +- [L1-L2 Communication (Portals)](./index.md) - Overview of cross-chain messaging diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/fees.md b/docs/lines/v6/docs-developers/docs/foundational-topics/fees.md new file mode 100644 index 00000000000..52ffe368230 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/fees.md @@ -0,0 +1,145 @@ +--- +title: Fees +sidebar_position: 4 +tags: [fees, mana, gas] +description: Understand Aztec's fee system including mana-based transaction pricing, Aztec token and Fee Juice payments, and how L1 and L2 costs are transparently calculated for users. +references: + [ + "labs/yarn-project/stdlib/src/gas/gas_settings.ts", + "labs/yarn-project/aztec.js/src/ethereum/portal_manager.ts", + "labs/yarn-project/aztec.js/src/fee/fee_juice_payment_method_with_claim.ts", + ] +--- + +import { Why_Fees } from '@site/src/components/Snippets/general_snippets'; + +<Why_Fees /> + +In a nutshell, the pricing of transactions transparently accounts for: + +- L1 costs, including L1 execution of a block, and data availability via blobs, +- L2 node operating costs, including proving + +This is achieved through multiple variables and calculations. + +## Terminology + +Familiar terms from Ethereum mainnet as referred to on the Aztec network: + +| Ethereum Mainnet | Aztec | Description | +| ---------------- | ------------------ | -------------------------------------------------------------- | +| gas | mana | Unit measuring computational effort for transaction operations | +| fee per gas | Fee Juice per mana | Price per unit of mana | +| fee (wei) | Fee Juice | Total fee paid for a transaction | + +## What is mana? + +Mana is Aztec's unit of computational effort, equivalent to gas on Ethereum. Every transaction consumes mana based on the operations it performs. + +Mana has two dimensions: + +- **Data Availability (DA) mana**: Cost of publishing transaction data to the data availability layer +- **L2 mana**: Cost of executing the transaction on Aztec + +The total transaction fee is calculated as: + +``` +fee = (daMana × feePerDaMana) + (l2Mana × feePerL2Mana) +``` + +:::note +The SDK and protocol code use "gas" in variable names (e.g., `daGas`, `l2Gas`, `feePerDaGas`, `feePerL2Gas`) rather than "mana". When reading code, `Gas` and mana refer to the same concept. +::: + +## What is Fee Juice? + +Fee Juice is the native fee token on Aztec, used to pay for transaction fees. It is bridged Aztec tokens from Ethereum and is **non-transferable** on Aztec - it can only be used to pay fees, not sent between accounts. + +Aztec borrows ideas from EIP-1559, including congestion multipliers and the ability to specify base and priority fees per mana. + +## Factors affecting fees + +Other fields used in mana and fee calculations are determined in various ways: + +- hard-coded constants (eg congestion update fraction) +- values assumed constant (eg L1 gas cost of publishing a block, blobs per block) +- informed from previous block header and/or L1 rollup contract (eg base fee per mana) +- informed via an oracle (eg wei per mana) + +Most constants are defined by the protocol, while others are part of the rollup contract on L1. + +### User-defined settings + +Users can define the following settings as part of a transaction: + +import { Gas_Settings_Components, Gas_Settings, Tx_Teardown_Phase } from '@site/src/components/Snippets/general_snippets'; + +#include_code gas_settings_vars labs/yarn-project/stdlib/src/gas/gas_settings.ts javascript + +<Gas_Settings_Components /> + +## Fee payment + +A fee payer obtains Fee Juice by bridging Aztec tokens from Ethereum. The fee payer can be the account itself or a fee-paying contract (FPC), which functions similarly to a paymaster on Ethereum. On Aztec, Fee Juice is held as a public balance, is non-transferable, and is only deducted by the protocol to pay for fees. + +### Bridging Fee Juice from Ethereum + +Fee Juice originates on Ethereum as an ERC-20 token. Bridging means depositing that token into the enshrined `FeeJuicePortal` contract on L1, then claiming the resulting balance on L2. Unlike user-deployed token portals, the `FeeJuicePortal` is part of the protocol's L1 deployment, but it uses the same [cross-chain messaging](../aztec-nr/framework-description/ethereum_aztec_messaging.md) mechanism available to any token; the [Token Bridge Tutorial](../tutorials/js_tutorials/token_bridge.md) describes portal contracts in general. + +Bridging happens in two steps: + +1. **Deposit on L1.** The depositor generates a random claim secret, approves the `FeeJuicePortal` to spend their tokens, and calls its deposit function with the recipient's Aztec address, the amount, and the hash of the claim secret. The portal locks the tokens and sends an L1-to-L2 message addressed to the `FeeJuice` protocol contract on Aztec. +2. **Claim on L2.** After the message becomes available on Aztec (about two L2 blocks after the deposit), the claimant presents the claim secret to the `FeeJuice` contract. It consumes the message, emitting a nullifier so the same deposit cannot be claimed twice, and credits the recipient's public Fee Juice balance. + +Only the hash of the claim secret appears on L1, and the recipient address is fixed at deposit time, so revealing the secret on L2 releases the funds only to the intended recipient. + +The bridge is one-way. There is no withdrawal path back to L1 for users: bridged Fee Juice can only be spent on fees, and tokens leave the L1 portal only when the rollup contract distributes collected fees to sequencers and provers. + +Claiming is itself an L2 transaction whose fee must be paid, which would leave a brand-new account stuck. The protocol resolves this by letting a transaction claim Fee Juice and spend it on that same transaction's fee: the claim runs in the transaction's non-revertible [setup phase](./transactions.md#setup-phase-non-revertible), so the credited balance is available to cover the fee even if the rest of the transaction reverts. This is how a new account can pay for its own deployment with bridged Fee Juice. The `FeeJuice` contract also offers a standalone claim for pre-funding an account that will pay fees later. + +In `aztec.js`, the `L1FeeJuicePortalManager` class handles the L1 side (token approval and deposit) and returns the claim details, and `FeeJuicePaymentMethodWithClaim` performs the claim and fee payment in a single transaction. See [Bridge Fee Juice from L1](../aztec-js/how_to_pay_fees.md#bridge-fee-juice-from-l1) for a code walkthrough. The `aztec-wallet` CLI's `bridge-fee-juice` command handles the L1 deposit step and prints the claim details for use in a later transaction. + +### Payment methods + +An account with Fee Juice can pay for its transactions directly. A new account can even pay for its own deployment transaction, provided Fee Juice was bridged to its address before deployment. + +Alternatively, accounts can use [fee-paying contracts (FPCs)](../aztec-js/how_to_pay_fees.md#use-fee-payment-contracts) to pay for transactions. An FPC holds its own Fee Juice balance to pay the protocol, and can accept other tokens from users in exchange. + +The **Sponsored FPC** pays fees unconditionally, enabling free transactions. It is available on testnet, devnet, and local network. On mainnet, ecosystem-deployed FPCs are the practical option: the built-in reference FPC contract does not work on mainnet alpha because custom token class IDs are not included in the default public setup allowlist. As an example, Nethermind's [Private Multi Asset FPC](https://github.com/NethermindEth/aztec-fpc) demonstrates one such design; it accepts multiple tokens and routes fee payments as private notes. + +### How FPCs work + +An FPC acts as a fee payer on the user's behalf. Simpler FPCs (like the Sponsored FPC) just call `set_as_fee_payer()` with no user payment at all. More sophisticated FPCs accept user tokens in exchange for paying Fee Juice. A common pattern for quote-based FPCs works as follows: + +1. **Quote.** The user requests a fee quote from the FPC operator, specifying the token they want to pay with and the estimated gas cost. The operator signs the quote, binding it to the user, asset, amounts, and an expiry. +2. **Authorization.** The user creates an [authentication witness](./advanced/authwit.md) authorizing the FPC to transfer tokens from their balance. This is the same authwit mechanism used for any delegated token transfer. +3. **Setup phase (non-revertible).** The FPC's entrypoint runs during the transaction's [setup phase](./transactions.md#setup-phase-non-revertible). It verifies the quote, collects the user's payment, declares itself as the fee payer via `set_as_fee_payer()`, and calls `end_setup()` to mark the boundary between non-revertible and revertible execution. Because this runs in the non-revertible phase, the payment is **irrevocably committed** before application logic executes: the user pays regardless of whether the app-logic phase reverts. +4. **App phase (revertible).** The user's actual transaction logic runs here. In the common fee-entrypoint flow the FPC is not involved in this phase, though some FPC designs (such as cold-start flows) perform additional app-phase work like claiming bridged tokens. + +:::note Setup-phase allowlist +Setup-phase execution is restricted by a protocol-level allowlist of permitted public function calls. Public token functions such as `transfer_in_public` and `_increase_public_balance` have been removed from the default allowlist; custom FPCs may only call protocol-contract setup functions (for example those on `AuthRegistry` and `FeeJuice`). See the [migration note](../resources/migration_notes.md#custom-token-fpcs-removed-from-default-public-setup-allowlist) for details. +::: + +Key properties for developers integrating with an FPC: + +- **Authwit scope.** The authwit authorizes a specific action hash (typically covering the transfer amount). A nonce can be included when otherwise-identical actions need to be distinguishable. The authwit is single-use, consumed by a nullifier onchain. +- **Token interface.** On networks that use the default setup allowlist, an FPC cannot call arbitrary public token functions during setup (see the callout above). Fee collection for quote-based FPCs therefore typically happens in the private domain: the user privately transfers an agreed amount to the FPC (or its operator) under the authwit from step 2, and the FPC separately declares itself the fee payer during setup using only protocol-contract calls. Any token that implements the standard Aztec token interface with authwit verification is compatible with this private-note pattern. +- **Gas estimation first.** The Fee Juice amount in the quote is typically derived from the transaction's gas estimate. Simulate and estimate gas _before_ requesting a quote, then pass the estimated cost to the FPC operator. +- **Quote expiry.** Quotes are time-bound and single-use. Fetch a fresh quote per transaction. +- **Cold-start variant.** Some FPCs offer a cold-start entrypoint where a brand-new account can bridge tokens from L1, claim them on L2, and pay the fee in one transaction, with no prior L2 balance or authwit needed, because the FPC itself claims and distributes the bridged tokens. The user still needs L1 tokens and ETH for the initial bridge transaction. + +Fee payments themselves can also be made private via a fully private FPC that holds Fee Juice internally and nominates itself as the fee payer during the setup phase, without revealing who initiated the transaction. See [Pay Fees Privately](../aztec-js/how_to_use_private_fee_juice.md) for how this pattern works and an example implementation. + +### Teardown phase + +<Tx_Teardown_Phase /> + +This enables FPCs to calculate the actual transaction cost and refund any overpayment to the user. Not all FPC designs use the teardown phase; some charge a fixed quoted amount with no refund, keeping unused Fee Juice in the FPC's balance for future transactions. + +### Operator rewards + +The calculated fee of a transaction is deducted from the fee payer (nominated account or fee-paying contract), then pooled together across transactions, blocks, and epochs. Once an epoch is proven, the total collected fees (minus any burnt congestion amount) are distributed to the provers and block proposers that contributed to the epoch. + +## Next steps + +For a guide on paying fees programmatically, including how to bridge Fee Juice from L1, see [How to Pay Fees](../aztec-js/how_to_pay_fees.md). diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/index.md b/docs/lines/v6/docs-developers/docs/foundational-topics/index.md new file mode 100644 index 00000000000..61da6b56f28 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/index.md @@ -0,0 +1,84 @@ +--- +title: Aztec Overview +sidebar_position: 0 +tags: [protocol] +description: Overview of Aztec, the decentralized Ethereum L2 for unstoppable private apps, with private and public state and execution. +--- + +import YouTubeEmbed from "@site/src/components/YouTubeEmbed"; + +This page outlines Aztec's fundamental technical concepts. It is recommended to read this before diving into building on Aztec. + +## What is Aztec? + +Aztec is a decentralized Layer 2 on Ethereum for building unstoppable private apps. It supports smart contracts with both private & public state and private & public execution: private functions run and are proven on the user's device, public functions run on the network, and you decide which is which. + +Prefer video? This explainer covers the core idea in under 90 seconds, and there are more [video lessons](../resources/video_lessons.mdx) available. + +<YouTubeEmbed videoId="urcBvo2QJp0" title="What is Aztec: Explained in Under 90 Seconds" /> + +<Diagram name="aztec-overview" alt="Private side: the PXE in the user's wallet executes and proves private functions. Public side: decentralized sequencers order transactions into rollup blocks and publish block data to the data availability layer." /> + +## High level view + +<Diagram name="transaction-lifecycle" alt="Transaction lifecycle: a user calls aztec.js, the PXE simulates and proves privately, an Aztec node sequences the transaction, the prover network proves it, and the rollup contract on Ethereum verifies it." /> + +1. A user interacts with Aztec through Aztec.js (like web3js or ethersjs) +2. Private functions are executed in the PXE, which is client-side +3. Proofs and tree updates are sent to the Public VM (running on an Aztec node) +4. Public functions are executed in the Public VM +5. The Public VM rolls up the transactions that include private and public state updates into blocks +6. The block data and proof of a correct state transition are submitted to Ethereum for verification + +## Private and public execution + +Private functions are executed client side, on user devices to maintain maximum privacy. Public functions are executed by a remote network of nodes, similar to other blockchains. These distinct execution environments create a directional execution flow for a single transaction--a transaction begins in the private context on the user's device then moves to the public network. This means that private functions executed by a transaction can enqueue public functions to be executed later in the transaction life cycle, but public functions cannot call private functions. + +### Private Execution Environment (PXE) + +Private functions are executed on the user's device in the Private Execution Environment (PXE, pronounced 'pixie'), then it generates proofs for onchain verification. It is a client-side library for execution and proof-generation of private operations. It holds keys, notes, and generates proofs. It is included in aztec.js, a TypeScript library, and can be run within Node or the browser. + +Note: It is easy for private functions to be written in a detrimentally unoptimized way, because many intuitions of regular program execution do not apply to proving. For more about writing performant private functions in Noir, see [this page](https://noir-lang.org/docs/guides/thinking_in_circuits) of the Noir documentation. + +### Aztec Virtual Machine (AVM) + +Public functions are executed by the Aztec Virtual Machine (AVM), which is conceptually similar to the Ethereum Virtual Machine (EVM). As such, writing efficient public functions follow the same intuition as gas-efficient solidity contracts. + +The PXE is unaware of the Public VM. And the Public VM is unaware of the PXE. They are completely separate execution environments. This means: + +- The PXE and the Public VM cannot directly communicate with each other +- Private transactions in the PXE are executed first, followed by public transactions + +## Private and public state + +Private state works with UTXOs, which are chunks of data that we call notes. To keep things private, notes are stored in an [append-only UTXO tree](./advanced/storage/indexed_merkle_tree.mdx), and a nullifier is created when notes are invalidated (aka deleted). Nullifiers are stored in their own [nullifier tree](./advanced/storage/indexed_merkle_tree.mdx). + +Public state works similarly to other chains like Ethereum, behaving like a public ledger. Public data is stored in a public data tree. + +<Diagram name="public-private-state" alt="Public state is a data tree whose leaves are updated in place. Private state is a UTXO model: spending a note emits a nullifier and creates new notes." /> + +Aztec [smart contract](../aztec-nr/framework-description/contract_structure.md) developers should keep in mind that different data types are used when manipulating private or public state. Working with private state is creating commitments and nullifiers to state, whereas working with public state is directly updating state. + +## Accounts and keys + +### Account abstraction + +Every account in Aztec is a smart contract (account abstraction). This allows implementing different schemes for authorizing transactions, nonce management, and fee payments. + +Developers can write their own account contract to define the rules by which user transactions are authorized and paid for, as well as how user keys are managed. + +Learn more about account contracts [here](./accounts/index.md). + +### Key pairs + +Each account in Aztec is backed by 3 key pairs: + +- A **nullifier key pair** used for note nullifier computation +- A **incoming viewing key pair** used to encrypt a note for the recipient +- A **outgoing viewing key pair** used to encrypt a note for the sender + +As Aztec has native account abstraction, accounts do not automatically have a signing key pair to authenticate transactions. This is up to the account contract developer to implement. + +## Noir + +Noir is a zero-knowledge domain specific language used for writing smart contracts for the Aztec network. It is also possible to write circuits with Noir that can be verified on or offchain. For more in-depth docs into the features of Noir, go to the [Noir documentation](https://noir-lang.org/). diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/pxe/execution_hooks.md b/docs/lines/v6/docs-developers/docs/foundational-topics/pxe/execution_hooks.md new file mode 100644 index 00000000000..a7afa87eb2a --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/pxe/execution_hooks.md @@ -0,0 +1,137 @@ +--- +title: Execution hooks +sidebar_position: 3 +tags: [pxe, wallets] +description: How wallets use PXE execution hooks to apply custom policies while simulating and proving transactions. +references: ["labs/yarn-project/pxe/src/hooks/*", "labs/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr", "labs/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/*"] +--- + +Execution hooks are callbacks that the PXE invokes while simulating and proving transactions when an operation needs a decision from the wallet. They let the wallet apply its own policies before execution proceeds, such as prompting the user, consulting a dynamic allowlist, or inspecting call arguments. All hooks are optional; when a hook is absent, the PXE applies a conservative default: for example it avoids privacy leaks (such as revealing a message's recipient onchain) unless specifically told otherwise. + +## Configuring hooks + +Pass a `hooks` object when creating the PXE: + +```typescript +import { createPXE } from "@aztec-labs/pxe/server"; + +const pxe = await createPXE(node, config, { + hooks: { + // Allow calls to a known helper contract, deny everything else. + authorizeUtilityCall: async (request) => { + return request.target.equals(trustedHelper) + ? { authorized: true } + : { authorized: false, reason: "Unknown target" }; + }, + // When no onchain handshake is registered for the recipient, fall back to a non-interactive handshake. + resolveTaggingSecretStrategy: async () => ({ type: "non-interactive-handshake" }), + }, +}); +``` + +## `authorizeUtilityCall` + +Called whenever a utility function makes a cross-contract call. A call made by a malicious contract could leak private information, so the hook lets the wallet decide, per call, whether to allow it. A static allowlist would not work here because neither the app nor the wallet can predict ahead of time which contracts will be invoked during execution: permission must be asked after execution has begun. Calls to standard contracts (such as the HandshakeRegistry, which is queried during every contract's sync) bypass this hook and are always authorized. + +Unlike [authentication witnesses (authwits)](../../aztec-js/how_to_use_authwit.md), the hook is invoked live, while execution is underway. Authwits can be recorded during simulation and signed once at the end, but the PXE cannot predict what a utility call would return, so it must ask before continuing. Most of the time the wallet should answer on its own, for example against a list of audited or previously trusted contracts, to avoid interrupting execution multiple times asking the user for confirmation. + +### Deciding what to authorize + +Private state is siloed per contract: a utility function runs on your device with access to its own contract's private state, and nothing else. Reading your own balance through a token contract's utility function is fine, and the hook never fires, because no contract boundary is crossed. The risk appears only when one contract's utility function calls into a *different* contract, because that call can reach private state the caller could not read on its own. + +Consider a single cross-contract operation, reading your token balance, made by two different callers. When a DeFi router calls the token's balance utility to quote you a swap, that is a legitimate cross-contract read, and you want it allowed. When an unknown, possibly malicious contract makes the very same call to snoop your balance, you want it denied. The exposed data is identical in both cases; the only thing that differs is *who is making the call*, which is exactly the decision the hook delegates to the wallet. + +The wallet makes that decision by inspecting the request, which identifies the caller and target by both address and contract class ID, to judge whether the call is safe to authorize. + +### In Noir tests + +When testing cross-contract utility calls in Noir using `TestEnvironment`, use `with_authorized_utility_call_targets` on your call options: + +```rust +// For private calls: +env.call_private_opts( + account, + CallPrivateOptions::new().with_authorized_utility_call_targets([target_address]), + MyContract::at(caller).some_private_fn(), +); + +// For private view calls: +env.view_private_opts( + account, + ViewPrivateOptions::new().with_authorized_utility_call_targets([target_address]), + MyContract::at(caller).some_view_fn(), +); + +// For utility calls: +env.execute_utility_opts( + ExecuteUtilityOptions::new().with_authorized_utility_call_targets([target_address]), + MyContract::at(caller).some_utility_fn(), +); +``` + +### In production + +Pass an `authorizeUtilityCall` hook when [creating the PXE](#configuring-hooks). It receives a `UtilityCallAuthorizationRequest` with the caller and target addresses, their contract class IDs, the function selector, the function name, the arguments, and the caller context (`'private'`, `'private view'`, or `'utility'`). Return `{ authorized: true }` to allow the call, or `{ authorized: false, reason: '...' }` to deny it with a message. + +When the hook is absent, cross-contract utility calls are denied. See [Cross-contract utility call denied](../../aztec-nr/debugging.md#cross-contract-utility-call-denied) for the resulting error. + +## `resolveTaggingSecretStrategy` + +Called as a fallback for message delivery: a registered onchain handshake's secret is reused directly, so this hook only fires when the sender-recipient pair has none yet. The wallet returns a concrete `TaggingSecretStrategy` (and any material the chosen derivation needs); see [Tagging secret strategy](../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy) for the variants, the trade-offs, and the defaults in each environment. + +For an unconstrained self-send (the recipient is one of the wallet's own accounts), the PXE always uses an [address-derived shared secret](../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy) regardless of what the hook returns: both sides' keys are local, so no handshake is needed. + +### In Noir tests + +When testing in Noir, leaving the strategy unset makes `TestEnvironment` fall back to the bare PXE default. Set a strategy +when creating the environment to exercise a specific one; it affects message delivery in private executions that use the +default wallet strategy hook. Use `with_default_tag_secret_strategy` to configure the strategy for a specific delivery +mode: + +```rust +let env = TestEnvironment::new_opts( + TestEnvironmentOptions::new().with_default_tag_secret_strategy( + MessageDelivery::onchain_unconstrained(), + TaggingSecretStrategy::non_interactive_handshake(), + ), +); +``` + +Use `with_default_tag_secret_strategy_all_modes` only when the same strategy should apply to both constrained and +unconstrained delivery. Contract-fixed delivery derivations bypass this default strategy. + +### In production + +Pass a `resolveTaggingSecretStrategy` hook when [creating the PXE](#configuring-hooks). It receives a `TaggingSecretStrategyRequest` with the executing contract's address and the message's sender, recipient, and delivery mode (`'constrained'` or `'unconstrained'`), so a wallet can apply per-application or per-recipient policies, or surface the decision to the user, instead of returning a fixed value. + +When the hook is absent, the PXE applies a default: both delivery modes use a [non-interactive handshake](../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy) so the recipient can discover the message without prior coordination. + +Returning `{ type: 'interactive-handshake' }` makes the handshake registry request the recipient's signed authorization through the [`resolveCustomRequest`](#resolvecustomrequest) hook (see the [example](#example-interactive-handshakes) below), so a wallet should only choose it when that request can be served. + +## `resolveCustomRequest` + +A general-purpose hook for *custom*, caller-defined requests. A contract reaches for it when it needs something it cannot get on its own: not from its local notes, not from the protocol's existing oracles. The contract gives the request a `kind` and an opaque `payload`, and the wallet returns an opaque response. Because the request is caller-defined, the wallet decides per `kind` how to answer, whether by reading state it holds, contacting another party, or fetching offchain data. + +### In production + +Pass a `resolveCustomRequest` hook when [creating the PXE](#configuring-hooks). It receives a `CustomRequest` with the issuing contract's address and class ID, the request `kind`, and the opaque `payload`, and returns the response. Because any contract can issue a request, the hook should check both the `kind` and the issuing contract before answering, dispatching on `kind` to the matching resolver. + +When the hook is absent, the request cannot be served and simulation fails. + +### Example: interactive handshakes + +The `HandshakeRegistry`'s `interactive_handshake` uses this hook to obtain the recipient's signed authorization. The payload carries what the signer needs to decide: who the recipient is, the handshake being authorized, and the chain context, but never the sender. The response carries what the registry needs to verify the recipient's signature in-circuit. + +## `resolveExpirationAndPadding` + +Every transaction publishes an expiration timestamp and its side effects (note hashes, nullifiers and private logs). Both can reveal which contracts a transaction called privately: a contract with a short update delay or a pending upgrade lowers the expiry to a value tied to it, and an unusual number of side effects stands out among other transactions. This hook lets the wallet choose the published expiry and pad the side effects with random values, to place the transaction in a larger privacy set. Padding costs gas like real side effects. + +### In production + +Pass a `resolveExpirationAndPadding` hook when [creating the PXE](#configuring-hooks). It receives an `ExpirationAndPaddingRequest` with the anchor block timestamp, the latest expiry the transaction allows, and its side effect counts after squashing. For a transaction with public calls, the counts are split into the non-revertible and revertible phases. The hook returns the expiry to publish and the counts to pad each side effect array up to, in the same shape as the request's counts. + +The PXE rejects a result that the kernel circuits cannot publish: an expiry later than the request's `maxExpirationTimestamp`, or counts below the request's `minSideEffects` or above the per-transaction maximum (such as `MAX_NOTE_HASHES_PER_TX`). + +The hook runs both when simulating and when proving a transaction, so that the gas estimated by simulation includes the padding. + +When the hook is absent, the PXE does not pad, and publishes the expiry rounded down by `roundDownTxExpirationTimestamp`. A hook that only changes padding can return `roundDownTxExpirationTimestamp(request)` to keep that expiry. diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/pxe/index.md b/docs/lines/v6/docs-developers/docs/foundational-topics/pxe/index.md new file mode 100644 index 00000000000..981ecb4a243 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/pxe/index.md @@ -0,0 +1,122 @@ +--- +title: Private Execution Environment (PXE) +sidebar_position: 8 +tags: [PXE] +description: Explore the PXE, a client-side library that handles private function execution, proof generation, secret management, and transaction orchestration in Aztec. +keywords: [pxe, private execution environment] +importance: 1 +references: ["labs/yarn-project/pxe/src/*", "labs/yarn-project/key-store/src/*", "labs/noir-projects/aztec-nr/aztec/src/oracle/version.nr"] +--- + +This page describes the Private Execution Environment (PXE, pronounced "pixie"), a client-side library for the execution of private operations. It is a TypeScript library that can be run within Node.js, inside wallet software or a browser. + +The PXE generates proofs of private function execution, and sends these proofs along with public function execution requests to the sequencer. Private inputs never leave the client-side PXE. + +The PXE is responsible for: + +- storing secrets (e.g. encryption keys, notes, tagging secrets for note discovery) and exposing an interface for safely accessing them +- orchestrating private function (circuit) execution and proof generation, including implementing [oracles](../../aztec-nr/framework-description/advanced/protocol_oracles.md) needed for transaction execution +- syncing users' relevant network state, obtained from an Aztec node +- safely handling multiple accounts with siloed data and permissions + +One PXE can handle data and secrets for multiple accounts, while also providing isolation between them as required. + +## System architecture + +```mermaid +flowchart TB + User --Interacts with--> Wallet + Wallet --Prompts--> User + subgraph Browser + Dapp + end + Dapp --Calls (requires auth)--> Wallet + subgraph Client-side + Wallet --Scoped--> PXE + PXE --Execute/Prove--> Circuits + Circuits --Oracle--> PXE + end + PXE --Queries world-state--> Node + Wallet --Track tx state--> Node +``` + +:::note[Privacy consideration] +When the PXE queries the node for world state (e.g., to check if a nullifier exists), the node learns which data the user is interested in. This is a known tradeoff. Users can mitigate this by running their own node. +::: + +## Components + +### Contract Function Simulator + +An application prompts the user's PXE to execute a transaction (e.g. execute function X with arguments Y from account Z). The application or wallet may handle gas estimation. + +The contract function simulator handles execution of smart contract functions by simulating transactions. It generates the required data and inputs for these functions, including partial witnesses and public inputs. + +By default, the simulator runs in a [kernelless mode](./kernelless_simulations.md): it executes the private bytecode and computes the values the private kernels would have produced in TypeScript, instead of running the kernel circuits themselves. This is faster than a full simulation and lets the wallet capture authentication witness requests as offchain effects without prompting the user to sign during simulation. + +### Proof Generation + +After simulation, the wallet calls `proveTx` on the PXE with all of the data generated during simulation and any [authentication witnesses](../advanced/authwit.md) (for allowing contracts to act on behalf of the user's account contract). + +Once proven, the wallet sends the transaction to the network and sends the transaction hash back to the application. + +### Database + +The PXE database stores various types of data locally: + +- **Notes**: Data representing users' private state. Notes are stored onchain as encrypted logs. Once discovered via [note tagging](../advanced/storage/note_discovery.md), notes are decrypted and stored locally in the PXE. +- **Authentication Witnesses**: Data used to approve others for executing transactions on your behalf. The PXE provides this data to transactions on-demand during transaction simulation via oracles. +- **Capsules**: Per-contract non-volatile local storage for caching computation results and persisting data across transactions. See [Using Capsules](../../aztec-nr/framework-description/advanced/how_to_use_capsules.md) for more details. +- **Address Book**: Complete addresses (address + public keys) for registered accounts and known senders. This enables the PXE to sync private logs tagged with registered sender addresses. + +Note discovery is handled by Aztec contracts, not the PXE. This allows users to customize or update their note discovery mechanism as needed. + +### Contract management + +Applications can add contract code required for a user to interact with the application to the user's PXE. The PXE will check whether the required contracts have already been registered. There are no getters to check whether a contract has been registered, as this could leak privacy (e.g. a dapp could check whether specific contracts have been registered in a user's PXE and infer information about their interaction history). + +### Keystore + +The keystore securely stores cryptographic keys for registered accounts, including: + +- **Nullifier keys**: Used to create nullifiers that invalidate notes when spent +- **Incoming viewing keys**: Used to decrypt notes sent to the account +- **Outgoing viewing keys**: Used to decrypt notes sent by the account +- **Tagging keys**: Used for note discovery via the tagging protocol + +### Oracles + +Oracles are pieces of data that are injected into a smart contract function from the client side. Learn more about [how oracles work](../../aztec-nr/framework-description/advanced/protocol_oracles.md). + +## Oracle versioning + +The set of oracles that the PXE exposes to private and utility functions is versioned, so that contracts can declare which oracles they expect to be available. Every contract compiled with `Aztec.nr` records the oracle version it was built against, and the PXE checks this version before executing any oracle call. + +The version uses two components, `major.minor`, with the following compatibility rules: + +- **`major`** must match exactly. A major bump is a breaking change: oracles were removed or their signatures changed, and a PXE on a different major cannot safely run the contract. +- **`minor`** indicates additive changes (new oracles). The PXE uses a best-effort approach here: a contract compiled against a higher `minor` than the PXE supports is still allowed to run, and an error is only thrown if the contract actually invokes an oracle the PXE does not know about. In practice, a contract built with a newer Aztec.nr may not use any of the newly added oracles at all, in which case it runs fine on an older PXE. + +The canonical version constants live in the PXE (`ORACLE_VERSION_MAJOR` / `ORACLE_VERSION_MINOR` in `yarn-project/pxe/src/oracle_version.ts`) and in Aztec.nr (`noir-projects/aztec-nr/aztec/src/oracle/version.nr`). The two are kept in lockstep as part of each release. + +### Resolving a version mismatch + +If you see an error like _"Oracle '…' not found. … The contract was compiled with Aztec.nr oracle version X.Y, but this private execution environment only supports up to A.B"_, the contract uses one or more oracles from a newer Aztec.nr than your PXE supports. + +To fix it, upgrade the software that ships the PXE (sandbox, wallet, or whatever embeds `@aztec-labs/pxe`) to a release whose Aztec.nr version is at least as new as the one the contract was compiled with. + +If the PXE reports a version that _should_ include every oracle the contract needs but an oracle is still missing, that is a contract bug rather than a version problem and you should likely report it to the app developer. + +## For developers + +To learn how to develop on top of the PXE, refer to these guides: + +- [Using capsules for local storage](../../aztec-nr/framework-description/advanced/how_to_use_capsules.md) +- [Using oracles in smart contracts](../../aztec-nr/framework-description/advanced/protocol_oracles.md) +- [Authentication witnesses](../advanced/authwit.md) + +## Next steps + +- [Wallets](../wallets.md) - Learn how wallets interact with the PXE +- [State management](../state_management.md) - Understand how private state is managed +- [Note discovery](../advanced/storage/note_discovery.md) - Learn how notes are discovered and synced diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/pxe/kernelless_simulations.md b/docs/lines/v6/docs-developers/docs/foundational-topics/pxe/kernelless_simulations.md new file mode 100644 index 00000000000..304c96b81b5 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/pxe/kernelless_simulations.md @@ -0,0 +1,99 @@ +--- +title: Kernelless simulations +sidebar_position: 2 +tags: [pxe, simulation, gas estimation] +description: How the PXE simulates transactions without running the private kernel circuits, why it is the default for .simulate(), and what it skips. +references: ["labs/yarn-project/pxe/src/contract_function_simulator/*", "labs/yarn-project/stdlib/src/tx/simulated_tx.ts", "labs/yarn-project/wallets/src/embedded/embedded_wallet.ts", "labs/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/*", "labs/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/*"] +--- + +This page explains what kernelless simulation is in the Private eXecution Environment (PXE), how it differs from a full simulation, and where it does and does not apply. If you are looking for the recipe to make `.simulate()` succeed without signing prompts, see [Simulate without signing prompts](../../aztec-js/how_to_simulate_without_signing.md). + +## Overview + +A "full" simulation in the PXE runs the user's private function bytecode, then runs every private kernel circuit (init, inner, reset, tail) over the resulting execution trace. The kernels enforce protocol rules such as side-effect counter sequencing. + +A **kernelless simulation** runs the same private bytecode, but skips the kernel circuits. Instead, the PXE computes the values the kernel would have produced in TypeScript via `generateSimulatedProvingResult`. The output of a kernelless simulation is the same shape as a full simulation, so callers can read return values, offchain effects, and gas estimates from it without caring which path produced them. + +Kernelless simulation is the **default** for `PXE.simulateTx`. The `skipKernels` option in `SimulateTxOpts` defaults to `true`, and `BaseWallet` inherits that default. In normal use, every call to `.simulate()` on a contract method already runs without the kernels. + +The main consequence is speed. Skipping the kernels removes the most expensive part of simulation, so a kernelless run is faster than a full run on typical transactions. + +## What the PXE still does + +A kernelless simulation is not a partial execution. The PXE still: + +- runs the real ACIR bytecode for every private function in the call chain +- executes oracles, decrypts notes, builds nullifiers, and captures offchain effects +- simulates public calls against an ephemeral fork of public state +- runs `node.isValidTx` against the resulting transaction, unless `skipTxValidation` is set +- at the raw `PXE.simulateTx` level, enforces fee payer presence unless `skipFeeEnforcement` is set. Contract `.simulate()` calls through `BaseWallet` already pass `skipFeeEnforcement: true` for estimation, so you do not need to provide a fee block for normal read simulations + +What it skips with `skipKernels: true`: + +- the private kernel init, inner, reset, and tail circuits +- the proof generation associated with those kernels + +The kernels themselves do not check authentication witnesses. Authwit validity is checked by user-contract code (the `is_valid` call that the `#[authorize_once]` macro injects into the called function). What lets a kernelless simulation skip the signing prompt is the **stub-account override**, not the absence of the kernels: replacing the caller's account contract with a stub whose `is_valid` always returns true lets that user-contract check pass without a signature. + +## Simulation overrides + +A kernelless simulation accepts an optional `SimulationOverrides` payload that lets you replace pieces of the state the PXE would otherwise read from chain. The shape (in `yarn-project/stdlib/src/tx/simulated_tx.ts`) is: + +```typescript +type ContractOverrides = Record< + string, + { instance: ContractInstanceWithAddress } +>; + +class SimulationOverrides { + publicStorage?: PublicStorageOverride[]; + contracts?: ContractOverrides; +} +``` + +Two parts: + +- `publicStorage` rewrites slots in the ephemeral public-data fork before simulation. This is compatible with kernel execution; you can use it with or without `skipKernels`. +- `contracts` swaps a contract instance in the simulator's contract DB by replacing its `currentContractClassId`. There is no `artifact` field; the new class id must already be registered with the PXE via `pxe.registerContractClass(...)` so the simulator can resolve the bytecode. This requires `skipKernels: true`: PXE explicitly rejects contract overrides combined with kernel execution, because the kernels would fail validations against the swapped class. + +The `contracts` override path is what makes "simulate without signing" possible. Replacing the caller's account contract with a stub whose `is_valid` always returns true lets the simulation reach `#[authorize_once]` call sites without prompting the user to sign anything. + +For the cheat that simulates a contract as if it had already been upgraded to a new class, see [`fastForwardContractUpdate`](../../aztec-js/how_to_test.md#fast-forwarding-a-contract-update), which returns a `SimulationOverrides` covering both the registry storage rewrite and the upgraded instance entry. + +## Stub account contracts + +The stub-account pattern is the standard way to drive a kernelless simulation without authwit prompts. + +The Noir sources live at `noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/` and `simulated_ecdsa_account_contract/`. Both implement `is_valid` to always return `IS_VALID_SELECTOR`, so authwit validity checks pass without a real signature. Their constructors deliberately emit the same shape of side effects as the real account contracts (one nullifier for the contract init, one nullifier for the `SinglePrivateImmutable` signing-key state, one note hash for the key note, and a private log) so that gas estimation against the stub produces the same numbers as the real account. + +## Authwit requests come from the app, not the stub + +The `CallAuthorizationRequest` offchain effects you see during a kernelless simulation are emitted by the **app or token contract's `#[authorize_once]` macro** during private execution. The stub account's only job is to let the validity check pass so the simulation can reach those call sites in the first place. The wallet then collects the requests via `collectOffchainEffects(privateExecutionResult)`, filters them by `CallAuthorizationRequest.getSelector()`, and decodes each one to build a real `AuthWitness`. + +## Where kernelless does not apply + +The default applies to `simulateTx`, but not to every entry point that looks like simulation: + +- **`profileTx`** is not kernelless. `PXE.profileTx` always runs the private kernels after private execution; `skipProofGeneration` controls only whether a proof is produced, not whether kernel logic runs. If you call `.profile()` to measure circuit gates, expect the full kernel cost. +- **Public-only fast path**. When `BaseWallet.simulateTx` detects a leading run of public static calls, it sends them straight to `node.simulatePublicCalls` through `simulateViaNode`, bypassing the PXE private path entirely. There is no kernel to skip. `SimulationOverrides` still applies on that path. +- **Utility functions**. `FunctionType.UTILITY` calls go through `wallet.executeUtility`, not `pxe.simulateTx`. `ContractFunctionInteraction.simulate` rejects `overrides.publicStorage` and `overrides.contracts` for utility functions. + +## Gas estimation parity + +If the real transaction will pay through a fee payment contract (FPC) with private side effects (the FPC emits notes during fee payment), include that FPC in the simulation's fee options. The FPC's side effects feed into gas estimation, and you can run kernelless with the FPC attached to get both the speed benefit and accurate gas numbers. The default of "omit the fee block" only produces accurate gas for transactions whose fee path has no private side effects. + +## Multi-account scopes + +A simulation can run with multiple scoped accounts via `additionalScopes`. If you build a stub-account override for the sender only, the simulation will still prompt for authwits from any other in-scope account it touches. The override map must cover every account in scope, not just `from`. + +The canonical implementation is `EmbeddedWallet.buildAccountOverrides` in `yarn-project/wallets/src/embedded/embedded_wallet.ts`: for each scoped address, fetch the live contract instance from the PXE, copy it, and rewrite `currentContractClassId` to point at the stub class id registered at wallet startup. When implementing overrides in your own wallet, follow this pattern and make sure the scope list you build against matches the one the simulation will run with. + +## When you might still want a full simulation + +Kernelless is the right default. Reach for `skipKernels: false` only when you are validating kernel-level behavior itself. For everything else, including accurate gas estimation through a fee payment contract with private side effects, run kernelless with the appropriate fee options. + +## Related + +- [Simulate without signing prompts](../../aztec-js/how_to_simulate_without_signing.md) for the recipe-oriented version of this page. +- [Reading contract data](../../aztec-js/how_to_read_data.md) for the basic `.simulate()` API. +- [Wallets](../wallets.md) for the wallet's role in capturing private authorizations during simulation. diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/state_management.md b/docs/lines/v6/docs-developers/docs/foundational-topics/state_management.md new file mode 100644 index 00000000000..546dbca3b77 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/state_management.md @@ -0,0 +1,133 @@ +--- +title: State Management +description: How public and private state work in Aztec, including storage slots, notes, and the UTXO model +sidebar_position: 0 +tags: [protocol, storage] +references: ["labs/noir-projects/aztec-nr/uint-note/src/uint_note.nr"] +--- + + +Aztec has a hybrid public/private state model. Contract developers can specify which data is public and which is private, as well as the functions that operate on that data. + +Private and public data are stored in two separate trees: a **public data tree** and a **note hashes tree**. Both trees store state for all accounts on the network directly as leaves, unlike Ethereum where a state trie contains smaller tries for individual accounts. + +This means storage must be carefully allocated to prevent collisions. Storage is _siloed_ to each contract, though the exact siloing mechanism differs slightly between public and private storage. + +## Public State + +Public state in Aztec works similarly to other blockchains. It is transparent and managed by smart contract logic. + +The sequencer stores and updates public state. It executes state transitions, generates proofs of correct execution (or delegates to the prover network), and publishes data to Ethereum. + +## Private State + +Private state is encrypted and owned by users who hold the decryption keys. It uses an append-only data structure since updating records directly would leak information about the transaction graph. + +To "delete" private state, you add an associated nullifier to a nullifier set. The nullifier is computed such that observers cannot link a state record to its nullifier without the owner's keys. + +Modifying state is accomplished by nullifying the existing record and creating a new one. This gives private state an intrinsic UTXO (unspent transaction output) structure. + +## Notes + +Private state uses UTXOs, commonly called **notes**. Notes are encrypted pieces of data that only their owner can decrypt. + +### How Notes Work + +In Ethereum's account-based model, each account maps to a specific storage location. In Aztec's UTXO model, notes specify their owner and have no fixed relationship between accounts and data locations. + +Rather than storing entire notes, the protocol stores **note commitments** (hashes) in a Merkle tree called the note hash tree. Users prove they know the note preimage when updating private state. + +When a note is consumed, Aztec creates a nullifier from the note data and may create new notes with updated information. This decouples the actions of creating, updating, and deleting private state. + +<Diagram name="public-private-state" alt="Public state is a data tree whose leaves are updated in place. Private state is a UTXO model: spending a note emits a nullifier and creates new notes." /> + +Notes work like cash. To spend a 5 dollar note on a \$3.50 purchase, you nullify the \$5 note and create two new notes: \$1.50 for yourself and \$3.50 for the recipient. Only you and the recipient know about the \$3.50 transfer. + +### Sending Notes + +When creating notes for a recipient, you need a way to deliver them: + +**Onchain (encrypted logs):** The standard method. Emit an encrypted log as part of your transaction. The encrypted note data is posted onchain, allowing recipients to find notes through [note discovery](./advanced/storage/note_discovery.md). + +**Offchain:** If you know the recipient directly, share the note data with them. They store it in their PXE and can spend it later. + +**Self-created notes:** Notes you create for yourself don't need broadcasting. Store them in your PXE to prove ownership and spend them later. + +### Abstracting Notes + +Users don't need to think about individual notes. The Aztec.nr library abstracts notes by letting developers define custom note types that specify how notes are created, nullified, transferred, and displayed. Aztec.nr also handles [note discovery](./advanced/storage/note_discovery.md) for notes encrypted to a user's account. + +## Technical Details + +### Storage Slots + +Public storage uses literal storage slots. Private storage uses logical storage slots that associate multiple notes together. See [storage slots](./advanced/storage/storage_slots.md) for details. + +### Contract Address Siloing + +The contract address is included when computing note hashes to ensure different contracts don't produce identical hashes. The protocol handles this automatically. + +### Note Types + +Aztec.nr provides several note types: + +- **`PrivateSet`** - A collection of notes, useful for balances represented as multiple value notes +- **`PrivateMutable`** - A single note representing one value that can be replaced +- **`PrivateImmutable`** - A single note that cannot be changed after initialization + +These state variables must be wrapped in an `Owned<>` type that specifies the note owner. The `Owned<>` wrapper binds a note collection to a specific owner address, ensuring notes are correctly associated with their owner for nullifier computation and access control. + +```rust +#[storage] +struct Storage<Context> { + balance: Owned<PrivateSet<UintNote, Context>, Context>, +} +``` + +Notes can also be custom types storing any values your application needs. Use the `#[note]` macro for standard notes or `#[custom_note]` for notes requiring custom hash or nullifier computation. + +### Built-in Note Types + +**`UintNote`** - Stores a numeric value (`u128`). Supports partial notes for scenarios where the value is determined in public execution. + +#include_code uint_note_def labs/noir-projects/aztec-nr/uint-note/src/uint_note.nr rust + +**`FieldNote`** - Stores a single `Field` value. + +### Creating and Destroying Notes + +The [lifecycle module](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr) contains functions for note management: + +- `create_note` - Creates a new note, computing its hash and pushing it to the context +- `destroy_note` - Nullifies a note by computing and emitting its nullifier + +Notes created and nullified within the same transaction are called **transient notes**. The kernel circuits automatically squash these, avoiding unnecessary tree insertions and improving efficiency. + +### Note Interface + +Notes must implement the `NoteHash` trait from [note_interface.nr](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/noir-projects/aztec-nr/aztec/src/note/note_interface.nr): + +- `compute_note_hash(self, owner, storage_slot, randomness)` - Computes the note's commitment +- `compute_nullifier(self, context, owner, note_hash_for_nullification)` - Computes the nullifier for consumption +- `compute_nullifier_unconstrained(self, owner, note_hash_for_nullification)` - Unconstrained nullifier computation + +The `#[note]` macro generates default implementations using `poseidon2_hash_with_separator`. + +### Reading Notes + +Only users with appropriate keys can read private values they have permission to access. Notes can be read offchain without modifying onchain state. + +When reading a note in a transaction, subsequent reads of the same note would reveal a link between transactions. To preserve privacy, notes read in transactions are typically "consumed" (nullified) and new notes created. + +With `PrivateSet`, a private variable's value can be interpreted as the sum of all notes at that storage slot. Nullifying is done by inserting a nullifier into the nullifier tree, not by deleting the note hash. + +### Updating Notes + +To update a value, nullify the existing note hash(es) and insert a new note hash for the updated value. The PXE tracks note state locally while the note hash tree records the cryptographic commitments. + +## Further Reading + +- [High level network architecture](./index.md) +- [Transaction lifecycle](./transactions.md#simple-example-of-the-private-transaction-lifecycle) +- [Storage slots](./advanced/storage/storage_slots.md) +- [Note discovery](./advanced/storage/note_discovery.md) diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/transactions.md b/docs/lines/v6/docs-developers/docs/foundational-topics/transactions.md new file mode 100644 index 00000000000..f44d8583667 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/transactions.md @@ -0,0 +1,148 @@ +--- +title: Transactions +sidebar_position: 3 +tags: [protocol] +description: Comprehensive guide to the Aztec transaction lifecycle, covering private execution, PXE interactions, kernel circuits, and the step-by-step process from user request to L1 settlement. +references: ["labs/noir-projects/noir-contracts/contracts/account/ecdsa_k_account_contract/*", "labs/yarn-project/aztec.js/src/contract/*", "labs/yarn-project/stdlib/src/tx/*"] +--- + +import Image from '@theme/IdealImage'; +import YouTubeEmbed from '@site/src/components/YouTubeEmbed'; + +On this page you'll learn: + +- The step-by-step process of sending a transaction on Aztec +- The role of components like PXE, Aztec Node, and the sequencer +- The private and public kernel circuits and how they execute function calls +- The call stacks for private and public functions and how they determine a transaction's completion + +For a two-minute visual overview of how a single transaction spans private and public execution, watch this explainer (find more on the [video lessons](../resources/video_lessons.mdx) page): + +<YouTubeEmbed videoId="MayopgQ1FjI" title="One Transaction, Two Worlds: Private and Public State on Aztec" /> + +## Simple Example of the (Private) Transaction Lifecycle + +The transaction lifecycle for an Aztec transaction is fundamentally different from the lifecycle of an Ethereum transaction. + +The introduction of the Private eXecution Environment (PXE) provides a safe environment for the execution of sensitive operations, ensuring that decrypted data are not accessible to unauthorized applications. However, the PXE exists client-side on user devices, which creates a different model for imagining what the lifecycle of a typical transaction might look like. The existence of a sequencing network also introduces some key differences between the Aztec transaction model and the transaction model used for other networks. + +The accompanying diagram illustrates the flow of interactions between a user, their wallet, the PXE, the node operators (sequencers / provers), and the L1 chain. + +<Image img={require("@site/static/img/transaction-lifecycle.png")} /> + +1. **The user initiates a transaction** – In this example, the user decides to privately send 10 DAI to gudcause.eth. After inputting the amount and the receiving address, the user clicks the confirmation button on their wallet. +2. **The PXE executes transfer locally** – The PXE, running locally on the user's device, executes the transfer method on the DAI token contract on Aztec and computes the state difference based on the user's intention. At this point, the transaction exists solely within the context of the PXE. +3. **The PXE proves correct execution** – The PXE proves correct execution (via zero-knowledge proofs) of the authorization and of the private transfer method. Once the proofs have been generated, the PXE sends the proofs and required inputs (new note commitments and nullifiers) to the sequencer. +4. **The sequencer processes the transaction** – The pseudorandomly-selected sequencer validates the transaction proofs along with required inputs for this private transfer. The sequencer also executes public functions and updates state: public state is updated by directly modifying entries in the sparse Merkle tree, while private state is updated by adding the newly created note commitments and nullifiers to the indexed Merkle trees. The sequencer then computes the new state root and posts the block to L1. +5. **The transaction settles to L1** – The block is posted to L1, and later, provers submit epoch proofs to the verifier contract on Ethereum. Once the epoch proof is verified, the state transitions are considered final and the private transfer has settled. + +### Detailed Diagram + +The following diagram provides a more detailed overview of the transaction execution process, highlighting three different types of transaction execution: contract deployments, private transactions, and public transactions. + +<Image img={require("@site/static/img/local_network_sending_a_tx.png")} /> + +See the page on [call types](./call_types.md) for more context on transaction execution. + +### Transaction Requests + +Transaction requests are how transactions are constructed and sent to the network. + +In Aztec.js: + +#include_code constructor labs/yarn-project/stdlib/src/tx/tx_request.ts javascript + +Where: + +- `origin` is the account contract where the transaction is initiated from. +- `argsHash` is the hash of the arguments of the entrypoint call. The complete set of arguments is passed to the PXE as part of the `TxExecutionRequest` and checked against this hash. +- `txContext` contains the chain id, version, and gas settings. +- `functionData` contains the function selector and indicates whether the function is private or public. +- `salt` is used to make the transaction request hash difficult to predict. The hash is used as the first nullifier if no nullifier is emitted throughout the transaction. + +The `TxExecutionRequest` class: + +#include_code tx_execution_request_class labs/yarn-project/stdlib/src/tx/tx_execution_request.ts javascript + +An account contract validates that the transaction request has been authorized via its specified authorization mechanism, via the `is_valid_impl` function. Here is an example using an ECDSA signature: + +#include_code is_valid_impl labs/noir-projects/noir-contracts/contracts/account/ecdsa_k_account_contract/src/main.nr rust + +Transaction requests are simulated in the PXE in order to generate the necessary inputs for generating proofs. Once transactions are proven, a `Tx` object is created and can be sent to the network to be included in a block: + +#include_code tx_class labs/yarn-project/stdlib/src/tx/tx.ts javascript + +#### Contract Interaction Methods + +Most transaction requests are created as interactions with specific contracts. The exception is transactions that deploy contracts. Here are the main methods for interacting with contracts related to transactions. + +1. [`simulate`](#simulate) +2. [`send`](#send) + +##### `simulate` + +#include_code simulate labs/yarn-project/aztec.js/src/contract/contract_function_interaction.ts javascript + +##### `send` + +#include_code send labs/yarn-project/aztec.js/src/contract/base_contract_interaction.ts javascript + +### Batch Transactions + +Batched transactions are a way to send multiple transactions in a single call. They are created by the `BatchCall` class in Aztec.js. This allows a batch of function calls from a single wallet to be sent as a single transaction through a wallet. + +#include_code batch_call_class labs/yarn-project/aztec.js/src/contract/batch_call.ts javascript + +### Enabling Transaction Semantics + +There are two kernel circuits in Aztec, the private kernel and the public kernel. Each circuit validates the correct execution of a particular function call. + +A transaction is built up by generating proofs for multiple recursive iterations of kernel circuits. Each call in the call stack is modeled as a new iteration of the kernel circuit and is managed by a [FIFO](<https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)>) queue containing pending function calls. There are two call stacks, one for private calls and one for public calls. + +One iteration of a kernel circuit will pop a call off of the stack and execute the call. If the call triggers subsequent contract calls, these are pushed onto the stack. + +Private kernel proofs are generated first. The transaction is ready to move to the next phase when the private call stack is empty. + +The public kernel circuit takes in proof of a public/private kernel circuit with an empty private call stack, and operates recursively until the public call stack is also empty. + +A transaction is considered complete when both call stacks are empty. + +The only information leaked about the transaction is: + +1. The number of private state updates triggered +2. The set of public calls generated + +The addresses of all private calls are hidden from observers. + +## Transaction phases + +An Aztec transaction is split into up to three phases at execution time. The boundaries matter mostly when integrating with fee-paying contracts (FPCs): which phase a call runs in determines whether it can revert, which public functions it is allowed to call, and when its side effects become final. + +### Setup phase (non-revertible) + +The setup phase runs before the user's application logic. Fee-related bookkeeping happens here: + +- The fee payer is nominated via a call to the protocol's `set_as_fee_payer()` function. An FPC typically calls this in its entrypoint; a user paying directly with Fee Juice does it implicitly via the default entrypoint. +- `end_setup()` is called to mark the boundary between the non-revertible and revertible phases. Everything committed before `end_setup()` stands regardless of whether later phases revert. + +Because the setup phase is non-revertible, the protocol restricts which public function calls are allowed during it. The default allowlist permits a small set of trusted setup functions (for example those on `AuthRegistry` and `FeeJuice`); in v4.2.0, public token functions such as `transfer_in_public` and `_increase_public_balance` were removed from it. See the [migration note](../resources/migration_notes.md#custom-token-fpcs-removed-from-default-public-setup-allowlist) for details. + +Practical consequences: + +- A fee payment committed during setup is charged to the payer even if the app phase later reverts. +- An FPC cannot collect payment by directly calling an arbitrary user token's public transfer during setup. It either works purely in the private domain, or relies on a token function the network operator has added to the allowlist. + +### App phase (revertible) + +The app phase runs the user's actual transaction logic. Private execution has already happened locally in the PXE before the transaction was submitted (producing the proof, nullifiers, and note commitments included with the transaction); what runs in this phase is the public call stack. It starts with the public calls that private execution enqueued, and grows as those public calls themselves enqueue further public calls. If any public call in this phase reverts, all state changes from the phase are discarded, but fees committed during setup are still paid. + +### Teardown phase (optional) + +Transactions can optionally include a teardown phase after app execution. During teardown, the final transaction fee is available to public functions, which is useful for FPCs that want to refund unused gas to the user. Not every FPC uses teardown; some charge a fixed quoted amount with no refund, retaining any surplus in the FPC's Fee Juice balance. + +## Next Steps + +- Learn about [accounts](./accounts/index.md) and how they authorize transactions +- Understand [state management](./state_management.md) and how transaction effects are stored +- Explore the [PXE](./pxe/index.md) in more detail +- Understand the [performance impact of kernel circuits](./advanced/circuits/private_kernel.md#performance-impact) on proving time diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/wallets.md b/docs/lines/v6/docs-developers/docs/foundational-topics/wallets.md new file mode 100644 index 00000000000..2a05dd7c53b --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/wallets.md @@ -0,0 +1,79 @@ +--- +title: Wallets +sidebar_position: 2 +tags: [accounts] +description: Overview of wallet responsibilities in Aztec including account management, private state tracking, transaction execution, key management, and authorization handling. +references: ["labs/yarn-project/aztec.js/src/account/*", "labs/yarn-project/wallets/src/*", "labs/noir-projects/noir-contracts/contracts/account/ecdsa_k_account_contract/*"] +--- + +This page covers the main responsibilities of a wallet in the Aztec network. + +Wallets are the applications through which users manage their accounts. Users rely on wallets to browse through their accounts, monitor their balances, and create new accounts. Wallets also store seed phrases and private keys, or interact with external keystores such as hardware wallets. + +Wallets also provide an interface for dapps. Dapps may request access to see the user accounts, in order to show the state of those accounts in the context of the application, and request to send transactions from those accounts as the user interacts with the dapp. + +In addition to these usual responsibilities, wallets in Aztec also need to track private state. This implies keeping a local database of all private notes encrypted for any of the user's accounts, so dapps and contracts can query the user's private state. Aztec wallets are also responsible for producing local proofs of execution for private functions. + +## Account setup + +The first step for any wallet is to let the user set up their [accounts](./accounts/index.md). An account in Aztec is represented onchain by its corresponding account contract that the user must deploy to begin interacting with the network. This account contract dictates how transactions are authenticated and executed. + +A wallet must support at least one specific account contract implementation, which means being able to deploy such a contract, as well as interacting with it when sending transactions. Code-wise, this requires [implementing the `AccountContract` interface](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/yarn-project/aztec.js/src/account/account_contract.ts). + +Note that users must be able to receive funds in Aztec before deploying their account. A wallet should let a user generate a [deterministic complete address](./accounts/keys.md#address-derivation) without having to interact with the network, so they can share it with others to receive funds. This requires that the wallet pins a specific contract implementation, its initialization arguments, a deployment salt, and the user's keys. These values yield a deterministic address, so when the account contract is actually deployed, it is available at the precalculated address. Once the account contract is deployed, the user can start sending transactions using it as the transaction origin. + +## Transaction lifecycle + +Every transaction in Aztec is broadcast to the network as a zero-knowledge proof of correct execution, in order to preserve privacy. This means that transaction proofs are generated on the wallet and not on a remote node. This is one of the biggest differences with regard to EVM chain wallets. + +A wallet is responsible for **creating** an _execution request_ out of one or more _function calls_ requested by a dapp. For example, a dapp may request a wallet to "invoke the `transfer` function on the contract at `0x1234` with the following arguments", in response to a user action. The wallet turns that into an execution request with the signed instructions to execute that function call from the user's account contract. In an [ECDSA-based account](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/account/ecdsa_k_account_contract/src/main.nr), for instance, this is an execution request that encodes the function call in the _entrypoint payload_, and includes its ECDSA signature with the account's signing private key. + +Once the _execution request_ is created, the wallet is responsible for **simulating** and **proving** the execution of its private functions. The simulation yields an execution trace, which can be used to provide the user with a list of side effects of the private execution of the transaction. During this simulation, the wallet is responsible for providing data to the virtual machine, such as private notes, encryption keys, or nullifier secrets. This execution trace is fed into the prover, which returns a zero-knowledge proof that guarantees correct execution and hides all private information. The output of this process is a _transaction object_. + +:::info +Private functions use a UTXO model, so their execution trace is determined entirely by the input notes. Since notes are immutable, simulation results match mined results exactly. However, a transaction may be dropped if it tries to consume a note that was nullified by another transaction first. + +Public functions use an account model (like Ethereum), so their execution trace depends on chain state at inclusion time, which may differ from simulation. +::: + +Before sending, the wallet may run a **simulation**, a lightweight execution using a stub account contract that avoids expensive kernel circuit execution. This simulation estimates gas limits for the transaction and captures any required private authorization data (see [Authorizing actions](#authorizing-actions) below). The `EmbeddedWallet` runs this step automatically on every send. For details on what the PXE skips in this mode and how to wire it up in your own wallet, see [Kernelless simulations](./pxe/kernelless_simulations.md). + +Finally, the wallet **sends** the resulting _transaction_ object, which includes the proof of execution, to an Aztec Node. The transaction is then broadcasted through the peer-to-peer network, to be eventually picked up by a sequencer and included in a block. + +## Authorizing actions + +Account contracts in Aztec expose an interface for other contracts to validate [whether an action is authorized by the account or not](./accounts/index.md#authentication-witnesses-authwit). For example, an application contract may want to transfer tokens on behalf of a user, in which case the token contract will check with the account contract whether the application is authorized to do so. These actions may be carried out in private or in public functions, and in transactions originated by the user or by someone else. + +Wallets should manage these authorizations, prompting the user when they are requested by an application. Authorizations in private executions come in the form of _auth witnesses_, which are usually signatures over an identifier for an action. Applications can request the wallet to produce an auth witness via the `createAuthWit` call. In public functions, authorizations are pre-stored in the account contract storage, which is handled by a call to an internal function in the account contract implementation. + +Wallets can automate private authorization by capturing authorization requests during simulation. The `EmbeddedWallet`, for example, detects which private authwits a transaction needs and generates them automatically, so dapps don't need to explicitly create or manage private authorizations. Public authorizations still require explicit setup, as they involve onchain state changes that must occur before the authorized action. + +## Key management + +As in EVM-based chains, wallets are expected to manage user keys, or provide an interface to hardware wallets or alternative key stores. Keep in mind that in Aztec each account requires [multiple key pairs](./accounts/keys.md): protocol keys (nullifier and incoming viewing keys) are mandated by the protocol and used for spending notes and decryption, whereas signing keys are dependent on the account contract implementation rolled out by the wallet. Should the account contract support it, wallets must provide the user with the means to rotate or recover their signing keys. + +:::info +Due to limitations in the current architecture, protocol keys need to be available in the wallet software itself and cannot be delegated to an external keystore. This restriction may be lifted in a future release. +::: + +## Recipient address management + +Wallets are also expected to manage the public encryption keys of any recipients of local transactions. When creating an encrypted note for a recipient given their address, the wallet needs to provide their [complete address](./accounts/keys.md#address-derivation). Recipients broadcast their complete addresses when deploying their account contracts, and wallets collect this information and save it in a local registry for easy access when needed. + +Note that, in order to interact with a recipient who has not yet deployed their account contract (and thus not broadcasted their complete address), it must also be possible to manually add an entry to a wallet's local registry of complete addresses. + +## Private state + +Wallets also store the user's private state. Aztec uses a [note tagging system](./advanced/storage/note_discovery.md) that allows users to efficiently discover notes that belong to them. When a note is created, the sender tags it with a value derived from a shared secret, allowing the recipient's wallet to query for relevant notes without attempting to decrypt every note on the network. Once discovered, notes are decrypted and added to the corresponding account's private state. + +To discover notes from a sender, the wallet must first register that sender's address with the PXE. This allows the wallet to compute the shared secrets needed for note tagging. Wallets typically register senders when users add contacts or interact with new counterparties. Wallets must also scan for private state in blocks prior to the deployment of a user's account contract, since users may have received notes before deployment. + +Private state can be encrypted and broadcast through the network, then committed to L1. While tags allow wallets to query the network for relevant notes, the tags themselves don't reveal the recipient - only the sender and recipient can compute and recognize them. This means wallets need to maintain a local database of their accounts' private state to answer queries efficiently. + +Dapps may require access to the user's private state, in order to show information relevant to the current application. For instance, a dapp for a token may require access to the user's private notes in the token contract in order to display the user's balance. It is the responsibility of the wallet to require authorization from the user before disclosing private state to a dapp. + +## Account interface + +The account interface is used for creating an _execution request_ out of one or more _function calls_ requested by a dapp, as well as creating an _auth witness_ for a given message hash. Account contracts are expected to handle multiple function calls per transaction, since dapps may choose to batch multiple actions into a single request to the wallet. + +#include_code account-interface labs/yarn-project/aztec.js/src/account/account.ts typescript diff --git a/docs/lines/v6/docs-developers/docs/resources/_category_.json b/docs/lines/v6/docs-developers/docs/resources/_category_.json new file mode 100644 index 00000000000..4c53f1c2248 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/resources/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Resources", + "position": 6, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/resources/community_calls.md b/docs/lines/v6/docs-developers/docs/resources/community_calls.md new file mode 100644 index 00000000000..954031a1506 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/resources/community_calls.md @@ -0,0 +1,40 @@ +--- +title: Community Calls +tags: [community, calls, office-hours] +description: Join live calls to connect with the Aztec team and other builders. +--- + +**Build with us, live.** + +Every week you can join office hours and ecosystem calls to get unblocked, learn from maintainers, and connect with other builders. Pick the call that fits your needs, add it to your calendar, and show up with questions. + +--- + +## Ecosystem Call + +- **When:** Biweekly · Wednesdays · 16:00 - 16:45 UTC +- **Where:** [Google Meet](https://meet.google.com/tnk-phse-bmz) +- **For:** The Ecosystem Call is the place to be if you're building on Aztec. Get updates on libraries and devnet, watch demo apps, peek at what's coming next, and connect with the community. Building an app? Don't miss it. + +--- + +## Aztec & Noir Developer Office Hours + +- **When:** Thursdays · 14:00 - 15:00 UTC +- **Where:** [Google Meet](https://meet.google.com/vev-waao-mab) +- **For:** Developers building with Aztec.nr smart contracts or writing and debugging Noir. Bring your questions about syntax, tooling, patterns, or protocol-level topics. Share a project you're working on, or just hang out with the Aztec Labs Dev Rel team and other devs. + +--- + +## Community + +- Follow discussions in the [Forum](https://forum.aztec.network) +- Ask daily questions in [Discord](https://discord.gg/aztec) + +--- + +## One calendar, all calls + +Save time, add everything with one click: + +[Subscribe to the Aztec Builder Calendar](https://calendar.google.com/calendar/u/0?cid=Y19kMTdhMTEzNmU3NDEwNDNiNDJkMTZlYWU2ZDUzODg4YjlhYTVhNzA5NzNkNjVkNDU3YTA1ZTc3NWNhMGEwNWY5QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20) diff --git a/docs/lines/v6/docs-developers/docs/resources/considerations/_category_.json b/docs/lines/v6/docs-developers/docs/resources/considerations/_category_.json new file mode 100644 index 00000000000..3d16e4d0937 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/resources/considerations/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Considerations and Limitations", + "position": 4, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/resources/considerations/limitations.md b/docs/lines/v6/docs-developers/docs/resources/considerations/limitations.md new file mode 100644 index 00000000000..c475f2e19d9 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/resources/considerations/limitations.md @@ -0,0 +1,162 @@ +--- +title: Limitations +description: Understand the current limitations of the Aztec network and its implications for developers. +sidebar_position: 6 +references: ["labs/noir-projects/aztec-nr/aztec/src/context/*"] +--- + +The Aztec stack is a work in progress. Packages have been released early to gather feedback on the capabilities of the protocol and user experiences. + +## What to expect + +- Regular breaking changes +- Missing features +- Bugs +- An "unpolished" UX +- Missing information + +## Why participate + +Front-run the future! + +Help shape and define: + +- Previously-impossible smart contracts and applications +- Network tooling +- Network standards +- Smart contract syntax +- Educational content +- Core protocol improvements + +## Limitations developers need to know about + +- The Aztec stack is unaudited and under active development. See the [Alpha Network](/participate/alpha) page for details on what this means. +- `msg_sender` is leaked by default when making private -> public calls. + - `self.enqueue(...)` sets `msg_sender` to the private caller's address, which is publicly visible. + - Use `self.enqueue_incognito(...)` to hide the sender. The called public function must use `maybe_msg_sender()` instead of `msg_sender()` to handle the null sender. +- The initial `msg_sender` is `-1`, which can be problematic for some contracts. +- Some side-effect counts are still visible in a transaction. Note hashes, nullifiers, and private logs are padded to hide their true counts, but the number of public function calls and L2->L1 messages remains visible. Privacy sets to further reduce leakage are still under development. +- A transaction can only emit a limited number of side-effects (notes, nullifiers, logs, L2->L1 messages). See [circuit limitations](#circuit-limitations). + - We have not settled on the final constants, since we are still in a testing phase. You could find that certain compositions of nested private function calls (for example, call stacks that are dynamic in size, based on runtime data) could accumulate so many side-effects as to exceed transaction limits. Such transactions would then be unprovable. Please open an issue if you encounter this, as it will help us decide on adequate sizes for our constants. +- Not all Noir cryptographic primitives work in public (AVM) functions. Signature verification (ECDSA secp256k1/r1), AES-128, Blake2s, and Blake3 are not supported. See [AVM Cryptographic Compatibility](../../foundational-topics/advanced/circuits/avm_compatibility.md) for details and workarounds. +- There are many features that we still want to implement. Check out GitHub and the forum for details. If you would like a feature, please open an issue on GitHub. + +## WARNING + +Do not use real, meaningful secrets on Aztec networks. Some privacy features are still in development, including ensuring a secure "zk" property. Since the Aztec stack is still being developed, there are no guarantees that real secrets will remain secret. + +## Limitations + +There are plans to resolve all of the below. + +### It is not audited + +None of the Aztec stack is audited. It is being iterated on every day. It will not be audited for quite some time. + +### Under-constrained + +Some of our more complex circuits are still in development, so they are still under-constrained. + +#### What are the consequences? + +Sound proofs are really only needed as a protection against malicious behavior, which we are not testing for at this stage. + +### Keys and addresses may change in future rollup versions + +The key derivation scheme is documented and stable within the current rollup version, but it may change in future rollup upgrades. Applications should not hardcode assumptions about the specific derivation algorithm. + +Please open new discussions on [Discourse](https://discourse.aztec.network) or open issues on [GitHub](https://github.com/AztecProtocol/aztec-packages) if you have requirements that are not being met by the current key derivation scheme. + +### No privacy-preserving queries to nodes + +Ethereum has a notion of a "full node" which keeps up with the blockchain and stores the full chain state. Many users do not wish to run full nodes, so they rely on third-party "full-node-as-a-service" infrastructure providers who service blockchain queries from their users. + +This pattern is likely to develop in Aztec as well, except there is a problem: privacy. If a privacy-seeking user makes a query to a third-party full node, that user might leak data about who they are, about their historical network activity, or about their future intentions. One solution to this problem is "always run a full node", but pragmatically, not everyone will. To protect less-advanced users' privacy, research is underway to explore how a privacy-seeking user may request and receive data from a third-party node without revealing what that data is, nor who is making the request. + +### Limited private data authentication + +The PXE supports a `scopes` parameter that restricts which accounts' notes a function call can access. However, this is caller-specified: the app chooses its own scopes. There is no mandatory, protocol-enforced authorization layer where the PXE denies an app access to another app's private data. A wallet can restrict scope on behalf of the user, but this is not yet standardized or enforced by default. + +### No client-side bytecode validation + +Public bytecode is validated at the protocol level when contract classes are registered (the Contract Class Registry verifies encoding and commitments). However, the PXE and wallets do not yet validate that the bytecode a user is about to execute matches their stated intentions (function signature and contract address). + +#### What are the consequences? + +If incorrect or malicious bytecode is executed, it could read private data from another contract and emit it publicly. Client-side bytecode validation is planned to close this gap. + +### Insecure hashes + +We are planning a full assessment of the protocol's hashes, including rigorous domain separation. + +#### What are the consequences? + +Collisions and other hash-related attacks might be possible. This is unlikely to cause problems at this early stage, but is a known area of ongoing work. + +### New privacy standards are required + +There are many [patterns](../../resources/considerations/privacy_considerations.md) which can leak privacy, even on Aztec. Standards have not been developed yet to encourage best practices when designing private smart contracts. + +#### What are the consequences? + +For example, until community standards are developed to reduce the uniqueness of ["Tx Fingerprints"](../../resources/considerations/privacy_considerations.md#function-fingerprints-and-tx-fingerprints), app developers might accidentally forfeit some function privacy. + +## Smart contract limitations + +We will never be done with all the features we want to add to Aztec.nr. We have many features that we still want to implement. Please check out GitHub and open new issues with any feature requests you might have. + +## Circuit limitations + +### Upper limits on function outputs and transaction outputs + +Due to the rigidity of zk-SNARK circuits, there are upper bounds on the amount of computation a circuit can perform, and on the amount of data that can be passed into and out of a function. + +> Blockchain developers are no stranger to restrictive computational environments. Ethereum has gas limits, local variable stack limits, call stack limits, contract deployment size limits, log size limits, etc. + +Here are the current limits: + +| Limit | Per call | Per transaction | +| --- | --- | --- | +| Note hashes | `MAX_NOTE_HASHES_PER_CALL` = 16 | `MAX_NOTE_HASHES_PER_TX` = 64 | +| Nullifiers | `MAX_NULLIFIERS_PER_CALL` = 16 | `MAX_NULLIFIERS_PER_TX` = 64 | +| Private function calls | `MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL` = 8 | `MAX_PRIVATE_CALL_STACK_LENGTH_PER_TX` = 16 | +| Enqueued public calls | `MAX_ENQUEUED_CALLS_PER_CALL` = 32 | `MAX_ENQUEUED_CALLS_PER_TX` = 32 | +| L2 to L1 messages | `MAX_L2_TO_L1_MSGS_PER_CALL` = 8 | `MAX_L2_TO_L1_MSGS_PER_TX` = 8 | +| Private logs | `MAX_PRIVATE_LOGS_PER_CALL` = 16 | `MAX_PRIVATE_LOGS_PER_TX` = 64 | +| Contract class logs | `MAX_CONTRACT_CLASS_LOGS_PER_CALL` = 1 | `MAX_CONTRACT_CLASS_LOGS_PER_TX` = 1 | +| Note hash read requests | `MAX_NOTE_HASH_READ_REQUESTS_PER_CALL` = 16 | `MAX_NOTE_HASH_READ_REQUESTS_PER_TX` = 64 | +| Nullifier read requests | `MAX_NULLIFIER_READ_REQUESTS_PER_CALL` = 16 | `MAX_NULLIFIER_READ_REQUESTS_PER_TX` = 64 | +| Key validation requests | `MAX_KEY_VALIDATION_REQUESTS_PER_CALL` = 16 | `MAX_KEY_VALIDATION_REQUESTS_PER_TX` = 64 | +| Public data writes | n/a | `MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX` = 63 | +| Public data reads | n/a | `MAX_PUBLIC_DATA_READS_PER_TX` = 64 | + +:::note +Public data writes have no per-call limit, and one of the 64 write slots per transaction is reserved by the protocol for the fee payer's fee juice balance update. +The full list of protocol constants, including tree heights and other protocol parameters, is available in the [API reference](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/protocol/constants/index.html). +::: + +#### What are the consequences? + +When you write an Aztec.nr function, there will be upper bounds on the following: + +- The number of public state reads and writes; +- The number of note reads and nullifications; +- The number of new notes that may be created; +- The number of encrypted logs that may be emitted; +- The number of unencrypted logs that may be emitted; +- The number of L1->L2 messages that may be consumed; +- The number of L2->L1 messages that may be submitted to L1; +- The number of private function calls; +- The number of public function calls that may be enqueued; + +Not only are there limits on a _per function_ basis, there are also limits on a _per transaction_ basis. + +**In particular, these _per-transaction_ limits will limit transaction call stack depths**. This means if a function call results in a cascade of nested function calls, and each of those function calls outputs many state reads and writes, or logs, then all of that accumulated output data might exceed the per-transaction limits that we currently have. This would cause such transactions to fail. + +There are plans to relax some of this rigidity by providing many "sizes" of circuit. + +> **In the meantime**, if you encounter a per-transaction limit when testing, please open an issue to explain what you were trying to do - we would love to hear about it. And if you are feeling adventurous, you could modify the PXE to increase the limits. **However**, the limits cannot be increased indefinitely. Although we do anticipate that we will be able to increase them slightly, do not provide yourself with 1 million state transitions per transaction. That would be as unrealistic as artificially increasing Ethereum gas limits to 1 trillion. + +## There is more + +See the [GitHub issues](https://github.com/AztecProtocol/aztec-packages/issues) for all known bug fixes and features currently being worked on. diff --git a/docs/lines/v6/docs-developers/docs/resources/considerations/privacy_considerations.md b/docs/lines/v6/docs-developers/docs/resources/considerations/privacy_considerations.md new file mode 100644 index 00000000000..67464e15e8b --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/resources/considerations/privacy_considerations.md @@ -0,0 +1,133 @@ +--- +title: Privacy Considerations +description: Learn about key privacy considerations when building applications on Aztec. +sidebar_position: 5 +tags: [protocol, PXE] +references: ["labs/noir-projects/aztec-nr/aztec/src/context/*"] +--- + +Privacy is a core value of Aztec Protocol. Keeping information private is difficult, and once information is leaked, it cannot be unleaked. This page outlines key privacy considerations that developers should understand when building applications on Aztec. + +## What can Aztec keep private? + +Aztec provides a set of tools to enable developers to build private smart contracts. The following can be kept private: + +**Private persistent state** + +Store state variables in an encrypted form, so that no one can see what those variables are, except those with the decryption key. + +**Private events and messages** + +Emit encrypted events, or encrypted messages from a private smart contract function. Only those with the decryption key will learn the message. + +**Private function execution** + +Execute a private function without the world knowing which function you've executed. + +**Private bytecode** + +The bytecode of private functions does not need to be distributed to the world; much like real-world contracts. + +:::danger +Privacy is not guaranteed without care. +Although Aztec provides the tools for private smart contracts, information can still be leaked unless you are careful. +Aztec is still under development, so real-world, meaningful, valuable secrets _should not_ be entrusted to the system. +This page outlines some best practices to help you build privacy-preserving applications. +::: + +--- + +## Leaky practices + +There are many caveats to the above. Since Aztec also enables interaction with the _public_ world (public L2 functions and L1 functions), private information can be accidentally leaked if developers aren't careful. + +### Crossing the private to public boundary + +Any time a private function makes a call to a public function, information is leaked. Now, that might be perfectly fine in some use cases (it's up to the smart contract developer). Indeed, most interesting apps will require some public state. But let's have a look at some leaky patterns: + +- Calling a public function from a private function. The public function execution will be publicly visible. +- Calling a public function from a private function and revealing the `msg_sender` of that call (the `msg_sender` will be publicly visible). You can hide the sender by using `self.enqueue_incognito(...)` instead of `self.enqueue(...)`, which sets `msg_sender` to a null address. The called function must use `maybe_msg_sender()` to handle this. +- Passing arguments to a public function from a private function. All of those arguments will be publicly visible. +- Calling an internal public function from a private function. The fact that the call originated from a private function of that same contract will be trivially known. +- Emitting unencrypted events from a private function. The unencrypted event name and arguments will be publicly visible. +- Sending L2->L1 messages from a private function. The entire message, and the resulting L1 function execution will all be publicly visible. + +### Crossing the public to private boundary + +If a public function sends a message to be consumed by a private function, the act of consuming that message might be leaked if not following recommended patterns. + +### Timing of transactions + +Information about the nature of a transaction can be leaked based on the timing of that transaction. + +If a transaction is executed at 8am GMT, it's much less likely to have been made by someone in the USA. + +If there's a spike in transactions on the last day of every month, those might be salaries. + +These minor details are information that can disclose much more information about a user than the user might otherwise expect. + +Suppose that every time Alice sends Bob a private token, 1 minute later a transaction is always submitted to the tx pool with the same kind of 'fingerprint'. Alice might deduce that these transactions are automated reactions by Bob. (Here, 'fingerprint' is an intentionally vague term. It could be a public function call, or a private tx proof with a particular number of nonzero public inputs, or some other discernible pattern that Alice sees). + +In short, you should think about the _timing_ of user transactions and how this might leak information. + +### Function Fingerprints and Tx Fingerprints + +A 'Function Fingerprint' is any data which is exposed by a function to the outside world. A 'Tx Fingerprint' is any data which is exposed by a tx to the outside world. We're interested in minimizing leakages of information from private txs. The leakiness of a Tx Fingerprint depends on the leakiness of its constituent functions' Function Fingerprints _and_ on the appearance of the tx's Tx Fingerprint as a whole. For a private function (and by extension, for a private tx), the following information _could_ be leaked (depending on the function, of course): + +- All calls to public functions. + - The contract address of the private function (if it calls an internal public function). + - This could be the address of the transactor themselves, if the calling contract is an account contract. + - All arguments which are passed to public functions. +- All calls to L1 functions (in the form of L2 -> L1 messages). + - The contents of L2 -> L1 messages. +- All public logs (topics and arguments). +- The roots of all trees which have been read from. +- The _number_ of some ['side effects'](https://en.wikipedia.org/wiki/Side_effect_(computer_science)). Note hashes, nullifiers, and private logs are padded to hide their true counts, but the following remain visible: + - \# public function calls + - \# L2->L1 messages + +> Note: many of these were mentioned in the ["Crossing the private to public boundary"](#crossing-the-private-to-public-boundary) section. + +> Note: a transaction's Tx Fingerprint is the combined set of publicly observable data listed above (for example: the number of public function calls, the number of L2->L1 messages, the contents of public logs, and which tree roots were read). Anyone watching the L2 transaction pool can see this fingerprint for every transaction that is submitted, and transactions with distinctive fingerprints can be linked to specific contracts or to patterns of user behavior. + +#### Standardizing Fingerprints + +If each private function were to have a unique Fingerprint, then all private functions would be distinguishable from each other, and all of the efforts of the Aztec Protocol to enable private function execution would have been pointless. + +Standards need to be developed to encourage smart contract developers to adhere to a restricted set of Tx Fingerprints. For example, a standard might propose that the number of new note hashes, nullifiers, logs, etc. must always be equal, and must always equal a power of two. Such a standard would effectively group private functions and transactions into "privacy sets," where all functions and transactions in a particular privacy set would look indistinguishable from each other when executed. + +### Data queries + +It's not just the broadcasting of transactions to the network that can leak data. + +Ethereum has a notion of a "full node" which keeps up with the blockchain and stores the full chain state. Many users don't wish to run full nodes, so they rely on third-party "full-node-as-a-service" infrastructure providers, who service blockchain queries from their users. + +This pattern is likely to develop in Aztec as well, except there's a problem: privacy. If a privacy-seeking user makes a query to a third-party full node, that user might leak data about who they are, their historical network activity, or their future intentions. One solution to this problem is to always run a full node, but pragmatically, not everyone will. To protect less-advanced users' privacy, research is underway to explore how a privacy-seeking user may request and receive data from a third-party node without revealing what that data is, nor who is making the request. + +You should be aware of this avenue for private data leakage. **Whenever an app requests information from a node, the entity running that node is unlikely to be your user.** + +#### What kind of queries can be leaky? + +##### Querying for up-to-date note sibling paths + +To read a private state is to read a note from the note hash tree. To read a note is to prove existence of that note in the note hash tree. And to prove existence is to re-compute the root of the note hash tree using the leaf value, the leaf index, and the sibling path of that leaf. This computed root is then exposed to the world, as a way of saying "This note exists", or more precisely "This note has existed at least since this historical snapshot time". + +If an old historical snapshot is used, then that old historical root will be exposed, and this leaks some information about the nature of your transaction: it leaks that your note was created before the snapshot date. It shrinks the 'privacy set' of the transaction to a smaller window of time than the entire history of the network. + +So for maximal privacy, it's in a user's best interest to read from the very-latest snapshot of the data tree. + +Naturally, the note hash tree is continuously changing as new transactions take place and their new notes are appended. Most notably, the sibling path for every leaf in the tree changes every time a new leaf is appended. + +If a user runs their own node, there's no problem: they can query the latest sibling path for their note(s) from their own machine without leaking any information to the outside world. + +But if a user is not running their own node, they would need to query the very-latest sibling path of their note(s) from some third-party node. In order to query the sibling path of a leaf, the leaf's index needs to be provided as an argument. Revealing the leaf's index to a third party trivially reveals exactly the note(s) you're about to read. And since those notes were created in some prior transaction, the third party will be able to link you with that prior transaction. Suppose then that the third party also serviced the creator of said prior transaction: they will slowly be able to link more and more transactions, and gain more and more insight into a network which is meant to be private. + +We're researching cryptographic ways to enable users to retrieve sibling paths from third parties without revealing leaf indices. + +> \* Note: due to the non-uniformity of Aztec transactions, the 'privacy set' of a transaction might not be the entire set of transactions that came before. + +##### Any query + +Any query to a node leaks information to that node. + +We're researching cryptographic ways to enable users to query any data privately. diff --git a/docs/lines/v6/docs-developers/docs/resources/glossary.md b/docs/lines/v6/docs-developers/docs/resources/glossary.md new file mode 100644 index 00000000000..aee5949060d --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/resources/glossary.md @@ -0,0 +1,195 @@ +--- +title: Glossary +tags: [protocol, glossary] +description: Comprehensive glossary of terms used throughout the Aztec documentation and protocol. +--- + +### ACIR (Abstract Circuit Intermediate Representation) + +ACIR bytecode is the compilation target of private functions. ACIR expresses arithmetic circuits and has no control flow: any control flow in functions is either unrolled (for loops) or flattened (by inlining and adding predicates). ACIR contains different types of opcodes including arithmetic operations, BlackBoxFuncCall (for efficient operations like hashing), Brillig opcodes (for unconstrained hints), and MemoryOp (for dynamic array access). Private functions compiled to ACIR are executed by the ACVM (Abstract Circuit Virtual Machine) and proved using Barretenberg. + +### AVM (Aztec Virtual Machine) + +The Aztec Virtual Machine (AVM) executes the public section of a transaction. It is conceptually similar to the Ethereum Virtual Machine (EVM) but designed specifically for Aztec's needs. Public functions are compiled to AVM bytecode and executed by sequencers in the AVM. The AVM uses a flat memory model with tagged memory indexes to track maximum potential values and bit sizes. It supports control flow (if/else) and includes specific opcodes for blockchain operations like timestamp and address access, but doesn't allow arbitrary oracles for security reasons. + +### Aztec + +Aztec is a decentralized Layer 2 rollup on Ethereum for building unstoppable private apps. It supports smart contracts with both private & public state and private & public execution. + +`aztec` is a CLI tool (with an extensive set of parameters) that enables users to perform a wide range of tasks. It can: compile and test contracts, run a node, run a local network, execute tests, generate contract interfaces for javascript and more. + +Full reference [here](../cli/aztec_cli_reference). + +### Aztec Wallet + +The Aztec Wallet is a CLI wallet, `aztec-wallet`, that allows a user to manage accounts and interact with an Aztec network. It includes a PXE. + +Full reference [here](../cli/aztec_wallet_cli_reference). + +### `aztec-up` + +`aztec-up` updates the local aztec executables to the latest version (default behavior) or to a specified version. + +### Aztec.js + +A [Node package](https://www.npmjs.com/package/@aztec-labs/aztec.js) to help make Aztec dApps. + +Read more and review the source code [here](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/yarn-project/aztec.js). + +### Aztec.nr + +[Aztec.nr](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/noir-projects/aztec-nr) is a Noir framework for writing Aztec smart contracts that abstracts away state management, handling note generation, state trees, and more. + +Read more and review the source code [here](https://aztec.nr). + +### Barretenberg + +Aztec's cryptography back-end. Refer to the graphic at the top of [this page](https://medium.com/aztec-protocol/explaining-the-network-in-aztec-network-166862b3ef7d) to see how it fits in the Aztec architecture. + +Barretenberg's source code can be found [here](https://github.com/AztecProtocol/barretenberg). + +### bb / bb.js + +`bb` (CLI) and its corresponding `bb.js` (node module) are tools that prove and verify circuits. It also has helpful functions such as: writing solidity verifier contracts, checking a witness, and viewing a circuit's gate count. + +### Commitment + +A cryptographic commitment is a hash of some data (plus randomness) that hides the original value but allows you to later prove you committed to that specific value, by proving knowledge of a valid preimage, without being able to change it. + +In Aztec, a commitment refers to a cryptographic hash of a note. Rather than storing entire notes in a data tree, note commitments (hashes of the notes) are stored in a merkle tree called the note hash tree. Users prove that they have the note pre-image information when they update private state in a contract. This allows the network to verify the existence of private data without revealing its contents. + +### Merkle Tree + +A Merkle tree is a binary tree data structure where adjacent nodes are hashed together recursively to produce a single node called the root hash. + +Merkle trees in Aztec are used to store cryptographic commitments. They are used across five Aztec Merkle trees: the note hash tree (stores commitments to private notes), the nullifier tree (stores nullifiers for spent notes), the public data tree (stores public state), the contract tree, and the archive tree. All trees use domain-separated Poseidon2 hashing with specific tree identifiers and layer separation to ensure security and prevent cross-tree attacks. + +### `nargo` + +With `nargo`, you can start new projects, compile, execute, and test your Noir programs. + +The Aztec installer ships its own pinned `nargo` and exposes it as the `aztec-nargo` wrapper on `PATH` (bare `nargo` is intentionally not provided so it does not shadow your own install). For Aztec contract work, prefer `aztec compile` and `aztec test`; for plain Noir commands, use `aztec-nargo` (or your own `nargo` install). + +You can find more information in the nargo installation docs [here](https://noir-lang.org/docs/installation) and the nargo command reference [here](https://noir-lang.org/docs/reference/nargo_commands). + +### Noir + +Noir is a Domain Specific Language (DSL) for SNARK proving systems. It is used for writing smart contracts in Aztec because private functions on Aztec are implemented as SNARKs to support privacy-preserving operations. + +### Noir Language Server + +The Noir Language Server can be used in vscode to facilitate writing programs in Noir by providing syntax highlighting, circuit introspection and an execution interface. The Noir LSP addon allows the dev to choose their tool, nargo or `aztec`, when writing a pure Noir program or an Aztec smart contract. + +You can find more info about the LSP [in the Noir docs](https://noir-lang.org/docs/tooling/language_server). + +### Node + +A node is a computer running Aztec software that participates in the Aztec network. A specific type of node is a sequencer. Nodes run the public execution environment (AVM), validate proofs, and maintain the five state Merkle trees (note hash, nullifier, public state, L1-L2 message, and archive trees). + +The Aztec testnet rolls up to Ethereum Sepolia. + +To run your own node see [here](/operate/operators). + +### Note + +In Aztec, a note is like an envelope containing private data. A commitment (hash) of this note is stored in an append-only Merkle tree maintained by all nodes in the network. Notes can be encrypted to be shared with other users. The data in a note represents a variable's state at a specific point in time. + +### Note Discovery + +Note discovery refers to the process of a user identifying and decrypting the encrypted notes that belong to them. Aztec uses a note tagging system where senders tag encrypted onchain logs containing notes in a way that only the sender and recipient can identify. The tag is derived from a shared secret and an index (a shared counter that increments each time the sender creates a note for the recipient). This allows users to efficiently find their notes without brute force decryption or relying on offchain communication. + +### Nullifier + +A nullifier is a unique value that, once posted publicly, proves something has been used or consumed without revealing what that thing was. + +In the context of Aztec, a nullifier is derived from a note and signifies the note has been "spent" or consumed without revealing which specific note was spent. When a note is updated or spent in Aztec, the protocol creates a nullifier from the note data using the note owner's nullifier key. This nullifier is inserted into the nullifier Merkle tree. The nullifier mechanism prevents double-spending while maintaining privacy by not requiring deletion of the original note commitment, which would leak information. + +### Partial Notes + +Partial notes are a concept that allows users to commit to an encrypted value, and allows a counterparty to update that value without knowing the specific details of the encrypted value. They are notes that are created in a private function with values that are not yet considered finalized (e.g., `amount` in a `UintNote`). The partial note commitment is computed using multi scalar multiplication on an elliptic curve, then passed to a public function where another party can add value to the note without knowing its private contents. This enables use cases like private fee payments, DEX swaps, and lending protocols. + +### Programmable Privacy + +Aztec achieves programmable privacy through its hybrid architecture that supports both private and public smart contract execution. Private functions run client-side with zero-knowledge proofs, while public functions run onchain. This allows developers to program custom privacy logic, choosing what data remains private and what becomes public, with composability between private and public state and execution contexts. + +### Provers + +The Prover in a ZK system is the entity proving they have knowledge of a valid witness that satisfies a statement. In the context of Aztec, this is the entity that creates the proof that some computation was executed correctly. Here, the statement would be "I know the inputs and outputs that satisfy the requirements for the computation, and I did the computation correctly." + +Aztec launched with a fully permissionless proving network that anyone can participate in. The proving network produces proofs for valid rollup state transitions. + +How this works will be discussed via a future RFP process on Discourse, similarly to the Sequencer RFP. + +### Proving Key + +A key that is used to generate a proof. In the case of Aztec, these are compiled from Noir smart contracts. + +### Private Execution Environment (PXE) + +The private execution environment is where private computation occurs. This is local to your device or browser. + +Read more [here](../foundational-topics/pxe/index.md). + +### Local Network + +The local network is a development Aztec network that runs on your machine and interacts with a development Ethereum node. It allows you to develop and deploy Noir smart contracts without interacting with testnet or mainnet. + +Included in the local network: + +- Local Ethereum network (Anvil) +- Deployed Aztec protocol contracts (for L1 and L2) +- A set of test accounts with some test tokens to pay fees +- Development tools to compile contracts and interact with the network (`aztec` and `aztec-wallet`) + +### Release line + +A major version of the Aztec software and the name it ships under. The v6 line is called **Tierra**, so `6.0.0`, `6.1.0`, and every other `6.x` release are Tierra. Networks (Alpha and Testnet) run a release line; your code pins an exact version. See the [Networks page](/networks) for what each network runs today. + +### Sequencer + +A sequencer is a specialized node that is generally responsible for: + +- Selecting pending transactions from the mempool +- Ordering transactions into a block +- Verifying all private transaction proofs and executing all public transactions to check their validity +- Computing the ROLLUP_BLOCK_REQUEST_DATA +- Computing state updates for messages between L2 & L1 +- Broadcasting the ROLLUP_BLOCK_REQUEST_DATA to the prover network via the proof pool for parallelizable computation. +- Building a rollup proof from completed proofs in the proof pool +- Tagging the pending block with an upgrade signal to facilitate forks +- Publishing completed block with proofs to Ethereum as an ETH transaction + +Aztec will be launched with a fully permissionless sequencer network that anyone can participate in. + +How this works is being discussed actively in the [Discourse forum](https://discourse.aztec.network/t/request-for-proposals-decentralized-sequencer-selection/350/). Once this discussion process is completed, we will update the glossary and documentation with specifications and instructions for how to run. + +Previously in [Aztec Connect](https://medium.com/aztec-protocol/sunsetting-aztec-connect-a786edce5cae) there was a single sequencer, and you can find the Typescript reference implementation called Falafel [here](https://github.com/AztecProtocol/aztec-connect/tree/master/yarn-project/falafel). + +### Smart Contracts + +Programs that run on the Aztec network are called smart contracts, similar to [programs](https://ethereum.org/en/developers/docs/smart-contracts/) that run on Ethereum. + +However, these will be written in the [Noir](https://noir-lang.org/) programming language, and may optionally include private state and private functions. + +### Statement + +A statement in Aztec's zero-knowledge context refers to the public assertion being proved about a private computation. For example, a statement might be "I know the inputs and outputs that satisfy the requirements for this computation, and I executed the computation correctly." The statement defines what is being proven without revealing the private details (the witness) that prove it. In Aztec, statements typically involve proving correct execution of private functions, valid note ownership, or proper state transitions. + +### Verifier + +The entity responsible for verifying the validity of a ZK proof. In the context of Aztec, this is: + +- **The sequencers**: verify that private functions were executed correctly. +- **The Ethereum L1 smart contract**: verifies batches of transactions were executed correctly. + +### Verification Key + +A key that is used to verify the validity of a proof generated from a proving key from the same smart contract. + +### Witness + +In the context of Aztec's zero-knowledge proofs, a witness refers to the private inputs and intermediate values that satisfy the constraints of a circuit. When executing a private function, the ACVM generates the witness of the execution - the complete set of values that prove the computation was performed correctly. The witness includes both the secret inputs provided by the user and all intermediate computational steps, but is never revealed publicly. Only a cryptographic proof of the witness's validity is shared. + +### Zero-knowledge (ZK) proof + +Zero-knowledge proofs in Aztec are cryptographic proofs that allow someone to prove they know certain information or have performed a computation correctly without revealing the underlying data. Aztec uses various ZK-SNARK protocols including UltraPlonk and Honk. These proofs enable private execution where users can prove they executed a private function correctly and that they own certain notes, without revealing the function inputs, note contents, or internal computation details. The proofs are verified onchain to ensure the integrity of private state transitions. diff --git a/docs/lines/v6/docs-developers/docs/resources/migration_notes.md b/docs/lines/v6/docs-developers/docs/resources/migration_notes.md new file mode 100644 index 00000000000..455c9e7ab58 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/resources/migration_notes.md @@ -0,0 +1,8297 @@ +--- +title: Migration notes +description: Read about migration notes from previous versions, which could solve problems while updating +keywords: [local network, sandbox, aztec, notes, migration, updating, upgrading] +tags: [migration, updating, sandbox, local network] +--- + +Aztec is in active development. Each version may introduce breaking changes that affect compatibility with previous versions. This page documents common errors and difficulties you might encounter when upgrading, along with guidance on how to resolve them. + +## TBD + +### [PXE] `generateSimulatedProvingResult` takes the tx's max expiration timestamp and an options object + +`generateSimulatedProvingResult` (exported from `@aztec-labs/pxe/simulator`) now takes the latest expiration timestamp the tx may publish as its fourth argument. For a tx that may be sent, compute it with `computeSimulatedMaxExpirationTimestamp`, so that the simulated expiry matches the proven one. The optional settings follow in an options object, which also accepts the wallet's `resolveExpirationAndPadding` hook. + +**Migration:** + +```diff +- await generateSimulatedProvingResult(result, getFunctionName, node, minRevertibleSideEffectCounter); ++ await generateSimulatedProvingResult( ++ result, ++ getFunctionName, ++ node, ++ await computeSimulatedMaxExpirationTimestamp(result, node), ++ { minRevertibleSideEffectCounterOverride: minRevertibleSideEffectCounter }, ++ ); +``` + +## 6.0.0 + +### [Aztec.nr] ECDSA note and protocol contract interfaces are published with aztec-nr + +`ecdsa_public_key_note` and the protocol contract interfaces now ship in `aztec-labs-eng/aztec-nr`. Depending +on them from `AztecProtocol/aztec-packages` or `aztec-labs-eng/aztec-node` pulls in a second copy of `aztec`, +which fails to compile with errors such as `No matching impl found` or `found type &mut PrivateContext`. +Depend on them from `aztec-labs-eng/aztec-nr`, at the same tag as `aztec`: + +```diff +-ecdsa_public_key_note = { git = "https://github.com/AztecProtocol/aztec-packages", tag = "v5.2.0", directory = "noir-projects/noir-contracts/contracts/libs/ecdsa_public_key_note" } +-contract_instance_registry = { git = "https://github.com/AztecProtocol/aztec-packages", tag = "v5.2.0", directory = "noir-projects/noir-contracts/contracts/protocol_interface/contract_instance_registry_interface" } +-fee_juice = { git = "https://github.com/AztecProtocol/aztec-packages", tag = "v5.2.0", directory = "noir-projects/noir-contracts/contracts/protocol_interface/fee_juice_interface" } ++ecdsa_public_key_note = { git = "https://github.com/aztec-labs-eng/aztec-nr", tag = "v<version>", directory = "ecdsa-public-key-note" } ++contract_instance_registry = { git = "https://github.com/aztec-labs-eng/aztec-nr", tag = "v<version>", directory = "contract-instance-registry-interface" } ++fee_juice = { git = "https://github.com/aztec-labs-eng/aztec-nr", tag = "v<version>", directory = "fee-juice-interface" } +``` + +### [Aztec.nr] Fact collections are scoped by `FactScope` + +The `aztec::facts` functions now take the collection's scope as an `aztec::facts::FactScope` instead of an `AztecAddress`, and `FactCollection::scope` is a `FactScope`. `FactScope::account(address)` is the scope of a single account, as before. The new `FactScope::public()` is shared by all accounts: any execution of the contract can access its collections, whichever accounts are in scope. + +**Migration:** + +```diff +- use aztec::facts::record_retractable_fact; ++ use aztec::facts::{FactScope, record_retractable_fact}; + +- record_retractable_fact(contract_address, recipient, type_id, collection_id, fact_type_id, payload, origin_block); ++ record_retractable_fact( ++ contract_address, ++ FactScope::account(recipient), ++ type_id, ++ collection_id, ++ fact_type_id, ++ payload, ++ origin_block, ++ ); +``` + +This is a breaking change to the PXE oracle interface (version 32 → 33): contracts must be recompiled against the updated `aztec-nr` to run against the new PXE. + +## 6.0.0-rc.3 + +### [Aztec.nr] `AuthRegistry` re-pinned at a new address + +A further standard-contract re-pin, against oracle interface version 32, moves the +canonical `AuthRegistry` to a new address and class id. Authorizations set on the +previous `AuthRegistry` instance must be set again on the new one. This is separate +from the re-pin that shipped in 6.0.0-rc.1, which moved only `MultiCallEntrypoint` +and `HandshakeRegistry`. + +### [Protocol] The contract address domain separator is bumped to V3; every contract address changes + +The domain separator that goes into contract address derivation (`preaddress = poseidon2(DOM_SEP__CONTRACT_ADDRESS_Vn, public_keys_hash, partial_address)`) is bumped: `DOM_SEP__CONTRACT_ADDRESS_V2` (`4099338721`) is replaced by `DOM_SEP__CONTRACT_ADDRESS_V3 = 993442748` (`hash_to_u32("az_dom_sep", "contract_address_v3")`). Every protocol version rotates this separator on purpose: an address can never exist on two rollup instances, so funds cannot be sent to an address that is live on one rollup and dead on another. As a result, every contract address, including account addresses, differs from the address the same class, salt, initialization and public keys produced under V2. The `V` number is the separator revision, not the protocol version. + +Nothing needs to change in contract code, but anything that hard-codes an address must be updated. The canonical addresses of the standard contracts, which are deterministic in their pinned artifacts and the separator, move to: + +| Contract | V2 address | V3 address | +| ------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | +| AuthRegistry | `0x2e05e8fbfb2ce9a31e4b4ae5097c01b79d9b3529b1e861016c9c9a5d62fdf7c9` | `0x2ba8714679fbee823f108f366f878ce3249861637de705b0ac7cfbd0f81b3a9c` | +| MultiCallEntrypoint | `0x09b93e8efd54a70cd718bc4d8ecf52c51db8150d300c8877c56796ee8c505137` | `0x265552a432ba2f9420ce5283bec14d7385e7a4accaff6c82648efe312d42fcc2` | +| PublicChecks | `0x0468c918e7d7e077f4fa75e275891db9bee8faaeafab83e870eeb39dbfaddbbe` | `0x2cfebe9733e41d10357b4362dc434a24a7c53f12e952f5727a38c6da7ec6d21a` | +| HandshakeRegistry | `0x24d2effac2983dfcae3aea48019aa10b80a7d2cfddc52cdbd9b90745ed0b0fc7` | `0x198aa5f21f7a13761f4729c544fb6edcae73a484d57b248b6390eb0dc5c06040` | + +`STANDARD_*_ADDRESS` in `@aztec-labs/standard-contracts` and `aztec::standard_addresses` carry the new values, so code that reads them needs only a recompile. HandshakeRegistry derives addresses itself, through `get_public_keys`, so the separator is compiled into its bytecode and the pinned standard-contract build is refreshed with it. That recompile also picks up the current aztec-nr in AuthRegistry and MultiCallEntrypoint, whose bytecode changes with it, so their artifact hashes and addresses move for that reason as well as for the separator. PublicChecks' bytecode is unchanged by the recompile; its address moves through the separator alone. + +The protocol contracts keep their magic addresses (`1`, `2`, ...). Their registration nullifiers are siloed by those magic addresses rather than by the derived ones, so the genesis constants move for a narrower reason: ContractInstanceRegistry calls `AztecAddress::compute` when it publishes an instance, so the separator is in its bytecode, its class id rotates, and exactly one of the six seeded registration nullifiers changes with it. + +### [Aztec.nr] `Writer::advance_offset` is removed + +Write the skipped fields instead, e.g. with their own `stream_serialize`: + +```diff +- writer.advance_offset(<T as Serialize>::N); ++ value.stream_serialize(writer); +``` + +`Serialize for Option<T>` now fails on a `None` whose payload is non-zero. `Option::none()` has a zero payload, so its encoding is unchanged and no contract or client change is needed. + +### [Aztec.nr] Batch nullifier status oracle + +The single-nullifier existence oracle is replaced by `aztec::oracle::nullifiers::get_nullifier_statuses`, which checks any number of nullifiers in one call and reports, for each settled one, the block it was included in. `check_nullifier_exists` is kept and now calls the new oracle. + +This is a breaking change to the PXE oracle interface (version 31 → 32): contracts must be recompiled against the updated `aztec-nr` to run against the new PXE. + +### [Aztec.nr] `OriginBlock` replaced by `BlockReference` + +`aztec::facts::OriginBlock` has been removed. Retractable facts now identify their origin block with `aztec::oracle::block_reference::BlockReference`, which has the same `block_number` and `block_hash` fields. + +**Migration:** + +```diff +- use aztec::facts::{OriginBlock, record_retractable_fact}; ++ use aztec::facts::record_retractable_fact; ++ use aztec::oracle::block_reference::BlockReference; + +- let origin_block = OriginBlock { block_number, block_hash }; ++ let origin_block = BlockReference { block_number, block_hash }; +``` + +### [AztecNode] `getBlockHashMembershipWitness` requires an existing reference block; new `getBlockHashMembershipWitnessAtArchive` + +`getBlockHashMembershipWitness(referenceBlock, blockHash)` returns a witness against the reference block header's `lastArchive.root`, the archive before the reference block was appended. v5 nodes also accepted a number-only reference one past the latest block, answering against the latest block's archive without that block existing. v6 does not: every reference, including a bare number, must name a block the node has, and a number past the tip fails once the node's hold-off for unseen blocks runs out. + +The node also checks that the archive it proves against is exactly the one the reference header commits to, and throws if its world state holds a different archive at that height (for instance after a reorg) instead of answering from it. + +To prove membership against a given archive root, including a block's own post-block archive, use the new `getBlockHashMembershipWitnessAtArchive({ archive }, blockHash)`. It accepts only an `{ archive }` selector, needs no later block to exist, and throws if the node does not know the root. Picking a root does not make it canonical or final: callers must choose a root their verifier accepts. + +```diff +- // Witness against the archive after block N, via the block after it +- const witness = await aztecNode.getBlockHashMembershipWitness(BlockNumber(n + 1), blockHash); ++ const witness = await aztecNode.getBlockHashMembershipWitnessAtArchive({ archive: blockN.archive.root }, blockHash); +``` + +Callers that anchor on a block header, such as PXE and its oracles, keep using `getBlockHashMembershipWitness`. + +## 6.0.0-rc.1 + +### [Aztec.nr] `MultiCallEntrypoint` and `HandshakeRegistry` re-pinned at new addresses + +The standard contracts have been re-pinned against the v6.0.0-rc.1 toolchain. The canonical `MultiCallEntrypoint` and `HandshakeRegistry` move to new addresses and class ids; `AuthRegistry` and `PublicChecks` keep theirs. Handshakes established with the previous registry instance are not visible to the new one and must be re-established. + +### [Protocol] The protocol nullifier is derived from the tx request's salt alone; `tx_request_salt` becomes `protocol_nullifier` + +The protocol nullifier, which the init kernel always inserts at index 0 of a transaction's nullifiers, is now `silo(NULL_MSG_SENDER, H(origin, chain_id, version, salt))` instead of `H(tx_request)`. Gas settings and the first call's arguments are no longer part of its preimage, so two transactions built with the same salt (a fee bump, or a cancellation) share it and at most one of them can be mined. The proof is still pinned to the full tx request by the init kernel. The kernel cannot check that the salt is random, so wallets must draw it fresh for every transaction: two requests with the same origin and salt are mutually exclusive. + +The `tx_request_salt` field of `PrivateCircuitPublicInputs`, `PrivateContextInputs` and `PrivateTxConstantData` is replaced by `protocol_nullifier`: the siloed protocol nullifier, kernel-checked and identical on every private call of the transaction. The raw salt no longer reaches app circuits. This is set by the framework and PXE; contracts need no changes beyond recompiling. `TxRequest` gains `computeProtocolNullifierValue()` / `computeProtocolNullifier()` in Aztec.js (`compute_protocol_nullifier_value` / `compute_protocol_nullifier` in Noir); `TxRequest.hash()` (Noir: `impl Hash for TxRequest`) and `DOM_SEP__TX_REQUEST` are removed, since computing the old protocol nullifier was their only job; and `computeProtocolNullifier(value)` in `@aztec-labs/stdlib/hash` takes the unsiloed value. + +Nothing in accounts, auth witnesses or wallets changes with this release; the field is only renamed and its value redefined. It is the protocol half of the design that lets accounts and auth witnesses bind to `protocol_nullifier` for replay protection, cancellation and fee bumping, which lands in the framework separately. + +### [Node] `ACVM_*` config renamed to `NOIR_EXECUTE_*` + +Protocol circuits are now executed with noir's `noir-execute` rather than the `acvm` binary, so the +configuration naming the executor follows the tool. The environment variables, the CLI flags and the +config fields are renamed; nothing about what you point them at changes, beyond the binary itself no +longer being called `acvm`. + +| Old | New | +| ---------------------------- | ------------------------------------ | +| `$ACVM_BINARY_PATH` | `$NOIR_EXECUTE_BINARY_PATH` | +| `$ACVM_WORKING_DIRECTORY` | `$NOIR_EXECUTE_WORKING_DIRECTORY` | +| `--sequencer.acvmBinaryPath` | `--sequencer.noirExecuteBinaryPath` | +| `--proverNode.acvmBinaryPath`| `--proverNode.noirExecuteBinaryPath` | +| `acvmBinaryPath` | `noirExecuteBinaryPath` | +| `acvmWorkingDirectory` | `noirExecuteWorkingDirectory` | + +The same rename applies to the `acvmWorkingDirectory` flags (`--sequencer.acvmWorkingDirectory`, +`--proverNode.acvmWorkingDirectory`). The release image sets the new variables itself, and its default +working directory moves from `/usr/src/acvm` to `/usr/src/noir-execute`. + +**Migration:** + +```diff +- ACVM_BINARY_PATH=/usr/src/labs-aztec-toolchain/bin/noir-execute +- ACVM_WORKING_DIRECTORY=/usr/src/acvm ++ NOIR_EXECUTE_BINARY_PATH=/usr/src/labs-aztec-toolchain/bin/noir-execute ++ NOIR_EXECUTE_WORKING_DIRECTORY=/usr/src/noir-execute +``` + +There is no fallback to the old names, and a node still setting only `ACVM_BINARY_PATH` fails to start +rather than running degraded: a prover agent exits with `Requested real proving but no path to bb or +noir-execute binaries provided`, and a prover node or sequencer configured for real proofs throws from +`BBNativeRollupProver.new` when it tries to stat an undefined path. Only a node running without real +proofs falls back to WASM simulation, which is much slower. + +Type names keep the ACVM spelling (`NativeACVMSimulator`, `ACVMConfig`, `ACVMWitness`), since those +describe the ACIR VM and its witness format rather than the tool that runs it. + +### [Protocol] Block header commits to a per-block tx effects tree + +The L2 block header gains a `tx_effects_tree_root` field: the root of a per-block merkle tree with one leaf per +transaction, where each leaf binds the transaction hash to a structured hash of its effects (note hashes, nullifiers, +L2-to-L1 messages, public data writes, and logs). Holding a block header is now enough to verify that a transaction was +included in that block and produced exactly a given set of effects with a short membership proof, instead of replaying +the checkpoint's full sponge blob. The root is computed by the rollup circuits and is available on `BlockHeader` as +`txEffectsTreeRoot`; `TxEffect.computeTxEffectLeaf()` and `Body.computeTxEffectsTreeRoot()` compute the leaves and root +from published data. + +This is a breaking protocol change: the block header serialization grew by one field, so every transaction hash, +verification key, contract artifact, canonical contract address, and the genesis constants change. Contracts must be +recompiled against the updated `aztec-nr`. The PXE oracle interface version was bumped (30 → 31), as was the TXE +oracle interface version (8 → 9), and existing PXE databases are re-initialized on next open (data schema version +14 → 15). Nodes resync their archiver, which now also stores the tx effects tree leaves of each block (archiver data +schema version 9 → 10). + +### [npm] Packages moved to the `@aztec-labs` scope + +Every package published from this repository is now scoped `@aztec-labs` instead of `@aztec`. +Update your dependencies and imports: + +```diff +-import { Contract } from '@aztec/aztec.js'; ++import { Contract } from '@aztec-labs/aztec.js'; +``` + +```diff +- "@aztec/aztec.js": "<version>", +- "@aztec/accounts": "<version>", ++ "@aztec-labs/aztec.js": "<version>", ++ "@aztec-labs/accounts": "<version>", +``` + +Subpath imports are unaffected beyond the scope: `@aztec/foundation/log` becomes +`@aztec-labs/foundation/log`. + +The `@aztec` scope keeps the packages published for earlier versions, so an existing project +pinned to an older release continues to install unchanged. There is no `@aztec-labs` release of +those older versions. + +The packages now depend on upstream `viem` instead of the `@aztec/viem` fork. If your project +aliased `viem` to the fork (`"viem": "npm:@aztec/viem@<version>"`), depend on upstream `viem` at +the version `@aztec-labs/aztec.js` uses instead. + +### [npm] Foundation packages moved to the `@aztec-foundation` scope + +The packages built by the foundation repository and consumed here - `bb.js`, `bb-avm-sim`, +`cdb`, `wsdb`, `ipc-runtime`, the `noir-*` packages, `l1-artifacts`, `constants-codegen` and +the protocol artifacts packages - are now scoped `@aztec-foundation` instead of `@aztec`. +Update any direct dependency on them: + +```diff +-import { Barretenberg } from '@aztec/bb.js'; ++import { Barretenberg } from '@aztec-foundation/bb.js'; +``` + +```diff +- "@aztec/bb.js": "<version>", +- "@aztec/noir-noir_js": "<version>", ++ "@aztec-foundation/bb.js": "<version>", ++ "@aztec-foundation/noir-noir_js": "<version>", +``` + +Most projects reach these packages only through `@aztec-labs/*`, which carries the change for +them; only a direct dependency needs updating. + +The `@aztec` scope keeps the packages published for earlier versions, so an existing project +pinned to an older release continues to install unchanged. There is no `@aztec-foundation` +release of those older versions: the scope starts at `6.0.0-nightly.20260901`. + +### [Aztec.nr] aztec-nr moved to `aztec-labs-eng/aztec-nr` + +The aztec-nr crates are now published at https://github.com/aztec-labs-eng/aztec-nr. The previous +repository, `AztecProtocol/aztec-nr`, has no tags from v6 onwards, so a project that only bumps its +`tag` fails to compile with `Remote branch v6.x.x not found`. Update the `git` field of every aztec-nr +dependency in your `Nargo.toml` files as well: + +```diff +-aztec = { git = "https://github.com/AztecProtocol/aztec-nr", tag = "v5.2.0", directory = "aztec" } ++aztec = { git = "https://github.com/aztec-labs-eng/aztec-nr", tag = "v<version>", directory = "aztec" } +``` + +`aztec compile` warns when a dependency still points at the previous repository. + +### [Aztec.nr] `DelayedPublicMutable` rejects delays below one hour + +`DelayedPublicMutable` now enforces a minimum delay of one hour (`aztec::state_vars::DELAYED_PUBLIC_MUTABLE_MINIMUM_DELAY`, 3600 seconds). Declaring a state variable with a shorter initial delay fails to compile, and `schedule_delay_change` reverts when passed a shorter delay: + +```noir +// Fails to compile: the delay is below DELAYED_PUBLIC_MUTABLE_MINIMUM_DELAY. +paused: DelayedPublicMutable<bool, 300, Context>, +``` + +Shorter delays make the time window users have to read chain state, produce a proof and get it included too short, effectively limiting access to the system, which is an unintended side-effect of controlling the delay. + +Raise any initial delay below one hour - a delay of at least a couple of hours remains the recommendation. + +### [CLI] Removed `bridge-erc20` + +`aztec bridge-erc20` performed only the L1 half of a token bridge deposit: it approved the ERC20 and called `depositToAztecPublic` (or `depositToAztecPrivate`) on a TokenPortal you supplied, then printed a claim secret for you to redeem on L2 yourself. Using it required already having deployed your own L1 TokenPortal and its L2 counterpart. + +Use `L1ToL2TokenPortalManager` from `@aztec-labs/aztec.js/ethereum` instead, which is what the command wrapped: + +```ts +import { L1ToL2TokenPortalManager } from '@aztec-labs/aztec.js/ethereum'; + +const manager = new L1ToL2TokenPortalManager(portalAddress, tokenAddress, handlerAddress, l1Client, logger); +const { claimAmount, claimSecret } = await manager.bridgeTokensPublic(recipient, amount, mint); +``` + +Fee juice bridging is unaffected: keep using `aztec-wallet bridge-fee-juice`. + +### [CLI] Removed `get-canonical-sponsored-fpc-address`, replaced by a wallet alias + +`aztec-wallet` now aliases the canonical SponsoredFPC as `contracts:SponsoredFPC` on startup, alongside the protocol and standard contracts, so commands can name it instead of taking its address: + +```bash +aztec-wallet register-contract contracts:SponsoredFPC SponsoredFPC --salt 0 +aztec-wallet send transfer --payment method=fpc-sponsored,fpc=contracts:SponsoredFPC ... +``` + +If you need the address itself (for a script or a non-wallet consumer), use `getSponsoredFPCAddress` from `@aztec-labs/cli/cli-utils`. + +### [CLI] Removed `inspect-contract`, `compute-selector`, `generate-secret-and-hash`, `parse-parameter-struct`, and `example-contracts` + +The `aztec` CLI no longer includes these contract-inspection helpers, which predate the current tooling and had no remaining use: + +- `aztec inspect-contract`: contract class details and function selectors are available programmatically via `getContractClassFromArtifact` and `FunctionSelector.fromSignature` in `@aztec-labs/aztec.js`. +- `aztec compute-selector`: use `FunctionSelector.fromSignature` from `@aztec-labs/aztec.js`. +- `aztec generate-secret-and-hash`: use `Fr.random()` and `computeSecretHash` from `@aztec-labs/aztec.js`. +- `aztec parse-parameter-struct`: no replacement. +- `aztec example-contracts`: the list of example contracts is the set of subpath exports of `@aztec-labs/noir-contracts.js`. + +### [Aztec.js] Contract artifacts preserve the names of `#[abi(tag)]` globals + +Globals exported from a Noir contract with `#[abi(tag)]` now keep their names in the artifact: entries in `ContractArtifact.outputs.globals` are `{ name, value }` objects as emitted by the compiler, where the names used to be stripped on load. This lets TypeScript read contract constants by name instead of duplicating their values: + +```noir +#[abi(constants)] +pub global EXPORTED_FIELD_CONSTANT: Field = 1234; +#[abi(constants)] +pub global EXPORTED_STRING_CONSTANT: str<8> = "exported"; +``` + +```typescript +import { getGlobalsByTag } from '@aztec-labs/aztec.js/abi'; + +const { EXPORTED_FIELD_CONSTANT, EXPORTED_STRING_CONSTANT } = getGlobalsByTag(MyContractArtifact, 'constants'); +``` + +There is no backwards compatibility path: artifacts compiled before Noir exported global names (with bare, unnamed entries) are rejected on load and must be recompiled with the current toolchain. The artifact hash commits to the contract artifact's ABI outputs (`ContractArtifact.outputs`), so adding names to `outputs.globals` changes the artifact hash, the contract class ID, and any addresses derived from it. The PXE data schema version was bumped accordingly, so existing PXE databases resync on first open. + +[Learn how to export globals under one or more tags and read them from TypeScript](../aztec-nr/framework-description/contract_artifact.md#exported-globals). + +### [Aztec.js] A function's return type is a single `returnType`, not a `returnTypes` list + +`FunctionAbi.returnTypes` is deprecated in favor of the single optional `FunctionAbi.returnType`, since multiple return values are already expressed as one `tuple` type. Read it through `getFunctionReturnType(abi)`, which also resolves artifacts serialized before `returnType` existed. `FunctionCall.returnTypes` is likewise replaced by `FunctionCall.returnType`. + +`decodeFromAbi` now takes the single type a function returns (or `undefined`) instead of a list, and returns `undefined` for a function that returns nothing, where it previously returned `[]`. To decode a list of values, such as a function's arguments, use the new `decodeEachFromAbi`. + +**Migration:** + +```diff +- decodeFromAbi(abi.returnTypes, values) ++ decodeFromAbi(getFunctionReturnType(abi), values) + +- decodeFromAbi(fn.parameters.map(param => param.type), args) as AbiDecoded[] ++ decodeEachFromAbi(fn.parameters.map(param => param.type), args) +``` + +### [Aztec.js] Protocol contracts removed from `@aztec-labs/noir-contracts.js` + +`@aztec-labs/noir-contracts.js` no longer includes the protocol contracts: the `FeeJuice`, `ContractClassRegistry`, and `ContractInstanceRegistry` artifacts and typed wrappers have been removed from the package, so imports such as `@aztec-labs/noir-contracts.js/FeeJuice` no longer resolve. These names are also no longer available to the `aztec` CLI's contract-name lookup (e.g. in `aztec example-contracts`). + +Protocol contracts are distributed via `@aztec-labs/protocol-contracts` (artifacts and canonical deployment data), and typed wrappers for them are exported from `@aztec-labs/aztec.js/protocol`. The `aztec.js` wrappers are bound to the contract's canonical address, so attaching takes only the wallet: there is no address parameter. + +**Migration:** + +```diff +- import { FeeJuiceContract } from '@aztec-labs/noir-contracts.js/FeeJuice'; ++ import { FeeJuiceContract } from '@aztec-labs/aztec.js/protocol'; + +- const feeJuice = FeeJuiceContract.at(ProtocolContractAddress.FeeJuice, wallet); ++ const feeJuice = FeeJuiceContract.withWallet(wallet); +``` + +### [Aztec.js] Protocol contract wrappers: `at(wallet)` deprecated in favor of `withWallet(wallet)` + +The protocol contract wrappers exported from `@aztec-labs/aztec.js/protocol` (`FeeJuiceContract`, `ContractClassRegistryContract`, `ContractInstanceRegistryContract`) rename their static `at(wallet)` to `withWallet(wallet)`. These wrappers are bound to the contract's canonical address, so their only parameter is the wallet to act through; `withWallet` states that directly and matches the existing `withWallet` instance method, whereas the one-argument `at` read as if it took an address. `at(wallet)` still works but is deprecated and will be removed in a future release. + +**Migration:** + +```diff +- const feeJuice = FeeJuiceContract.at(wallet); ++ const feeJuice = FeeJuiceContract.withWallet(wallet); +``` + +### [Aztec.nr] Domain separators moved out of the protocol constants module + +Nine `DOM_SEP__*` domain separators that used to live in the protocol constants module (`aztec::protocol::constants`, generated from `noir-protocol-circuits`) have moved into the `aztec` crate, next to the code that uses them. None of them were ever protocol constants (each hash is computed per-contract), so they no longer belong on the protocol export. + +Two remain public, at a new path: + +| Constant | Old path | New path | +| ---------------------------------- | ---------------------------- | --------------------------- | +| `DOM_SEP__PARTIAL_NOTE_COMMITMENT` | `aztec::protocol::constants` | `aztec::note::partial_note` | +| `DOM_SEP__NOTE_COMPLETION_LOG_TAG` | `aztec::protocol::constants` | `aztec::note::partial_note` | + +```diff +- use aztec::protocol::constants::{DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__PARTIAL_NOTE_COMMITMENT}; ++ use aztec::note::partial_note::{DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__PARTIAL_NOTE_COMMITMENT}; +``` + +The other seven are now crate-internal (`pub(crate)`) and can no longer be imported from outside the `aztec` crate: `DOM_SEP__AUTHWIT_NULLIFIER`, `DOM_SEP__TX_NULLIFIER`, `DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER`, `DOM_SEP__CONSTRAINED_MSG_NULLIFIER`, `DOM_SEP__ECDH_SUBKEY`, `DOM_SEP__ECDH_FIELD_MASK`, and `DOM_SEP__INITIALIZATION_NULLIFIER`. + +**Impact**: Contracts that use aztec-nr's high-level APIs (notes, authwit, state variables, message delivery, ECDH) are unaffected, since these separators are applied internally. A contract that imported one of these constants directly must either switch to the new `aztec::note::partial_note` path (for the two public ones) or, for the now-internal ones, call the corresponding aztec-nr helper instead of recomputing the hash by hand. The generated TypeScript `DomainSeparator` enum in `@aztec-labs/constants` / `@aztec-labs/stdlib` likewise no longer contains the seven removed members (their values were unused in TypeScript). + +Account contracts that implement their own entrypoint and hashed `DOM_SEP__TX_NULLIFIER` to emit the cancellation nullifier should call `compute_tx_nullifier` instead: + +```diff +- use aztec::protocol::{constants::DOM_SEP__TX_NULLIFIER, hash::poseidon2_hash_with_separator}; ++ use aztec::authwit::account::compute_tx_nullifier; + +- context.push_nullifier_unsafe(poseidon2_hash_with_separator([tx_nonce], DOM_SEP__TX_NULLIFIER)); ++ context.push_nullifier_unsafe(compute_tx_nullifier(tx_nonce)); +``` + +### [Aztec Node] `GasPrice` renamed to `FeesPerGas` in `@aztec-labs/ethereum` + +The `GasPrice` interface exported from `@aztec-labs/ethereum/l1-tx-utils` is now `FeesPerGas`, matching viem's name for the same thing, and the methods and fields carrying it are renamed to match. These values were never a price paid: they are the EIP-1559 caps a transaction is sent with (`maxFeePerGas`, `maxPriorityFeePerGas`, `maxFeePerBlobGas`), and what the transaction actually pays is decided at inclusion. The fields inside the type are unchanged. + +| Old | New | +| ------------------------------------------- | --------------------------------------------- | +| `GasPrice` | `FeesPerGas` | +| `ReadOnlyL1TxUtils.getGasPrice()` | `ReadOnlyL1TxUtils.getFeesPerGas()` | +| `L1TxState.gasPrice` | `L1TxState.feesPerGas` | +| `L1TxState.gasPriceHistory` | `L1TxState.feesPerGasHistory` | +| `TimedOutTxState.gasPriceHistory` | `TimedOutTxState.feesPerGasHistory` | +| `TimedOutTxState.finalGasPrice` | `TimedOutTxState.finalFeesPerGas` | +| `L1Deployer.sendTransaction()` → `gasPrice` | `L1Deployer.sendTransaction()` → `feesPerGas` | + +**Migration:** + +```diff +- import type { GasPrice } from '@aztec-labs/ethereum/l1-tx-utils'; ++ import type { FeesPerGas } from '@aztec-labs/ethereum/l1-tx-utils'; + +- const gasPrice = await l1TxUtils.getGasPrice(gasConfig, true, 0); ++ const feesPerGas = await l1TxUtils.getFeesPerGas(gasConfig, true, 0); + + const { state } = await l1TxUtils.sendTransaction(request); +- logger.info(`Sent at ${state.gasPrice.maxFeePerGas}`); ++ logger.info(`Sent at ${state.feesPerGas.maxFeePerGas}`); +``` + +**Impact**: A rename with no behavior change. Two on-disk formats move with it, neither of which needs operator action: + +- The node's L1 transaction state store writes the new key but still reads states written under the old one, so a node restarting on this version keeps monitoring transactions it sent before the upgrade. +- Failed-L1-transaction debug records (written when `L1_TX_FAILED_STORE` is set) now use `sentFeesPerGas` and `sentFeesPerGasLadder` instead of `sentGasPrice` and `sentGasPriceLadder`. Records written before the upgrade keep the old keys, and parsing one through `FailedL1TxSchema` returns it without those fields, though the values are still present in the stored JSON under the old names. + +## 5.2.0 + +### [Aztec.nr] Note types declared inside a contract must be `pub` + +Noir v1.0.0-beta.25 enforces the caller's visibility when comptime code resolves a trait method into a typed expression. The `#[aztec]` macro resolves each note type's `NoteType::get_id` that way when it generates `compute_note_hash_and_nullifier`, so a note struct declared directly inside the `contract` module without `pub` no longer compiles: + +``` +error: Function `get_id` is private + ┌─ .../aztec/src/macros/utils.nr:152:5 + │ `get_id` is declared in `MyContract` +``` + +Note types declared in their own module are unaffected, since they are already `pub` for the contract to reach them. + +**Migration:** + +```diff + #[derive(Packable, Eq)] + #[note] +- struct MyNote { ++ pub struct MyNote { + owner: AztecAddress, + } +``` + +### [Sequencer] A sequencer with p2p enabled no longer proposes while it has no connected peers + +The sequencer skips building and proposing a checkpoint when p2p is enabled and it is connected to fewer than `SEQ_MIN_PEERS_TO_PROPOSE` peers (default `1`). Proposing into an empty peer set burns the slot and can publish a checkpoint whose transaction data nobody received. L1-only duties (governance and slashing votes, prune, invalidation) are unaffected, and setups that disable p2p entirely (sandbox, local network, single node) still propose as before. + +A peerless node also warns periodically, stops filing data-withholding slashing offenses, and rejects incoming transactions until it reconnects. + +**Impact**: set `SEQ_MIN_PEERS_TO_PROPOSE=0` to restore the previous behaviour. Fixing the underlying peer connectivity is the real remedy. + +### [Node API] JSON-RPC internal errors now return code `-32603` + +The JSON-RPC server returned `-32600` ("Invalid Request") for internal server failures. It now returns `-32603` ("Internal error"), which is what the JSON-RPC 2.0 specification defines for that case. Genuine malformed-request errors still return `-32600`. + +**Impact**: clients that branch on `-32600` to detect an internal server failure must also handle `-32603`. + +### [Node API] `GET /status` now returns a JSON body with per-component health + +The status endpoint previously set only an HTTP status code and returned no body. It now responds with `application/json` describing per-component health, for example: + +```json +{ "ok": true, "components": { "p2p": { "healthy": true, "enabled": true, "connectedPeers": 12 } } } +``` + +`StatusCheckFn` widens from `() => boolean | Promise<boolean>` to also allow a structured result, and the new `ComponentStatus` / `ServerStatus` / `ServerStatusFn` types plus `SafeJsonRpcServer.getStatus()` are exported. The p2p component fails the check when the connected peer count is below `P2P_HEALTH_MIN_PEERS` (default `0`, which never fails). + +**Impact**: consumers that only read the HTTP status code are unaffected. Custom status callbacks that returned a bare boolean keep working. + +### [Aztec.js] `deserializeArrayFromVector` removed from `@aztec/foundation/serialize` + +`deserializeArrayFromVector` has been deleted from `@aztec/foundation`'s serialization helpers and is no longer exported. + +**Impact**: decode length-prefixed vectors with `BufferReader` (`reader.readVector(...)`) instead. + +## 5.1.0 + +### [Aztec.nr] Canonical HandshakeRegistry re-pinned at a new address + +The canonical `HandshakeRegistry` has been re-pinned so that it includes the owner's address in its `PrivateMutable` initialization nullifiers, keeping the handshake state of accounts that share keys independent. The registry moves to a new address. Handshakes established with the previous registry instance are not visible to the new one and must be re-established. The other standard contracts keep their addresses. + +### [Aztec.nr] Note property selectors are typed and use packed-layout indices + +The selectors in the generated `properties()` used the field's position in the note struct declaration, which pointed at the wrong packed field for any note with an earlier field packing to more than one `Field` (a `Point`, an array, a nested struct). Selector indices are now the field's offset in the note's packed representation, so `select`/`sort` criteria constrain the field they name. + +Breaking changes: + +- `PropertySelector<T>` carries the selected property's type. Hand-constructed literals need a type annotation, e.g. `let selector: PropertySelector<Field> = PropertySelector { index: 0, offset: 0, length: 32 };`. +- `select`/`sort` reject properties that pack to more than one `Field` at compile time. +- `select` takes its value typed as the property's type. Cast the value if a mixed-type comparison was intentional. +- `properties()` cannot be used with a custom `Packable` layout. Define property selectors manually for such notes. +- Every note field type must implement `Packable`, even when the note's own `Packable` is hand-written. + +## 5.0.1 + +### [Aztec.nr] History note nullification helpers renamed and restricted to own-contract notes + +The `history::note` helpers that recompute a note's nullifier have been renamed with a `local_` prefix and now assert that the note belongs to the executing contract: + +| Old | New | +| ---------------------------------- | ---------------------------------------- | +| `assert_note_was_valid_by` | `assert_local_note_was_valid_by` | +| `assert_note_was_nullified_by` | `assert_local_note_was_nullified_by` | +| `assert_note_was_not_nullified_by` | `assert_local_note_was_not_nullified_by` | + +These helpers derive the note's nullifier from the executing contract's app-siloed nullifier key, and it is not possible for a contract to retrieve the app-siloed nullifier for a different contract, as this would constitute leakage of key material. These helpers incorrectly used the local contract's key siloing, which for a note of a different contract resulted in the non-inclusion check passing unconditionally and wrongly reporting a nullified note as not nullified. The helpers now assert `contract_address == context.this_address()` and fail with "Note nullification history is only supported for the executing contract's own notes" otherwise. + +**Impact**: Update call sites to the new names. Own-contract usage (the common case) is unaffected beyond the rename. `assert_note_existed_by` is unchanged and still supports notes of any contract, since note-hash inclusion involves no keys. + +### [Aztec.nr] `set_as_fee_payer` now asserts it is called during the setup phase + +`PrivateContext::set_as_fee_payer` now asserts that execution is still in the setup (non-revertible) phase, i.e. that `end_setup` has not yet been called by any function in the transaction. Electing a fee payer in the revertible phase was never safe: compensation collected by the fee payer after `end_setup` can be discarded if a public call later reverts, while the protocol still debits the fee payer's fee-juice balance. + +**Impact**: A transaction in which `set_as_fee_payer` runs after the setup phase has ended now fails with `fee payer must be elected during the setup phase`. Standard fee payment flows, which call `set_as_fee_payer` before or together with `end_setup`, are unaffected. + +### [PXE] Stores are now selected by `(l1ChainId, rollupAddress, schemaVersion)` instead of being wiped on mismatch + +Previously, connecting a PXE or embedded wallet to a different or redeployed rollup, or bumping the store schema version, wiped the existing on-disk store in place. That meant master account keys could be destroyed simply by pointing a wallet at a different network. PXE data stores now exist per `(l1ChainId, rollupAddress, schemaVersion)` triple, and switching networks (or upgrading) selects or creates the matching store instead of overwriting previous ones. The embedded wallet's `wallet_data` store is partitioned the same way, so accounts and aliases are per network: switching networks starts with an empty account list until accounts are re-imported, and switching back finds the originals intact. + +**Impact**: The first start after upgrading to this version begins with a fresh, empty store; the pre-upgrade data is not deleted. On Node.js environments (lmdb-v2) pre-upgrade data stays at `<dataDirectory>/<name>` while new per-identity `pxe_data` stores live under `<dataDirectory>/<name>-stores/`. The embedded Node.js wallet previously stored data in cwd-relative, rollup-address-suffixed directories instead (`pxe_data_<rollupAddress>/pxe_data` for the PXE store, `wallet_data_<rollupAddress>/wallet_data` for the wallet store): if you used it before this release, that is where the old data lives. The embedded wallet now defaults its data root to `aztec-wallet-data/`, with per-identity wallet stores under `<dataDirectory>/wallet_data-stores/` on Node.js and OPFS store names prefixed `wallet_data_` in the browser. Browser apps can enumerate and clean up `pxe_data` and `wallet_data` stores for networks no longer in use with the new `listStores()` / `deleteStore()` utilities: + +```ts +import { deleteStore, listStores } from "@aztec/kv-store/sqlite-opfs"; + +const names = await listStores(); +await deleteStore(names[0]); +``` + +This change also removes `createStore` from `@aztec/kv-store/sqlite-opfs` and `@aztec/kv-store/deprecated/indexeddb`: stores are now opened by name with `AztecSQLiteOPFSStore.open` / `AztecIndexedDBStore.open`. + +## 5.0.0 + +### [PXE] Local PXE database is reset on upgrade + +The persisted tagging stores now key every entry by the self-describing `<kind>:<secret>:<app>` form of `AppTaggingSecret`; unconstrained secrets previously used a two-part `<secret>:<app>` key. This bumps the PXE data schema version, and there is no forward migration for the old keys: on first open the PXE clears any database whose stored schema version differs from the current one. The wipe resets the entire PXE store, not just the tagging data, because all of it shares one backing database. + +**Impact**: On upgrade your local PXE state is reset. You must re-register accounts and re-sync from genesis. Wallets should surface a "your local state was reset, please re-register accounts and re-sync" path. + +### [Aztec.nr] `TestEnvironmentOptions::with_tagging_secret_strategy` replaced + +`TestEnvironmentOptions::with_tagging_secret_strategy` is now `with_default_tag_secret_strategy_all_modes` for tests +that want the same default wallet strategy for both onchain delivery modes. The new naming reflects that these helpers +configure the TXE default wallet strategy hook; contract-fixed delivery derivations bypass that default. + +For mode-specific defaults and hook semantics, see the +[`resolveTaggingSecretStrategy` test helper docs](../foundational-topics/pxe/execution_hooks.md#resolvetaggingsecretstrategy). + +### [Aztec.nr] L1-to-L2 message consumption takes the secret as an array + +`PrivateContext::consume_l1_to_l2_message` and `PublicContext::consume_l1_to_l2_message` now take the message secret as an arbitrary-length array `[Field; N]` instead of a single `Field`, so a consumer can derive its secret hash from more than one field. The helpers `compute_secret_hash` and `compute_l1_to_l2_message_nullifier` are likewise now generic over the secret length. A single-field secret behaves exactly as before (the hashes are unchanged for `N = 1`) — just wrap it in an array. + +**Migration:** + +```diff +- context.consume_l1_to_l2_message(content, secret, sender, leaf_index); ++ context.consume_l1_to_l2_message(content, [secret], sender, leaf_index); +``` + +```diff +- let secret_hash = compute_secret_hash(secret); ++ let secret_hash = compute_secret_hash([secret]); +``` + +**Impact**: Contracts that consume L1-to-L2 messages, or that call `compute_secret_hash` / `compute_l1_to_l2_message_nullifier`, must wrap their single-field secret in an array. Already-deployed contracts are unaffected: their unchanged bytecode keeps working, as PXE serves the previous L1-to-L2 membership-witness oracle through a compatibility adaptor. + +### [Aztec.js] `computeL1ToL2MessageNullifier` replaced by `computeFeeJuiceMessageNullifier` + +The `@aztec/stdlib` helper `computeL1ToL2MessageNullifier(contract, messageHash, secret)`, which returned the siloed message nullifier, has been removed. Its replacement `computeFeeJuiceMessageNullifier(messageHash, secret)` returns the **unsiloed** nullifier — siloing now happens at the point where the nullifier is looked up. `getL1ToL2MessageWitness` accordingly takes an optional `{ contractAddress, nullifier }` (unsiloed) and silos internally. `computeSecretHash` is unchanged, and `getNonNullifiedL1ToL2MessageWitness` keeps the same signature. + +**Migration:** + +```diff +- const nullifier = await computeL1ToL2MessageNullifier(contract, messageHash, secret); ++ // computeFeeJuiceMessageNullifier returns the UNSILOED nullifier; silo it before looking it up, or hand the ++ // unsiloed value to getL1ToL2MessageWitness which silos for you. ++ const nullifier = await siloNullifier(contract, await computeFeeJuiceMessageNullifier(messageHash, secret)); +``` + +**Impact**: Only affects code calling these low-level messaging helpers directly - most integrations use `getNonNullifiedL1ToL2MessageWitness`, which is unchanged. + +### [Aztec.js] Account signing keys are no longer derived from the privacy secret + +Schnorr account signing keys used to be derived from the account's privacy secret (via the now-removed `deriveSigningKey`), which meant the ownership key could be reconstructed from a value the PXE holds. The relationship is now reversed: the signing key is the root, and the privacy secret is derived from it with `deriveSecretKeyFromSigningKey` (exported from `@aztec/accounts/utils`). + +As a result: + +- `deriveSigningKey` is removed. +- `getSchnorrAccountContractAddress` and `getSchnorrInitializerlessAccountContractAddress` now take the signing key first and an optional secret: `(signingPrivateKey, salt, secretKey?)`. When `secretKey` is omitted it is derived from the signing key. +- The embedded wallet's `createSchnorrAccount` and `createSchnorrInitializerlessAccount` now require an explicit signing key argument. + +**Migration:** + +```diff +- import { deriveSigningKey } from '@aztec/stdlib/keys'; +- const signingKey = deriveSigningKey(secret); +- const address = await getSchnorrAccountContractAddress(secret, salt, signingKey); ++ import { GrumpkinScalar } from '@aztec/aztec.js/fields'; ++ const signingKey = GrumpkinScalar.random(); // supply your own signing key ++ const address = await getSchnorrAccountContractAddress(signingKey, salt, secret); +``` + +**Impact**: Account addresses change, since both the signing key and the privacy secret feed the address. Code that derived the signing key from the secret, or passed the secret first to the address helpers, no longer compiles. + +### [Aztec.nr] `MessageContext` removed: message processing uses `ResolvedTx` + +`aztec::messages::processing::MessageContext` has been removed in favor of the new `ResolvedTx`, which carries the same `tx_hash`, `unique_note_hashes_in_tx`, and `first_nullifier_in_tx`, plus `block_number` and `block_hash`. The offchain handoff type `OffchainMessageWithContext` is likewise renamed to `OffchainMessageWithTx`. + +This affects contracts that implement a custom message handler (registered via `AztecConfig::custom_message_handler`): the handler's context parameter is now a `ResolvedTx`. + +**Migration:** + +```diff + use aztec::messages::processing::{ +- enqueue_event_for_validation, MessageContext, ++ enqueue_event_for_validation, ResolvedTx, + }; + + unconstrained fn handle_my_message( + // ... +- message_context: MessageContext, ++ resolved_tx: ResolvedTx, + scope: AztecAddress, + ) { +- // ...message_context.tx_hash... ++ // ...resolved_tx.tx_hash... + } +``` + +### [PXE] `pxe.updateContract` removed and `pxe.registerContract` no longer takes an artifact + +Registering classes and instances are now separate, unvalidated operations. `registerContractClass(artifact)` registers a class, `registerContract(instance)` registers an instance and no longer takes an artifact. `registerContract` does not check that PXE knows the contract's artifact: a missing artifact surfaces only when the contract is later simulated. + +**Migration:** + +- `pxe.registerContract` now takes the instance directly (its address preimage) and returns the derived address. Register the class separately via `registerContractClass`: + +```diff +- await pxe.registerContract({ instance, artifact }); ++ await pxe.registerContractClass(artifact); ++ await pxe.registerContract(instance); +``` + +If you were calling it without an artifact, just drop the wrapping object: `pxe.registerContract({ instance })` becomes `pxe.registerContract(instance)`. The `wallet.registerContract(instance, artifact?, secretKeyOrKeys?)` convenience is unchanged and performs both registrations for you. + +- To make a new class's code available after an onchain upgrade, register the new artifact instead of calling `updateContract`: + +```diff +- await pxe.updateContract(address, newArtifact); ++ await pxe.registerContractClass(newArtifact); +``` + +The new class is used automatically once the upgrade takes effect on chain; no further PXE action is needed. Registering it beforehand is harmless: until the update activates, the node still resolves the contract's current class to the previous one, so it keeps running its old code. + +- `pxe.getContractInstance(address)` and `wallet.getContractMetadata(address).instance` now return the contract's **address preimage**, which no longer includes `currentContractClassId`. + +### [Aztec.js] `AccountWithSecretKey` removed, read account keys from the `AccountManager` or PXE + +`AccountWithSecretKey` was a thin wrapper that bundled an account's transaction signer with its master secret key, used mainly to print or export the secret. It has been removed, and `AccountManager.getAccount()` now returns the plain `Account` signer. The wrapper's extra methods are no longer available on that value: + +- `getSecretKey()`: read it from the `AccountManager`, which still exposes `getSecretKey()`. +- `getEncryptionSecret()`: this was unused and has been removed. To recover an account's encryption (address) secret, pass its master incoming viewing secret key to `computeAddressSecret`. You can read that key, along with the account's other master secret keys, from `pxe.getAccountSecretKeys(address)`. + +**Migration:** + +```diff + +- import { AccountWithSecretKey } from '@aztec/aztec.js/account'; +- +- const account = await accountManager.getAccount(); +- const secretKey = account.getSecretKey(); ++ const secretKey = accountManager.getSecretKey(); +``` + +To do what `AccountWithSecretKey` was meant for (exporting an account into a separate PXE or wallet), account registration accepts a full set of master secret keys instead of only a single seed. `wallet.registerContract(instance, artifact?, secretKeyOrKeys?)` takes either an `Fr` seed (as before) or a `MasterSecretKeys` object (exported from `@aztec/aztec.js/keys`), for an account whose privacy keys were generated independently rather than from one seed. + +The PXE never receives the seed nor the message-signing and fallback secret keys: it is not trusted to hold them. The wallet derives the account's privacy keys and passes the PXE only the four privacy secret keys (nullifier-hiding, incoming-viewing, outgoing-viewing, tagging) plus the message-signing and fallback _public_ keys. Accordingly, `pxe.getAccountSecretKeys(address)` returns only those four privacy secret keys. + +**Impact**: Importing `AccountWithSecretKey`, or calling `getSecretKey()`/`getEncryptionSecret()` on the result of `getAccount()`, no longer compiles. The signer `getAccount()` returns is otherwise unchanged, and passing a single `Fr` or a `MasterSecretKeys` to `wallet.registerContract` keeps working. + +### [CLI] `aztec-wallet` `--secret-key` is renamed to `--signing-key` + +`aztec-wallet` accounts are now rooted on their signing key rather than on a privacy secret. The `--secret-key` option (on `create-account` and `simulate`) is renamed to `--signing-key`, and the `SECRET_KEY` environment variable to `SIGNING_KEY`. When creating an account, a random signing key is generated by default and the privacy secret is derived from it. + +**Migration:** + +```diff +- aztec-wallet create-account --secret-key 0x... ++ aztec-wallet create-account --signing-key 0x... +``` + +**Impact**: The value you save and restore for an account is now its signing key, and account addresses change, so existing wallet databases and funded addresses are not carried over. + +### [Bot] `senderPrivateKey` is now the account signing key + +The transaction bot previously derived its account's signing key from the configured `senderPrivateKey`. That value is now used directly as the signing key, with the privacy secret derived from it. + +**Impact**: The bot's account address changes for a given `senderPrivateKey`, so the account must be re-funded at its new address. + +### [PXE] Unconstrained delivery defaults to a non-interactive handshake for external recipients + +When no `resolveTaggingSecretStrategy` hook is configured, onchain unconstrained delivery now defaults to a non-interactive handshake when the recipient is external (an account whose keys the wallet does not hold), instead of an address-derived shared secret. A self-send (the recipient is one of the wallet's own accounts) still uses an address-derived secret, which needs no handshake and leaves no onchain trace. + +**Impact**: An external recipient can now discover unconstrained-delivered messages without having registered the sender in advance, but establishing the handshake publishes an onchain marker derived from the recipient's address (anyone who knows that address can tell a handshake was created for them, though not by whom nor the contents). Wallets that want the previous behavior can configure a `resolveTaggingSecretStrategy` hook that returns an `address-derived` strategy. + +### [Aztec.nr] `PrivateContext` data fields are no longer public + +`PrivateContext`'s data fields are now private (or crate-internal): its public API is now exclusively its methods. Contracts that read these fields directly must switch to the corresponding getter. A new `get_side_effect_counter()` getter exposes the side-effect counter, and a new `is_static_call()` getter replaces reaching into `inputs.call_context`. The `get_anchor_block_header()` getter already existed. + +**Migration:** + +```diff +- let header = context.anchor_block_header; ++ let header = context.get_anchor_block_header(); + +- let counter = context.side_effect_counter; ++ let counter = context.get_side_effect_counter(); + +- let is_static = context.inputs.call_context.is_static_call; ++ let is_static = context.is_static_call(); +``` + +**Impact**: Direct field access on `PrivateContext` (e.g. `context.anchor_block_header`, `context.side_effect_counter`, `context.inputs`) no longer compiles. Contract state should be read through the context's methods. + +### [PXE] Browser KV-store default is now SQLite-OPFS; the IndexedDB entrypoint moved and will be deprecated + +The browser PXE data store and the embedded wallet (`@aztec/wallets`) now persist to SQLite-OPFS instead of IndexedDB by default. The recommended way to obtain the browser backend is `@aztec/kv-store/sqlite-opfs`. + +**Migration:** + +```diff +- import { createStore } from '@aztec/kv-store/indexeddb'; ++ import { createStore } from '@aztec/kv-store/sqlite-opfs'; +``` + +If you must stay on IndexedDB for now, import from the deprecated entrypoint instead: + +```diff +- import { createStore } from '@aztec/kv-store/indexeddb'; ++ import { createStore } from '@aztec/kv-store/deprecated/indexeddb'; +``` + +**Impact**: Existing IndexedDB-backed data is not migrated, so browser PXE and wallet state starts fresh on SQLite-OPFS (the v5 protocol upgrade wipes local state regardless). SQLite-OPFS also holds an exclusive, origin-wide lock on its store directory, so a second browser tab opening the same store will fail. Consequently, we recommend to explicitly manage this case in your app if it uses `EmbeddedWallet`. + +### [Aztec.js] `getPublicEvents` is now cursor-paginated + +`getPublicEvents` returns a single page of events (at most `MAX_LOGS_PER_TAG`, the node's per-tag page size) and pages instead of the `maxLogsHit` flag, which didn't provide any way to fetch the next page of events: + +- The result's `maxLogsHit` boolean is replaced by `nextCursor`. When `nextCursor` is present, more events might exist; pass it as the next query's `afterEvent` to fetch the following page. When it is absent, the range is exhausted. +- The filter's `afterLog` cursor is renamed to `afterEvent`. +- Both cursors are the new `EventCursor` type (exported from `@aztec/aztec.js/events`), not the node-layer `LogCursor`. + +**Migration:** + +```diff +- const { events, maxLogsHit } = await getPublicEvents(node, MyContract.events.MyEvent, { contractAddress }); ++ // One page: ++ const { events, nextCursor } = await getPublicEvents(node, MyContract.events.MyEvent, { contractAddress }); ++ ++ // All events: ++ const all = []; ++ let afterEvent; ++ do { ++ const page = await getPublicEvents(node, MyContract.events.MyEvent, { contractAddress, afterEvent }); ++ all.push(...page.events); ++ afterEvent = page.nextCursor; ++ } while (afterEvent); +``` + +**Impact**: Reading `maxLogsHit` or passing `afterLog` no longer compiles. Previously a single call was silently capped at `MAX_LOGS_PER_TAG` events with no usable way to continue, so the old API was unusable anyway. You can now page through the full set with `afterEvent`/`nextCursor`. + +### [PXE] Sender and shared-secret registration unified into `TaggingSecretSource` + +The PXE methods for registering tagging-secret sources have been replaced by a single set that takes a `TaggingSecretSource` discriminated union. `registerSender`/`getSenders`/`removeSender` and `registerSharedSecret`/`removeSharedSecret` are gone; use `registerTaggingSecretSource`/`removeTaggingSecretSource`/`getTaggingSecretSources` instead. The `Wallet` interface (`wallet.registerSender`, `getAddressBook`) is unchanged, so this only affects code that talks to a `PXE` instance directly. + +| Before | After | +| --------------------------------------------- | ---------------------------------------------------------------------------------- | +| `pxe.registerSender(address)` | `pxe.registerTaggingSecretSource({ kind: 'address-derived', sender: address })` | +| `pxe.removeSender(address)` | `pxe.removeTaggingSecretSource({ kind: 'address-derived', sender: address })` | +| `pxe.getSenders()` | `pxe.getTaggingSecretSources({ kind: 'address-derived' })` | +| `pxe.registerSharedSecret(recipient, secret)` | `pxe.registerTaggingSecretSource({ kind: 'arbitrary-secret', recipient, secret })` | +| `pxe.removeSharedSecret(recipient, secret)` | `pxe.removeTaggingSecretSource({ kind: 'arbitrary-secret', recipient, secret })` | + +### [Aztec.js] Unchecked `AztecAddress` constructors renamed with an `Unsafe` suffix + +The synchronous `AztecAddress` constructors that build an address from a raw value do not verify that the value is a valid address (the x-coordinate of a point on the Grumpkin curve, which is what allows it to be encrypted to). An invalid value is accepted silently and only fails later, when a transaction is sent. To make this obvious at the call site, they now carry an `Unsafe` suffix: + +| Before | After | +| ------------------------- | ------------------------------- | +| `AztecAddress.fromField` | `AztecAddress.fromFieldUnsafe` | +| `AztecAddress.fromBigInt` | `AztecAddress.fromBigIntUnsafe` | +| `AztecAddress.fromNumber` | `AztecAddress.fromNumberUnsafe` | +| `AztecAddress.fromString` | `AztecAddress.fromStringUnsafe` | + +**Migration:** + +```diff +- const address = AztecAddress.fromBigInt(123n); ++ const address = AztecAddress.fromBigIntUnsafe(123n); +``` + +For a random, genuinely valid address in tests use `AztecAddress.random()`, and to check an untrusted value use `address.isValid()`. The serialization constructors `fromBuffer` and `fromFields` keep their names (they are part of the (de)serialization interface and read addresses from already-validated data), but their docs now note that they perform no validation either. + +### Cross-contract utility calls now have a `msg_sender` + +A utility function called by another contract (utility to utility, or private to utility) can read the calling contract's address via `self.msg_sender()`, mirroring private and public functions. A top-level utility call (e.g. invoked directly by a wallet or dapp) has no caller: `self.msg_sender()` panics, and `self.context.maybe_msg_sender()` returns `Option::none()`. + +`msg_sender` is only set for cross-contract calls, where it is taken from the call graph and so cannot be forged. A directly-invoked utility (from a wallet or dapp) has no verifiable caller, so it exposes none rather than trusting a value the caller could pick freely. + +#### [Aztec.nr] `ExecuteUtilityOptions` gains `with_from` to simulate a cross-contract caller in tests + +In `TestEnvironment`, use `execute_utility_opts` with the new `ExecuteUtilityOptions::with_from` builder method to set the `msg_sender` a utility observes, simulating a cross-contract caller without routing through an actual nested call (by default it observes no caller): + +```rust +let secret = env.execute_utility_opts( + ExecuteUtilityOptions::new().with_from(caller), + Registry::at(registry_address).get_app_siloed_secret(sender, recipient), +).map(|secrets| secrets.shared); +``` + +### [Prover Node JSON-RPC] Prover API moved to the admin endpoint; `getL2Tips`/`getWorldStateSyncStatus` removed + +The prover node's JSON-RPC methods (`prover_*`) have moved off the public node RPC server and onto the admin RPC server. They now require the admin API key and are served on the admin port (8880) instead of the public port (8080). + +In addition, `prover_getL2Tips` and `prover_getWorldStateSyncStatus` have been removed from the prover API. They duplicated data already served by the node: use `aztec_getChainTips` (same shape as the old `getL2Tips`) and `aztec_getWorldStateSyncStatus` instead. + +If you call the prover RPC directly (e.g. via `curl`), point at the admin endpoint with the API key and use the remaining methods: + +- `prover_startProof` — schedule proving for an epoch +- `prover_getJobs` — list proving jobs + +A client factory `createProverNodeAdminClient(url, versions?, fetch?, apiKey?)` is now exported from `@aztec/stdlib/interfaces/server`, and the CLI exposes `aztec prover start-proof --epoch <n> --admin-url <url> --api-key <key>` and `aztec prover get-jobs` (the API key defaults to `AZTEC_ADMIN_API_KEY`). + +### [Aztec.nr] `ContractInstance.contract_class_id` renamed to `original_contract_class_id` + +The `contract_class_id` field of the `ContractInstance` struct (returned by `get_contract_instance`) has been renamed to `original_contract_class_id`. The struct is the contract's _address preimage_, so this field is the class id the contract was deployed with: for contracts whose class was later updated via the `ContractInstanceRegistry`, it is NOT the class currently executing. The rename makes that explicit. + +**Migration:** + +```diff +let instance = get_contract_instance(address); +- let class_id = instance.contract_class_id; ++ let class_id = instance.original_contract_class_id; +``` + +Note that this value is not available during public execution, which only has access to the _current_ contract class. + +### [Aztec.nr] `get_contract_instance_class_id_avm` renamed to `get_contract_instance_current_class_id_avm` + +The AVM contract-instance class id getter has been renamed to make explicit that it returns the _current_ class id, i.e. it reflects updates performed via the `ContractInstanceRegistry`. + +**Migration:** + +```diff +- use aztec::oracle::get_contract_instance::get_contract_instance_class_id_avm; ++ use aztec::oracle::get_contract_instance::get_contract_instance_current_class_id_avm; + +- let class_id = get_contract_instance_class_id_avm(address); ++ let class_id = get_contract_instance_current_class_id_avm(address); +``` + +### [Aztec.nr] `for_each` visits elements in order; removing during iteration no longer supported + +`CapsuleArray::for_each` and `EphemeralArray::for_each` previously iterated backwards (from the last element to the first) so that the callback could safely remove the current element. They now visit elements in order, from first to last, as is usually expected in other languages. Structurally mutating the array (e.g. via `push` or `remove`) from inside the callback is no longer supported. + +For `EphemeralArray`, replace remove-during-iteration with `filter`: + +```diff +- array.for_each(|index, value| { +- if should_remove(value) { +- array.remove(index); +- } +- }); ++ let kept = array.filter(|value| !should_remove(value)); +``` + +`filter` collects the kept elements into a fresh array at a new slot. If the original slot matters (e.g. a `TransientArray` slot shared with other call frames), rebuild it from the filtered result: + +```noir +let kept = array.filter(|value| !should_remove(value)); +let _ = array.clear(); +kept.for_each(|_index, value| array.push(value)); +``` + +`EphemeralArray`'s are cheap and by nature not persistent though, so in most cases you probably can just work with the new copy instead of going through this hassle. + +`CapsuleArray` has no `filter`, so iterate manually, backwards. Removing the current element is safe in a backward loop because it only shifts elements at higher indices: + +```noir +let mut i = array.len(); +while i > 0 { + i -= 1; + if should_remove(array.get(i)) { + array.remove(i); + } +} +``` + +### [Protocol] `PrivateCircuitPublicInputs` and `PrivateContextInputs` gain a `tx_request_salt` field + +The init kernel now always injects the protocol nullifier (`H(tx_request)`) as the transaction's first nullifier and binds the entry-point proof to the tx request's salt. To support this, `PrivateCircuitPublicInputs` and `PrivateContextInputs` each carry a new `tx_request_salt` field (set by the framework), and the `first_nullifier_hint` input to the init kernel is removed. This is handled automatically by the framework and PXE; contracts using the standard entrypoint need no changes beyond recompiling against the new protocol circuits. + +The salt serves two purposes. It keeps `H(tx_request)` (the protocol nullifier) unpredictable, preventing a dictionary attack that guesses the tx-request preimage to recompute the nullifier. It also lets the init kernel bind the proof to a specific tx request: the kernel asserts `tx_request_salt` equals `tx_request.salt`, so a third party holding the proof cannot rebind it to a different request (and thus a different protocol nullifier). + +Because `tx_request_salt` is now part of the private function's public inputs, an app or account contract can read it. Together with the other public inputs (the call context, `args_hash`, `tx_context`, and the function selector), an entrypoint can reconstruct the full `tx_request` and verify a signature over `tx_request.hash()`, instead of over a separate payload. This lets an account authorize the complete, kernel-checked transaction request rather than just the calls it contains. Building on that, the protocol nullifier can be made to provide a transaction's replay protection and cancellation directly, reducing the number of nullifiers an account needs to emit. This is not done by the standard entrypoint today; see issues #461 (the protocol/design direction) and #462 (the Aztec.nr account-layer work) for the full picture. + +### [Aztec.js] Prefunded local network test accounts are now initializerless + +The genesis-funded test accounts in the local network (sandbox), returned by `getInitialTestAccountsData()`, are now initializerless Schnorr accounts (`schnorr_initializerless`). An initializerless account has no onchain deployment transaction: its address commits to the signing public key (through `immutables_hash`) and its contract state is materialized locally in the PXE, so these accounts are usable right away. + +Because their address is derived differently from a regular Schnorr account, register them with `createSchnorrInitializerlessAccount` rather than `createSchnorrAccount`. + +**Migration:** + +```diff +const [alice] = await getInitialTestAccountsData(); +- await wallet.createSchnorrAccount(alice.secret, alice.salt); ++ await wallet.createSchnorrInitializerlessAccount(alice.secret, alice.salt); +``` + +### [Aztec.js] `AccountContract` interface adds `getImmutablesHash()` + +The `AccountContract` interface now declares `getImmutablesHash(): Promise<Fr | undefined>`, which returns the hash of the account's immutable instantiation parameters committed into its address, or `undefined` if the feature is not used. `getAccountContractAddress()` and `AccountManager.create()` call it to derive the address when an `immutablesHash` is not passed explicitly, so the address of an initializerless account is now resolved from the contract itself. + +Account contracts that extend `DefaultAccountContract` inherit a default implementation that returns `undefined` and need no changes. + +### [Aztec.js] Wallets validate declared gas limits against the network's per-tx admission limit + +Wallets now reject a transaction whose declared `gasLimits` exceed the network's per-tx admission limit (the node-advertised `txsLimits.gas`), throwing before the tx is sent — e.g. `Declared DA gas limit (X) exceeds the maximum this network allows per tx (Y)`. When you declare no gas limits, the wallet fills in the network's admission limits for you. This mirrors the node's inbound `GasLimitsValidator`, surfacing the rejection locally instead of on submission. + +**Impact**: Transactions that previously over-declared gas (and were silently skipped by the proposer) now fail fast with a descriptive error. Declare limits at or below `txsLimits.gas`, or declare none and let the wallet fill them in. + +### [Aztec.js] `estimateGas` / `estimatedGasPadding` simulate options and the `estimatedGas` result field removed + +The `estimateGas` and `estimatedGasPadding` fee options are gone, and the `estimatedGas` field on simulation results is replaced by `gasUsed` (the raw gas the simulation consumed). Apps that want explicit gas limits read `gasUsed` and pad it themselves; otherwise the wallet fills in the network's admission limits automatically. + +**Migration:** + +```diff +- const { estimatedGas } = await contract.methods.foo(args).simulate({ +- from, +- fee: { estimateGas: true, estimatedGasPadding: 0.1 }, +- }); +- const gasLimits = estimatedGas.gasLimits; ++ const { gasUsed } = await contract.methods.foo(args).simulate({ from, includeMetadata: true }); ++ const gasLimits = gasUsed.totalGas.mul(1.1); // pad yourself +``` + +### [Aztec.js] `getGasLimits` moved to `@aztec/wallet-sdk` and is no longer exported from `@aztec/aztec.js` + +`getGasLimits` is no longer exported from `@aztec/aztec.js`. It now lives in `@aztec/wallet-sdk/base-wallet`, takes the simulated `gasUsed` and the network's per-tx admission limit, and clamps the padded estimates to it. If the simulated usage already exceeds the network limit, it throws immediately rather than returning a limit the node would reject. + +**Migration:** + +```diff +- import { getGasLimits } from '@aztec/aztec.js'; +- const { gasLimits, teardownGasLimits } = getGasLimits(simulationResult, 0.1); ++ import { getGasLimits } from '@aztec/wallet-sdk/base-wallet'; ++ const { txsLimits } = await node.getNodeInfo(); ++ const { gasLimits, teardownGasLimits } = getGasLimits(simulationResult.gasUsed, Gas.from(txsLimits.gas), 0.1); +``` + +### [Aztec.js / PXE] `NodeInfo.txsLimits` is now required + +`NodeInfo` now carries a required `txsLimits` field: every node advertises the maximum gas a single tx may declare (`{ gas: { daGas, l2Gas } }`) and wallets rely on it for fallback gas limits. Clients built against this version cannot talk to nodes that predate the field. + +### [Aztec.js] `GasSettings.fallback` requires explicit `gasLimits` + +`GasSettings.fallback` no longer supplies a default value for `gasLimits`. Callers must pass the network's per-tx admission limit explicitly — read it from the node's `txsLimits.gas`. + +**Migration:** + +```diff +- const settings = GasSettings.fallback({ maxFeesPerGas }); ++ const { txsLimits } = await node.getNodeInfo(); ++ const settings = GasSettings.fallback({ gasLimits: Gas.from(txsLimits.gas), maxFeesPerGas }); +``` + +### [Aztec.js / stdlib] Removed legacy fallback gas constants + +The following exports have been removed from `@aztec/stdlib`: + +- `APPROXIMATE_MAX_DA_GAS_PER_BLOCK` +- `FALLBACK_TEARDOWN_L2_GAS_LIMIT` +- `FALLBACK_TEARDOWN_DA_GAS_LIMIT` + +**Impact**: Any code that imported these symbols must switch to the live node-advertised limits via the node's `txsLimits.gas`. + +### [Aztec.nr] `messages::message_delivery` module moved to `messages::delivery` + +The `message_delivery` module has been renamed to `delivery`. Update imports accordingly: + +```diff +- use aztec::messages::message_delivery::MessageDelivery; ++ use aztec::messages::delivery::MessageDelivery; +``` + +### [Node JSON-RPC] Method prefixes changed to `aztec_*` and `aztecAdmin_*` + +All Aztec node JSON-RPC method prefixes have changed: + +- `node_*` → `aztec_*` (public node methods, port 8080) +- `nodeAdmin_*` → `aztecAdmin_*` (admin methods, port 8880) +- `nodeDebug_*` → `aztecDebug_*` (debug methods, port 8080, local-network or `--node-debug` only) +- `p2p_*` namespace removed; P2P queries are on `aztec_*`: `getPeers`, `getCheckpointAttestationsForSlot`, `getProposalsForSlot` +- New archiver sync helpers on `aztec_*`: `getL1Constants`, `getSyncedL2SlotNumber`, `getSyncedL2EpochNumber`, `getSyncedL1Timestamp` + +If you call the node RPC directly (e.g. via `curl` or a custom client), update all method names accordingly. +Clients created via `createAztecNodeClient`, `createAztecNodeAdminClient`, and `createAztecNodeDebugClient` are updated automatically. + +### [Node RPC] `registerContractFunctionSignatures` moved to the debug API + +`registerContractFunctionSignatures` is no longer part of the main node JSON-RPC API (`aztec_` namespace). It is now a debug-only method exposed under the `aztecDebug_` namespace, which is only mounted when the node runs with debug endpoints enabled (`--node-debug`, always on in the in-process sandbox). This removes an unauthenticated write to node memory from prod-like nodes. + +Clients that registered public function signatures over `aztec_registerContractFunctionSignatures` should call `aztecDebug_registerContractFunctionSignatures` against a debug-enabled node instead. In the PXE, this is now driven by an optional debug client: pass a `nodeDebug` client (e.g. `createAztecNodeDebugClient(nodeUrl)`) when creating the PXE to keep named public-execution traces; when it is absent, signature registration is skipped. Client-side error enrichment from the contract ABI is unaffected. + +### [Aztec.nr] `get_pending_tagged_logs` oracle interface updated (oracle version 28) + +The `aztec_utl_getPendingTaggedLogs` oracle now takes an additional `provided_secrets` parameter of type `EphemeralArray<ProvidedSecret>`. This lets apps pass tagging secrets that PXE cannot derive on its own (e.g. handshake-derived secrets) alongside the secrets PXE manages internally. + +### [Aztec.nr] `set_sender_for_tags` oracle removed + +The `set_sender_for_tags` oracle has been removed. Contracts that used it to override the sender for discovery tag derivation should now use the `with_sender` builder method on `MessageDelivery`: + +```diff +- use aztec::oracle::notes::set_sender_for_tags; ++ use aztec::messages::delivery::MessageDelivery; + +- unsafe { set_sender_for_tags(some_address) }; +- note.deliver(MessageDelivery::onchain_constrained()); ++ note.deliver(MessageDelivery::onchain_constrained().with_sender(some_address)); +``` + +When `with_sender` is not called, `MessageDelivery` uses the wallet-supplied default sender. + +### [Aztec.nr] `MessageDelivery` API syntax change + +`MessageDelivery` variants are now accessed via constructor functions instead of dot notation: + +```diff +- MessageDelivery.OFFCHAIN ++ MessageDelivery::offchain() + +- MessageDelivery.ONCHAIN_UNCONSTRAINED ++ MessageDelivery::onchain_unconstrained() + +- MessageDelivery.ONCHAIN_CONSTRAINED ++ MessageDelivery::onchain_constrained() +``` + +### [Aztec.js] `getTxReceipt` returns a lifecycle union and takes `GetTxReceiptOptions` + +`AztecNode.getTxReceipt` now takes an optional `GetTxReceiptOptions` argument and returns a `PendingTxReceipt | DroppedTxReceipt | MinedTxReceipt` union instead of a single `TxReceipt` class. Mined-only fields (`transactionFee`, `blockHash`, `blockNumber`, `txIndexInBlock`, and the execution result) are only guaranteed once the transaction is mined, so narrow with `isMined()`, `isPending()`, or `isDropped()` before reading variant-specific fields. The full `TxEffect` is attached only when you request it via `{ includeTxEffect: true }`. + +`AztecNode.getTxEffect` is deprecated. Replace direct calls with `getTxReceipt(txHash, { includeTxEffect: true })` and read the `.txEffect` field. + +`TxReceipt.empty()` and `TxReceipt.schema` no longer exist, since `TxReceipt` is now a union type rather than a class. Use `DroppedTxReceipt.empty()` and `TxReceiptSchema` instead. + +```diff +- const effect = await node.getTxEffect(txHash); +- if (effect) { +- console.log(effect.data.nullifiers.length); +- } ++ const receipt = await node.getTxReceipt(txHash, { includeTxEffect: true }); ++ if (receipt.isMined() && receipt.txEffect) { ++ console.log(receipt.txEffect.nullifiers.length); ++ } + +- const empty = TxReceipt.empty(); +- const schema = TxReceipt.schema; ++ const empty = DroppedTxReceipt.empty(); ++ const schema = TxReceiptSchema; +``` + +**Impact**: This is a breaking change to the public node RPC with no wire back-compat. Callers that read mined fields off a bare receipt must narrow with the `is*` guards first, callers of `getTxEffect` should migrate to `getTxReceipt`, and references to `TxReceipt.empty()` / `TxReceipt.schema` must move to `DroppedTxReceipt.empty()` / `TxReceiptSchema`. + +### [Aztec.js] `ExtendedDirectionalAppTaggingSecret` renamed to `AppTaggingSecret` + +`ExtendedDirectionalAppTaggingSecret` has been renamed to `AppTaggingSecret`. + +**Migration:** + +```diff +- import { ExtendedDirectionalAppTaggingSecret } from '@aztec/stdlib/logs'; ++ import { AppTaggingSecret } from '@aztec/stdlib/logs'; + +- ExtendedDirectionalAppTaggingSecret.fromString(value) ++ AppTaggingSecret.fromString(value) +``` + +**Impact**: Code importing or referencing `ExtendedDirectionalAppTaggingSecret` should update to `AppTaggingSecret`. + +### [Protocol] Remaining protocol-contract addresses compacted to 1-3 + +After the `auth_registry`, `public_checks`, and `multi_call_entrypoint` demotions freed up address slots `1`, `4`, and `6`, the three remaining protocol contracts have been compacted into the lowest slots: `ContractClassRegistry` moves from `3` to `1`, `ContractInstanceRegistry` stays at `2`, and `FeeJuice` moves from `5` to `3`. Code that hardcoded the previous values must be updated. `MAX_PROTOCOL_CONTRACTS` is unchanged (still `11`); only the assigned addresses moved. + +### [Aztec.nr] `multi_call_entrypoint` demoted from protocol contract + +`multi_call_entrypoint` is no longer a protocol contract; its address is derived from its artifact rather than hardcoded at `6`, and PXE no longer auto-registers it. It is now a standard contract that PXE _preloads_: both `createPXE` and `EmbeddedWallet` preload the standard MultiCallEntrypoint automatically (and `EmbeddedWallet` additionally preloads `AuthRegistry`). **If you use the standard PXE or `EmbeddedWallet`, no changes are needed** — multicall keeps working out of the box. + +To preload a different set of standard contracts (for example to also preload `PublicChecks`, which is not preloaded by default), a wallet or app passes its own `preloadedContractsProvider` through the wallet's PXE options: + +```ts +const wallet = await EmbeddedWallet.create(node, { + pxe: { + preloadedContractsProvider: { + // EmbeddedWallet's built-in default preloads only MultiCallEntrypoint + AuthRegistry. + // A custom provider REPLACES that default (it is not additive), so re-list the ones you + // still want and add the extras — here, PublicChecks. + getPreloadedContracts: async () => [ + await getStandardMultiCallEntrypoint(), + await getStandardAuthRegistry(), + await getStandardPublicChecks(), + ], + }, + }, +}); +``` + +The provider _replaces_ the default list (it is not additive), so include every standard contract you want available. + +### [Aztec.nr] `public_checks` demoted from protocol contract + +`public_checks` is no longer a protocol contract. Its address is now derived from its artifact rather than hardcoded at `6`. The aztec-nr constant has moved and been renamed: + +```diff +- use protocol_types::constants::PUBLIC_CHECKS_ADDRESS; ++ use crate::standard_addresses::STANDARD_PUBLIC_CHECKS_ADDRESS; +``` + +Unlike MultiCallEntrypoint and AuthRegistry, `PublicChecks` is **not** preloaded by `createPXE` or `EmbeddedWallet`. If your contract uses `privately_check_timestamp` or `privately_check_block_number`, `PublicChecks` must be deployed and made available to your PXE — either include it in a custom `preloadedContractsProvider` (see the `multi_call_entrypoint` note above) or register it directly: + +```ts +import { getStandardPublicChecks } from "@aztec/standard-contracts/public-checks"; + +const { instance, artifact } = await getStandardPublicChecks(); +await pxe.registerContract({ instance, artifact }); +``` + +For browser bundles, import from `@aztec/standard-contracts/public-checks/lazy` instead. + +Deploy `PublicChecks` once per fresh rollup: `aztec-wallet deploy public_checks_contract@PublicChecks --salt 1 --universal -f <fee-paying-account>`. + +### [Aztec.nr] `auth_registry` demoted from protocol contract + +`auth_registry` is no longer a protocol contract. Its address is now derived from its artifact rather than hardcoded at `1`. The aztec-nr constant has moved and been renamed: + +```diff +- use protocol_types::constants::CANONICAL_AUTH_REGISTRY_ADDRESS; ++ use crate::standard_addresses::STANDARD_AUTH_REGISTRY_ADDRESS; +``` + +PXE no longer auto-registers `AuthRegistry` on startup. `EmbeddedWallet` preloads it automatically (alongside the MultiCallEntrypoint — see the `multi_call_entrypoint` note above), so apps using the standard wallet need no changes. If you use a PXE setup that doesn't preload it, add it to a custom `preloadedContractsProvider` or register it explicitly: + +```ts +import { getStandardAuthRegistry } from "@aztec/standard-contracts/auth-registry"; + +const { instance, artifact } = await getStandardAuthRegistry(); +await pxe.registerContract({ instance, artifact }); +``` + +For browser bundles, import from `@aztec/standard-contracts/auth-registry/lazy` instead. + +Deploy `AuthRegistry` once per fresh rollup: `aztec-wallet deploy auth_registry_contract@AuthRegistry --salt 1 --universal -f <fee-paying-account>`. + +### [Aztec.nr] `public_checks` helpers moved to `aztec-nr` + +The `privately_check_timestamp`, `privately_check_block_number`, and related caller helpers previously in `noir-contracts/contracts/protocol/public_checks_contract/src/utils.nr` are now in `aztec-nr/aztec/src/public_checks.nr`. Consumer contracts should update their imports: + +```diff +- use public_checks::utils::privately_check_timestamp; ++ use aztec::public_checks::privately_check_timestamp; +``` + +### [Aztec Node / Aztec.js / CLI] Log retrieval API consolidated to two tag-based methods + +The four log-retrieval methods on `AztecNode` have been collapsed into two. `getContractClassLogs` and the `LogFilter`-shaped `getPublicLogs` are removed entirely; the surviving methods are `getPrivateLogsByTags(query)` and `getPublicLogsByTags(query)`, both taking a single query object and returning `LogResult[][]` (one inner array per requested tag, in input order). + +**Removed methods on `AztecNode`:** + +| Removed | Replacement | +| ------------------------------------------------------------------------- | ----------------------------------------------------- | +| `getPublicLogs(filter: LogFilter)` | `getPublicLogsByTags({ contractAddress, tags, ... })` | +| `getContractClassLogs(filter: LogFilter)` | none — RPC removed; no production consumer existed | +| `getPrivateLogsByTags(tags, page?, referenceBlock?)` | `getPrivateLogsByTags({ tags, ... })` | +| `getPublicLogsByTagsFromContract(contract, tags, page?, referenceBlock?)` | `getPublicLogsByTags({ contractAddress, tags, ... })` | + +**New query and response shapes:** + +```ts +// Query +type TagQuery<T> = T | { tag: T; afterLog?: LogCursor }; + +type LogsQueryBase = { + fromBlock?: BlockNumber; // inclusive + toBlock?: BlockNumber; // exclusive + txHash?: TxHash; // mutually exclusive with fromBlock/toBlock + referenceBlock?: BlockHash; // reorg-safety anchor; throws if missing + includeEffects?: boolean; // attach noteHashes + all nullifiers +}; + +type PrivateLogsQuery = LogsQueryBase & { tags: TagQuery<SiloedTag>[] }; +type PublicLogsQuery = LogsQueryBase & { + contractAddress: AztecAddress; + tags: TagQuery<Tag>[]; +}; + +// Response (per log) +type LogResult = { + logData: Fr[]; + blockNumber: BlockNumber; + blockHash: BlockHash; + blockTimestamp: UInt64; + txHash: TxHash; + logIndexWithinTx: number; + noteHashes?: Fr[]; // present only when includeEffects is set + nullifiers?: Fr[]; // all nullifiers of the tx, not just the first +}; +``` + +**Public queries now require a contract address.** Tag-only / contract-less public queries are no longer supported (the public log index is keyed on `(contract, tag)`). + +**Per-tag `afterLog` cursors replace the global `page` argument.** Each tag advances independently — pass `{ tag, afterLog: LogCursor.fromLog(lastLog) }` to resume that tag, and omit it for tags that are already exhausted. + +**Aztec.js wallet — `PublicEventFilter.contractAddress` is now required, and `afterLog` is a `LogCursor`:** + +```diff +- type PublicEventFilter = EventFilterBase & { contractAddress?: AztecAddress }; ++ type PublicEventFilter = EventFilterBase & { contractAddress: AztecAddress }; + + type EventFilterBase = { + txHash?: TxHash; + fromBlock?: BlockNumber; + toBlock?: BlockNumber; +- afterLog?: LogId; ++ afterLog?: LogCursor; + }; +``` + +`LogId`, `LogFilter`, `TxScopedL2Log`, `ExtendedPublicLog`, `ExtendedContractClassLog`, `GetPublicLogsResponse`, and `GetContractClassLogsResponse` are no longer exported from `@aztec/aztec.js`. Build cursors with `LogCursor.fromLog(log)` and decode public-event payloads from `result.logData.slice(1)` (the tag is field 0). + +**CLI — `aztec get-logs` now requires `--contract-address` and `--tag`:** + +```diff +- aztec get-logs [--tx-hash <tx>] [--from-block <n>] [--to-block <n>] [--after-log <id>] ++ aztec get-logs --contract-address <address> --tag <tag> \ ++ [--tx-hash <tx>] [--from-block <n>] [--to-block <n>] [--after-log <cursor>] +``` + +`--after-log` now takes a `LogCursor` of the form `<blockNumber>-<txIndexWithinBlock>-<logIndexWithinTx>` (formerly a `LogId`). + +**Mutual exclusion**: setting both `txHash` and `fromBlock`/`toBlock` is rejected (a `txHash` already pins a block). `txHash` + `afterLog` is allowed and paginates within the tx's logs for a tag. + +**Impact**: Any consumer of `getPublicLogs(LogFilter)`, `getContractClassLogs`, the old tag-based methods, `PublicEventFilter` without a `contractAddress`, or `EventFilterBase.afterLog: LogId` must be updated. The CLI rejects calls missing `--contract-address` or `--tag`. + +### [Aztec.js] `AccountManager.create` takes an options bag + +`AccountManager.create` no longer takes `salt` as a positional argument. The trailing `salt?: Salt` parameter has been folded into a new `AccountManagerCreateOptions` bag alongside `immutablesHash` and `deployer`: + +```diff +- AccountManager.create(wallet, secret, accountContract, salt) ++ AccountManager.create(wallet, secret, accountContract, { salt }) +``` + +`immutablesHash` lets callers commit a non-zero immutables hash on the resulting `ContractInstance` (folded into the salted initialization hash, so it affects the derived address). `deployer` overrides the deployer address recorded on the instance (defaults to `AztecAddress.ZERO`). The same `immutablesHash` field is now also threaded through `DeployMethod` / `DeployAccountMethod` so the address derived at deploy time matches the one on `accountManager.getInstance()`. + +### [Aztec.nr] Defining a custom `sync_state` function now requires `AztecConfig` + +Contracts that previously overrode the default `sync_state` by defining their own function with that name will now get a compile error. Use `AztecConfig::custom_sync_state()` instead. + +The custom hook receives the same parameters as `do_sync_state` and is responsible for calling it if default behavior is also desired. You can perform work before and/or after the default `do_sync_state` call, or skip it entirely. + +```diff ++ unconstrained fn my_custom_sync( ++ contract_address: AztecAddress, ++ compute_note_hash: ComputeNoteHash, ++ compute_note_nullifier: ComputeNoteNullifier, ++ process_custom_message: Option<CustomMessageHandler>, ++ offchain_inbox_sync: Option<OffchainInboxSync>, ++ scope: AztecAddress, ++ ) { ++ // optional: work before default sync ++ do_sync_state(contract_address, compute_note_hash, compute_note_nullifier, process_custom_message, offchain_inbox_sync, scope); ++ // optional: work after default sync ++ } + +- #[aztec] ++ #[aztec(::aztec::macros::AztecConfig::new().custom_sync_state(crate::my_custom_sync))] + contract MyContract { +- use aztec::macros::functions::external; +- +- #[external("utility")] +- unconstrained fn sync_state(scope: AztecAddress) { +- // custom sync logic +- } + } +``` + +**Impact**: Only contracts that manually defined a `sync_state` function are affected. Contracts using the default macro-generated `sync_state` require no changes. + +### [Aztec.nr] `push_nullifier` renamed to `push_nullifier_unsafe` + +`PrivateContext::push_nullifier` and `PublicContext::push_nullifier` have been renamed to `push_nullifier_unsafe` to +make it clear that they are low-level functions that require careful domain separation. This is consistent with the +`_unsafe` suffix already used by `emit_private_log_unsafe`, `emit_raw_note_log_unsafe`, and `emit_public_log_unsafe`. + +```diff +- context.push_nullifier(nullifier); ++ context.push_nullifier_unsafe(nullifier); +``` + +Prefer higher-level abstractions like `SingleUseClaim` or `destroy_note` which handle domain separation automatically. + +### [Aztec.nr] `LogRetrievalRequest` now includes `source`, `from_block`, and `to_block` fields + +`LogRetrievalRequest` has been extended with three new fields to support filtering logs by source and block range. The `get_logs_by_tag` oracle now also returns all matching logs per tag instead of only the first match. + +A `LogRetrievalRequest::new(contract_address, tag)` constructor is provided that defaults to querying both public and private logs with no block range filter: + +```rust +LogRetrievalRequest::new(contract_address, my_tag) +``` + +If you need to customize source or block range, construct the struct manually with the new fields: + +```diff + LogRetrievalRequest { + tag: my_tag, ++ source: LogSource.PUBLIC_AND_PRIVATE, ++ from_block: Option::none(), ++ to_block: Option::none(), + } +``` + +`source` controls which RPCs are queried: `LogSource.PRIVATE`, `LogSource.PUBLIC`, or `LogSource.PUBLIC_AND_PRIVATE`. `from_block` and `to_block` define a half-open `[from, to)` block range filter. Both are `Option<Field>` and default to `Option::none()` (no filtering). + +### [Protocol] Public-key hashes replace points in `PublicKeys` + +Ships together with immutables hash changes (shown below). + +Per [AZIP-8](https://github.com/AztecProtocol/governance/blob/main/AZIPs/azip-8.md), `PublicKeys` no longer carries the four master public keys as elliptic curve points. Three of them (`npk_m`, `ovpk_m`, `tpk_m`) are now exposed only as their poseidon2 hash digests; only `ivpk_m` (the master incoming viewing key) remains a point because address derivation needs it as a curve point. + +**This is a hard fork:** every contract address and account address derived from a non-default `PublicKeys` changes. + +**Contract author migration.** Read the master nullifier hash directly off `PublicKeys` instead of computing it from a point: + +```diff +- let owner_npk_m = get_public_keys(owner).npk_m; +- let secret = context.request_nhk_app(owner_npk_m.hash()); ++ let owner_npk_m_hash = get_public_keys(owner).npk_m_hash; ++ let secret = context.request_nhk_app(owner_npk_m_hash); +``` + +The same field-rename applies to `.ovpk_m` and `.tpk_m`: these are now `.ovpk_m_hash` and `.tpk_m_hash` respectively. Code that needed those keys as points will not compile; the points are no longer accessible to contract code. + +**Custom account contracts.** Wallets that ship their own Noir account contracts must recompile. Macro-generated calldata extraction and the `request_nsk_app` / `request_ovsk_app` paths use the hash form natively. + +**TS / wallet author migration.** The `PublicKeys` constructor signature changes from four `Point`s to `(npkMHash: Fr, ivpkM: Point, ovpkMHash: Fr, tpkMHash: Fr)`. `KeyValidationRequest` carries `pkMHash: Fr` instead of `pkM: Point`. `KeyStore.getMasterSecretKey` now takes a `pkMHash: Fr` rather than a `Point`. Callers using the auto-generated TS binding pick this up automatically; callers that hand-roll the arg buffer must update. + +**Wallet UI.** Any panel that displayed `masterNullifierPublicKey`, `masterOutgoingViewingPublicKey`, or `masterTaggingPublicKey` as Grumpkin points will no longer compile against the new `PublicKeys` class. The points themselves are no longer in `ContractInstancePublished` and cannot be recovered from the onchain record. Switch to displaying the hashes (`npkMHash`, `ovpkMHash`, `tpkMHash`) or drop the display. + +**PXE storage migration.** `DatabaseVersionManager` deletes pre-v6 databases on first open: users will see registered accounts, contacts, address aliases, and synced notes wiped. Wallets should surface a "your local state was reset, please re-register accounts and re-sync" path. There is no forward migration because the address derived from a given secret changes (the new `public_keys_hash` is over four single-key digests, not four raw points). Previous addresses are not recoverable from the same secret; assets and notes attached to them are inaccessible at the protocol level. + +**Indexer / event-decoder migration.** The `ContractInstancePublished` private log payload is now 13 fields: + +```text +[ MAGIC, address, version, salt, class_id, init_hash, + immutables_hash, + npk_m_hash, + ivpk_m.x, ivpk_m.y, + ovpk_m_hash, + tpk_m_hash, + deployer ] +``` + +`version` is `2`. v1 events should be rejected. + +**Security note (PXE side).** The kernel circuit no longer checks that `npk_m`, `ovpk_m`, `tpk_m` are on-curve or non-infinity (those points are no longer in the witness). The PXE / key store relies on `deriveKeys`'s by-construction guarantee that derived points are on-curve and non-infinity. Account-creation flows that bypass `deriveKeys` (e.g. importing pre-derived public keys from an external source) must validate this themselves, or risk producing unspendable notes. + +### [Contracts] `ContractInstance` gains `immutablesHash`, address derivation changes + +`ContractInstance` now has a new `immutablesHash: Fr` field that commits to a contract's immutable storage values. The field is folded into the salted initialization hash, so contract addresses are impacted: + +``` +salted_initialization_hash = poseidon2(DOM_SEP__SALTED_INITIALIZATION_HASH, [salt, initialization_hash, deployer, immutables_hash]) +``` + +**You may need to act if:** + +- You hardcode contract addresses computed from instance fields outside the SDK. Recompute them under the new derivation. +- You parse the `ContractInstancePublished` private log directly. The event payload has an extra field, with `immutables_hash` inserted between `initialization_hash` and the public-keys block: + + ``` + [tag, address, version, salt, classId, initialization_hash, immutables_hash, ...publicKeys(5), deployer] + ``` + +- You call `ContractInstanceRegistry.publish_for_public_execution` directly. The function now takes 6 arguments instead of 5, with `immutables_hash` inserted between `initialization_hash` and `public_keys`: + + ```diff + - publish_for_public_execution(salt, contract_class_id, initialization_hash, public_keys, universal_deploy) + + publish_for_public_execution(salt, contract_class_id, initialization_hash, immutables_hash, public_keys, universal_deploy) + ``` + +- You call the `GetContractInstance` AVM opcode directly or use the per-member helpers in `aztec-nr`. A new enum value `ContractInstanceMember::IMMUTABLES_HASH = 3` selects `immutables_hash`. Use the wrapper helper from `aztec::oracle::get_contract_instance`: + + ```rust + use aztec::oracle::get_contract_instance::get_contract_instance_immutables_hash_avm; + let immutables_hash: Option<Field> = get_contract_instance_immutables_hash_avm(address); + ``` + +The `aztec.js` `publishInstance` helper handles this automatically. + +### [Aztec.nr] `emit_private_log_unsafe` / `emit_raw_note_log_unsafe` now take `BoundedVec` + +The old array-based `emit_private_log_unsafe(tag, log: [Field; N], length)` and `emit_raw_note_log_unsafe(tag, log: [Field; N], length, note_hash_counter)` have been removed. The temporary `_vec_unsafe` variants introduced in a prior release have been renamed to take their place. + +```diff +- context.emit_private_log_unsafe(tag, log_array, length); ++ context.emit_private_log_unsafe(tag, bounded_vec_log); + +- context.emit_raw_note_log_unsafe(tag, log_array, length, note_hash_counter); ++ context.emit_raw_note_log_unsafe(tag, bounded_vec_log, note_hash_counter); +``` + +If you were already using `emit_private_log_vec_unsafe` / `emit_raw_note_log_vec_unsafe`, simply drop the `_vec` from the function name: + +```diff +- context.emit_private_log_vec_unsafe(tag, log); ++ context.emit_private_log_unsafe(tag, log); + +- context.emit_raw_note_log_vec_unsafe(tag, log, note_hash_counter); ++ context.emit_raw_note_log_unsafe(tag, log, note_hash_counter); +``` + +### [bb.js / accounts / aztec.nr] Schnorr signatures switched to Poseidon2 + +The Schnorr challenge hash function changed from `blake2s(pedersen(R.x, pubkey.x, pubkey.y) ‖ message)` to `Poseidon2(DST, R.x, pubkey.x, pubkey.y, message)`, where `DST = poseidon2_hash_bytes("schnorr_grumpkin_poseidon2")` is a domain separation tag binding signatures to this scheme. The change applies end-to-end across the native signer (`bb`), `@aztec/bb.js`, the noir verifier library (`noir-lang/schnorr` v0.2.0 → v0.4.0), and both standard Schnorr account contracts. The auth witness on-wire shape also changes from `[u8; 64]` (the serialized `(s, e)` bytes) to `[Field; 4]` (`[s.lo, s.hi, e.lo, e.hi]`, each scalar split into two 128-bit limbs). + +**Impact:** A previously-deployed Schnorr account cannot be controlled by the new TypeScript code. Both the signature scheme and the auth witness format change, so signatures produced by the new code will fail in-circuit verification against the old account contract, and old-style 64-byte auth witnesses will not decode in the new contract. Users with existing Schnorr accounts on testnet must deploy a fresh account contract and migrate funds. ECDSA accounts (`ecdsa_k`, `ecdsa_r`) are unaffected. + +**If you maintain a custom Schnorr account contract**, bump the `schnorr` dependency in `Nargo.toml`: + +```diff +- schnorr = { tag = "v0.2.0", git = "https://github.com/noir-lang/schnorr" } ++ schnorr = { tag = "v0.4.0", git = "https://github.com/noir-lang/schnorr" } +``` + +and update `is_valid_impl` to consume the auth witness as four `Field` limbs and pass `outer_hash` directly: + +```diff +- let signature: [u8; 64] = unsafe { get_auth_witness_as_bytes(outer_hash) }; +- schnorr::verify_signature(pub_key, signature, outer_hash.to_be_bytes::<32>()) ++ let limbs: [Field; 4] = unsafe { get_auth_witness(outer_hash) }; ++ let signature = ( ++ std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[0], limbs[1]), ++ std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[2], limbs[3]), ++ ); ++ schnorr::verify_signature(pub_key, signature, outer_hash) +``` + +The `Schnorr` TypeScript API in `@aztec/foundation/crypto/schnorr` keeps the same surface (`constructSignature(msg: Uint8Array, ...)`, `verifySignature(msg, ...)`), but the `msg` parameter is now required to be exactly 32 bytes — a serialized field element (e.g. `Fr.toBuffer()` or `messageHash.toBuffer()`). Passing arbitrary-length byte strings will fail at the bb.js boundary. + +### [Aztec.nr] `attempt_note_discovery` is no longer exposed; use `process_private_note_msg` + +`attempt_note_discovery` is now crate-private. Custom message handlers (implementations of `CustomMessageHandler`) that previously called it directly should call `process_private_note_msg` instead, which runs the standard private note message decoding and discovery pipeline. + +`process_private_note_msg` takes the raw `msg_metadata` and `msg_content` rather than already-decoded note fields, so it handles decoding (and silently discards undecodable messages) on your behalf: + +```diff +- attempt_note_discovery( +- contract_address, +- tx_hash, +- unique_note_hashes_in_tx, +- first_nullifier_in_tx, +- compute_note_hash, +- compute_note_nullifier, +- owner, +- storage_slot, +- randomness, +- note_type_id, +- packed_note, +- ); ++ process_private_note_msg( ++ contract_address, ++ tx_hash, ++ unique_note_hashes_in_tx, ++ first_nullifier_in_tx, ++ compute_note_hash, ++ compute_note_nullifier, ++ msg_metadata, ++ msg_content, ++ ); +``` + +**Impact**: Custom message handlers that reused the standard note message processing pipeline must switch to `process_private_note_msg`. Contracts using only built-in private note handling are unaffected. + +### [Aztec.nr] TXE `call_public_incognito` no longer takes a `from` parameter + +`TestEnvironment::call_public_incognito` previously accepted a `from` address that was silently ignored (the function always uses a null `msg_sender`). The `from` parameter has been removed. + +```diff +- env.call_public_incognito(sender, SampleContract::at(addr).some_function()); ++ env.call_public_incognito(SampleContract::at(addr).some_function()); +``` + +If you need to call a public function _with_ a sender, use `call_public` instead. + +### [Aztec.nr] TXE `view_public_incognito` is deprecated + +`TestEnvironment::view_public_incognito` is now deprecated in favor of `view_public`, which has the same behavior (null `msg_sender`, static call). + +```diff +- env.view_public_incognito(SampleContract::at(addr).some_view()); ++ env.view_public(SampleContract::at(addr).some_view()); +``` + +### [Aztec.js] `DeployMethod` address-affecting parameters move to construction time + +Salt, deployer, and public keys are now passed when the `DeployMethod` is constructed, not on every call to `send` / `simulate` / `request` / `getInstance`. This locks the contract address once it is determined and prevents the silent salt-cache poisoning bug where the address could change between calls. + +`contractAddressSalt`, `deployer`, and `universalDeploy` have been removed from `DeployOptions`, `RequestDeployOptions`, and `SimulateDeployOptions`. They now live on a new `DeployInstantiationOptions` argument passed at construction. `deployer` and `universalDeploy` are mutually exclusive; passing both throws. `Contract.deployWithPublicKeys` and the generated `MyContract.deployWithPublicKeys(...)` factories have been removed; pass `publicKeys` via the `instantiation` argument of `deploy(...)` instead. The buggy synchronous `address` and `partialAddress` getters have been removed and replaced with `getAddress()` and `getPartialAddress()` (both `async`). + +The compact form keeps working: `MyContract.deploy(wallet, ...args).send({ from: alice })` deploys with `deployer = alice` and `salt = random()`, exactly as before. The deployer is locked the first time `send` / `simulate` / `profile` is called (from `options.from`, with `NO_FROM` or undefined → universal) and cannot change after that: + +- Subsequent `send` / `simulate` / `profile` calls with a `from` that would imply a different deployer throw, instead of silently producing a different address. +- A lock to universal (`AztecAddress.ZERO`) is the only one compatible with any sender, since the universal address does not depend on `from`. +- A lock to a concrete address only accepts that exact `from` on subsequent calls. + +**Migration:** + +Universal deployment with a fixed salt: + +```diff +- const deploy = MyContract.deploy(wallet, ...args); +- await deploy.send({ +- from: alice, +- contractAddressSalt: salt, +- universalDeploy: true, +- }); ++ const deploy = MyContract.deploy(wallet, ...args, { salt, universalDeploy: true }); ++ await deploy.send({ from: alice }); +``` + +Non-universal deploy where `from` doubles as the deployer: + +```diff +- const deploy = MyContract.deploy(wallet, ...args); +- await deploy.send({ from: alice, contractAddressSalt: salt }); ++ const deploy = MyContract.deploy(wallet, ...args, { salt }); ++ await deploy.send({ from: alice }); +``` + +If you need to read the address before sending, lock the deployer at construction: + +```typescript +const deploy = MyContract.deploy(wallet, ...args, { salt, deployer: alice }); +const address = await deploy.getAddress(); // resolves; deployer was locked at construction +await deploy.send({ from: alice }); // deploys at the address `getAddress` returned +``` + +Universal deploys can be sent by any account, since the universal address does not depend on `from`: + +```typescript +const deploy = MyContract.deploy(wallet, ...args, { universalDeploy: true }); +await deploy.send({ from: bob }); // OK, universal accepts any sender +``` + +A lock to a concrete deployer rejects sending from a different account, instead of silently deploying at a different address: + +```typescript +const deploy = MyContract.deploy(wallet, ...args, { deployer: alice }); +await deploy.send({ from: bob }); // throws: deployer is locked to alice +``` + +`deployWithPublicKeys` is gone; pass `publicKeys` in the instantiation options instead: + +```diff +- const deploy = MyContract.deployWithPublicKeys(publicKeys, wallet, ...args); ++ const deploy = MyContract.deploy(wallet, ...args, { publicKeys }); +``` + +`ContractDeployer.deploy(...)` now takes the instantiation argument as its first parameter (pass `{}` to use defaults and rely on lazy locking from `from`): + +```diff +- const cd = new ContractDeployer(artifact, wallet); +- await cd.deploy(...ctorArgs).send({ from: alice, contractAddressSalt: salt }); ++ const cd = new ContractDeployer(artifact, wallet); ++ await cd.deploy(ctorArgs, { salt }).send({ from: alice }); +``` + +The synchronous `address` / `partialAddress` getters are gone: + +```diff +- const address = deploy.address; // sync, possibly undefined +- const partial = await deploy.partialAddress; // sync getter wrapping async value ++ const address = await deploy.getAddress(); // requires the deployer to be locked ++ const partial = await deploy.getPartialAddress(); // requires the deployer to be locked +``` + +`getInstance()` no longer takes options; use the construction-time instantiation instead: + +```diff +- const instance = await deploy.getInstance({ contractAddressSalt: salt }); ++ const deploy = MyContract.deploy(wallet, ...args, { salt, deployer: alice }); ++ const instance = await deploy.getInstance(); +``` + +### [aztec-up] Bundled binaries are no longer exposed under bare names on `PATH` + +The Aztec installer previously placed bundled binaries directly into `$HOME/.aztec/current/bin` under bare names (`forge`, `nargo`, `bb`, `pxe`, ...). Anything with the same name in your own `PATH` was silently shadowed in unrelated projects. + +Every bundled binary is now exposed only under an `aztec-` prefixed name in `$HOME/.aztec/current/bin`. Bare names are not on `PATH` at all and resolve to your own install (if any). + +| Was on `PATH` | Now | +| ------------------ | ------------------------ | +| `forge` | `aztec-forge` | +| `cast` | `aztec-cast` | +| `anvil` | `aztec-anvil` | +| `chisel` | `aztec-chisel` | +| `nargo` | `aztec-nargo` | +| `noir-profiler` | `aztec-noir-profiler` | +| `bb` | `aztec-bb` | +| `bb-cli` | `aztec-bb-cli` | +| `pxe` | `aztec-pxe` | +| `txe` | `aztec-txe` | +| `validator-client` | `aztec-validator-client` | +| `blob-client` | `aztec-blob-client` | + +`aztec`, `aztec-wallet`, and `aztec-up` keep their existing names. + +If you relied on a bundled bare-name binary for general use: + +- For Aztec contract work, prefer `aztec compile` and `aztec test`. +- For other Noir / Foundry commands, invoke the `aztec-*` symlink directly (e.g. `aztec-nargo fmt`, `aztec-forge build`). +- Or install Foundry / nargo separately via `foundryup` / `noirup`. + +If you set `Noir: Nargo Path` in the VS Code Noir extension to `$HOME/.aztec/current/bin/nargo`, change it to `$HOME/.aztec/current/bin/aztec-nargo` (the symlink is a drop-in for `nargo`). See the [Noir VSCode Extension guide](../aztec-nr/installation.md) for details. + +### [Stdlib] `SimulationOverrides.contracts` entries no longer carry an artifact + +`ContractOverrides` entries are now `{ instance }` only. To override a contract's artifact, pre-register the target class via `pxe.registerContractClass(artifact)` and set the override instance's `currentContractClassId` to that class id: + +```diff +- const instance = await getContractInstanceFromInstantiationParams(stubArtifact, { salt: Fr.random() }); ++ const instance = await pxe.getContractInstance(addr); ++ await pxe.registerContractClass(stubArtifact); ++ const stubClassId = (await getContractClassFromArtifact(stubArtifact)).id; +- overrides = { contracts: { [addr.toString()]: { instance, artifact: stubArtifact } } }; ++ overrides = { contracts: { [addr.toString()]: { instance: { ...instance, currentContractClassId: stubClassId } } } }; +``` + +### [Aztec.js] `simulate` accepts `overrides` for testing "what if storage value was X?" + +`Contract.methods.foo(...).simulate(...)` now accepts an `overrides` option that injects values into the simulator's (ephemeral) world-state fork and contract DB before the call runs. The supported field is `publicStorage`, which writes a `(contract, slot, value)` into the public-data tree as if a previous tx had set it. Overrides are thrown away after simulation completes. + +```typescript +const result = await contract.methods.read_balance(account).simulate({ + overrides: { + publicStorage: [ + { + contract: contract.address, + slot: BALANCE_SLOT, + value: new Fr(1_000_000n), + }, + ], + }, +}); +``` + +The same option flows through `wallet.simulateTx` and eventually to `simulatePublicCalls` RPC on `AztecNode`. + +Direct callers of the `SimulationOverrides` constructor must switch from a positional `contracts` argument to an options bag: + +```diff +- new SimulationOverrides(contracts); ++ new SimulationOverrides({ contracts }); +``` + +`overrides.contracts` swaps contract instances in the simulator's contract DB — useful for simulating a contract being on a different class than the one it was deployed with. To simulate a complete onchain upgrade flow, use the `fastForwardContractUpdate` helper which returns a `SimulationOverrides` covering both registry storage rewrites and the upgraded instance entry: + +```typescript +import { fastForwardContractUpdate } from "@aztec/aztec.js"; + +const overrides = await fastForwardContractUpdate({ + instanceAddress: contract.address, + newClassId: upgradedClass.id, + node, +}); +const result = await contract.methods.upgraded_method().simulate({ overrides }); +``` + +### [PXE] `proveTx` takes an options bag + +`PXE.proveTx` used to accept `scopes` as a positional argument; it now takes an options bag consistent with `simulateTx` and `profileTx`, and adds an optional `senderForTags` field. Update direct callers: + +```diff +- pxe.proveTx(txRequest, scopes); ++ pxe.proveTx(txRequest, { scopes }); +``` + +The new `senderForTags` field sets the address recipients use to find private messages (notes, events, logs) emitted by this tx. Most wallets don't need to set it; the wallet SDK derives it from the tx's `from` address: + +```typescript +// Most callers: just migrate scopes +pxe.proveTx(txRequest, { scopes }); + +// When from === NO_FROM (e.g. self-paid account deploy), supply the tag sender explicitly: +pxe.proveTx(txRequest, { scopes, senderForTags: deployedAddress }); +``` + +### [Aztec.nr] `set_sender_for_tags` is now scoped to the calling contract + +`set_sender_for_tags` previously persisted through nested calls. It is now scoped to the contract that calls it: nested calls, siblings, and parents are unaffected and always start from the initial default. This closes a silent note-discovery DoS vector where any nested callee could overwrite the tag sender for legitimate contracts called below it. + +The wallet SDK now supplies the default sender-for-tags from the transaction's `from` address, with an optional `sendMessagesAs` override for flows that don't have a signing account (e.g. self-paid deploys, which `DeployAccountMethod` sets automatically). Account contracts therefore no longer need to call `set_sender_for_tags(self.address)` in their entrypoints; those calls have been removed from the standard schnorr and ECDSA account contracts, and you can drop the equivalent call in any custom account contract. + +The save/restore idiom previously used in account-contract constructors (`get` → `set(self.address)` → work → `set(prev)`) is also no longer needed and has been removed: the override never leaks out of the constructor, so there is nothing to restore. + +### [Aztec Node] Unified `getBlock` / `getCheckpoint` RPC API + +The Aztec Node JSON-RPC surface for fetching blocks and checkpoints has been consolidated. The unified `getBlock` and `getCheckpoint` methods return uniform `BlockResponse` / `CheckpointResponse` shapes. The extra fields a caller cares about (tx bodies, L1 publish info, committee attestations, nested blocks) are now controlled by an `options` argument rather than by picking the right method. `getBlocks` and `getCheckpoints` retain their names but now return the new response shapes. + +**Removed methods:** + +| Removed | Replacement | +| ---------------------------------- | -------------------------------------------- | +| `getBlockByHash(hash)` | `getBlock(hash)` or `getBlock({ hash })` | +| `getBlockByArchive(archive)` | `getBlock({ archive })` | +| `getBlockHeaderByArchive(archive)` | `getBlock({ archive }).then(r => r?.header)` | +| `getProvenBlockNumber()` | `getBlockNumber('proven')` | +| `getCheckpointedBlockNumber()` | `getBlockNumber('checkpointed')` | + +**Deprecated but still present** (scheduled for removal once internal consumers of the archiver shape are rewired): `getL2Tips` (use `getChainTips`), `getBlockHeader` (use `getBlock(param).then(r => r?.header)`), `getCheckpointedBlocks` (use `getBlocks(from, limit, { includeL1PublishInfo: true, includeAttestations: true })`). Do not adopt these in new code. (`getCheckpointsDataForEpoch` was previously listed here; see the dedicated checkpoint-API entry below for its removal.) + +**New response shapes:** `BlockResponse` always carries `header`, `archive`, `hash`, `number`, `checkpointNumber`, and `indexWithinCheckpoint`. `body`, `l1` (an `L1PublishInfo` discriminated union), and `attestations` are present only when the matching include option is set. `CheckpointResponse` mirrors this for checkpoints, with `blocks` gated on `includeBlocks`, and always carries `feeAssetPriceModifier` as a base field. The response types are generic over the options object, so passing a literal `{ includeTransactions: true }` narrows the return type and `response.body` becomes non-optional. + +**Nested blocks on `getCheckpoint`:** only `includeTransactions` is forwarded to the blocks embedded by `includeBlocks: true`. `includeL1PublishInfo` and `includeAttestations` on a checkpoint request attach L1 / attestation data to the checkpoint itself, not to its nested blocks. + +**Return type changes for `getBlocks` / `getCheckpoints`:** the return type is now `BlockResponse[]` / `CheckpointResponse[]` instead of `L2Block[]` / `PublishedCheckpoint[]`. Callers that previously consumed fields of `L2Block` (e.g. `.body`) must now opt in via `{ includeTransactions: true }`; callers that consumed `PublishedCheckpoint.checkpoint.blocks` must opt in via `{ includeBlocks: true }`. + +**Migration for wallet/SDK consumers (`@aztec/aztec.js`, `@aztec/wallet-sdk`):** + +```diff +- const block = await node.getBlockByHash(hash); ++ const block = await node.getBlock(hash, { includeTransactions: true }); + +- const archiveBlock = await node.getBlockByArchive(archive); ++ const archiveBlock = await node.getBlock({ archive }, { includeTransactions: true }); + +- const provenNumber = await node.getProvenBlockNumber(); ++ const provenNumber = await node.getBlockNumber('proven'); + +- const checkpointedNumber = await node.getCheckpointedBlockNumber(); ++ const checkpointedNumber = await node.getBlockNumber('checkpointed'); + +- const tips = await node.getL2Tips(); ++ const tips = await node.getChainTips(); +``` + +`getBlockHeader`, `getCheckpointedBlocks`, `getCheckpointsDataForEpoch`, and `getL2Tips` continue to work in this release but are deprecated; migrate to the replacements above. + +**Chain-tip selectors:** `getBlockNumber` and `getCheckpointNumber` now accept an optional `ChainTip` argument (`'proposed' | 'checkpointed' | 'proven' | 'finalized'`). The `'proposed'` semantics are described in the dedicated checkpoint-API entry below — they were tightened in this release to mean "the proposed-tip checkpoint" rather than "the latest L1-confirmed checkpoint." + +**Block parameter variants:** `BlockParameter` now also accepts a block hash, an archive root, and chain-tip names. The existing `number | 'latest'` forms continue to work — `'latest'` is an alias for `'proposed'`. + +**Impact**: Source changes are required anywhere the removed methods are called. Type changes are required anywhere `L2Block` / `BlockHeader` / `CheckpointedL2Block` were consumed from the RPC — those call sites now receive `BlockResponse` / `CheckpointResponse` and must request the fields they need via `options`. Production nodes will reject JSON-RPC calls to the removed method names. + +### [Aztec Node] Checkpoint RPC: `'proposed'` is now strictly proposed; `'latest'` removed; `getCheckpointsData` takes a query + +Follow-up to the unified-RPC change above. Tightens the checkpoint-side API surface: removes the old positional / per-shape entrypoints, drops the wire-level alias that conflated proposed and confirmed checkpoints, and replaces the deprecated epoch-only `getCheckpointsDataForEpoch` method with a unified query-shaped `getCheckpointsData` that mirrors the block-side API. + +**`getCheckpointsDataForEpoch(epoch)` removed.** The previously-deprecated method is gone. Use `getCheckpointsData({ epoch })` instead. The new `getCheckpointsData` also accepts a contiguous range: + +```diff +- const cps = await node.getCheckpointsDataForEpoch(epoch); ++ const cps = await node.getCheckpointsData({ epoch }); + + // New: contiguous range ++ const cps = await node.getCheckpointsData({ from: 1, limit: 5 }); +``` + +**`'latest'` removed from `CheckpointParameter`.** The `'latest'` literal previously accepted by `getCheckpoint('latest', options)` is no longer valid. Use `'checkpointed'` to address the latest confirmed checkpoint, or `'proposed'` for the proposed-tip semantics described below. (Block-side `'latest'` in `BlockParameter` is unaffected.) + +```diff +- await node.getCheckpoint('latest'); ++ await node.getCheckpoint('checkpointed'); +``` + +**`'proposed'` semantics changed.** Previously `'proposed'` on the checkpoint side aliased to "latest L1-confirmed checkpoint" — a documented foot-gun. After this release: + +- `getCheckpoint('proposed')` resolves to the proposed-tip checkpoint number and looks it up confirmed-first, then falls back to the proposed-checkpoint store. When a proposed entry exists at that number it is returned; when none exists, the proposed-tip falls back to the confirmed tip and the call returns the latest confirmed checkpoint. Returns `undefined` only when neither store has the resolved number. +- `getCheckpointNumber('proposed')` returns the proposed-tip checkpoint number, falling back to the latest confirmed checkpoint number when no proposed entry exists. Return type stays `Promise<CheckpointNumber>`. + +If you want the latest L1-confirmed checkpoint regardless of proposed state, switch the call to `'checkpointed'`: + +```diff +- const cp = await node.getCheckpoint('proposed'); +- const n = await node.getCheckpointNumber('proposed'); ++ const cp = await node.getCheckpoint('checkpointed'); ++ const n = await node.getCheckpointNumber('checkpointed'); +``` + +**By-number / by-slot lookups gain a confirmed→proposed fallback.** `getCheckpoint({ number: N })` and `getCheckpoint({ slot: S })` now check the confirmed store first, then fall back to the proposed store. Tag-based lookups (`'checkpointed'`, `'proven'`, `'finalized'`) do not fall back — those tags name confirmed-only positions. + +**Throws on a proposed match + L1/attestations.** Proposed checkpoints have no L1 publish info or committee attestations (those data points only exist after L1 confirmation). The throw fires only when the lookup actually lands on a proposed entry — i.e. the confirmed store missed and the proposed store hit. When the proposed-tip falls back to the confirmed tip (no proposed entry exists), `'proposed' + includeAttestations` returns the latest confirmed checkpoint with attestations rather than throwing: + +```ts +// Throws BadRequestError when a proposed entry exists at the resolved number: +await node.getCheckpoint("proposed", { includeAttestations: true }); +await node.getCheckpoint("proposed", { includeL1PublishInfo: true }); + +// And when a by-number / by-slot lookup falls back to a proposed entry: +await node.getCheckpoint({ number: N }, { includeAttestations: true }); +// → throws if N is matched only in the proposed store +``` + +If your code asks for `includeAttestations` / `includeL1PublishInfo` and might land on a proposed entry, gate the call on `getCheckpoint(param)` first, then re-issue with the include flags only after confirming the result is from the confirmed store (e.g. by checking that the tag-based equivalent returns the same checkpoint number). + +**Impact**: Wallet, indexer, and tooling code that called `node.getCheckpoint('proposed')` or `node.getCheckpoint('latest')` will need to update their tag. Any code relying on the old "proposed = latest confirmed" alias should switch to `'checkpointed'`. Code that combined `'proposed'` (or by-number/by-slot fallbacks) with `includeAttestations` / `includeL1PublishInfo` will now throw at runtime; gate those flags as described above. + +### [Aztec Node] `feeAssetPriceModifier` now correctly populated on confirmed checkpoints + +Confirmed checkpoints previously reported `feeAssetPriceModifier = 0n` regardless of the value observed on L1, because the archiver dropped the field on checkpoint confirmation. The field is now persisted and returned correctly on `CheckpointResponse`. Any wallet or indexer logic that special-cased `0n` as a sentinel for "no modifier" will need to be updated; it is now a valid value in its own right. + +### [CLI] `aztec-up` no longer exposes transitive npm bins on PATH + +The `aztec-up` installer used to add `$HOME/.aztec/current/node_modules/.bin` to your shell `PATH`, which put ~40 transitive npm bins (`jest`, `tsc`, `tsserver`, `semver`, `uuid`, `json5`, ...) onto your interactive shell and silently shadowed your own installed versions of those tools. Only the seven `@aztec/*`-owned bins (`aztec`, `aztec-wallet`, `bb`, `bb-cli`, `blob-client`, `noir-codegen`, `txe`) are now exposed. + +If you had an Aztec version installed before this release, your shell profile (`~/.bashrc` or `~/.zshrc`) still contains the old `PATH` line. Re-run the installer once (replacing `[VERSION]` with whichever toolchain version you're on, e.g. `4.2.0`) to replace it: + +```bash +VERSION=[VERSION] bash -i <(curl -sL https://install.aztec.network) +``` + +Open a fresh shell and confirm the leak is gone: + +```bash +echo $PATH +``` + +`$HOME/.aztec/current/node_modules/.bin` should no longer appear in the output. You'll also see your own `jest`, `tsc`, etc. again instead of the ones bundled with the Aztec toolchain. + +### [Protocol] Domain separators introduced for merkle-node, block-headers, and blob hashes + +Several protocol hashes that previously used bare `poseidon2_hash` are now domain-separated via `poseidon2_hash_with_separator`. This is a security hardening change — it prevents a value produced by one hash context from being reinterpreted in another (e.g. a sibling path from one tree being transported to another). + +**New domain separators:** + +- `DOM_SEP__MERKLE_HASH` — sibling-pair hash for append-only trees (note-hash, L1→L2, archive, VK tree, and the balanced/unbalanced tree hash helpers in `@aztec/foundation/trees`). +- `DOM_SEP__NULLIFIER_MERKLE`, `DOM_SEP__PUBLIC_DATA_MERKLE`, `DOM_SEP__WRITTEN_SLOTS_MERKLE`, `DOM_SEP__RETRIEVED_BYTECODES_MERKLE` — per-tree sibling-pair hash for each indexed tree. Each tree uses its own separator so sibling paths are non-transportable across trees. +- `DOM_SEP__BLOCK_HEADERS_HASH` — used when accumulating block headers into `blockHeadersHash`. +- `DOM_SEP__BLOB_HASHED_Y_LIMBS`, `DOM_SEP__BLOB_CHALLENGE_Z`, `DOM_SEP__BLOB_Z_ACC`, `DOM_SEP__BLOB_GAMMA_ACC`, `DOM_SEP__BLOB_GAMMA_FINAL` — blob accumulator and challenge derivations. + +**:warning: Hard-coded test constants will no longer match.** Every value derived from any of the hashes above is new in this release. This includes (non-exhaustively): + +- **All merkle tree roots** — note-hash, nullifier, public-data, L1→L2 message, archive, VK, and the AVM-internal written-slots and retrieved-bytecodes (class-ids) trees. +- **Genesis constants** — `GENESIS_BLOCK_HEADER_HASH`, `GENESIS_ARCHIVE_ROOT`, `AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_ROOT`, `AVM_RETRIEVED_BYTECODES_TREE_INITIAL_ROOT`. +- **Every block hash and archive root** — they commit to tree roots and the new block-headers-hash. +- **Protocol contract addresses** — their derivation depends on the private-function tree root. +- **Blob commitments / challenges** — any test that pins `z`, `gamma`, or the accumulator outputs. + +Regenerate these values from a fresh build of this release — do not copy them from previous release fixtures. + +**If you re-implement any of these hashes off-circuit** (e.g. a wallet that computes nullifier low-leaf membership, or an indexer that derives block hashes / tree roots), update the call sites: + +```diff + // Merkle sibling-pair hash (append-only trees) +- poseidon2Hash([left, right]) ++ poseidon2HashWithSeparator([left, right], DomainSeparator.MERKLE_HASH) + + // Indexed-tree sibling-pair hash — pick the matching tree separator +- poseidon2Hash([left, right]) ++ poseidon2HashWithSeparator([left, right], DomainSeparator.NULLIFIER_MERKLE) ++ // or PUBLIC_DATA_MERKLE, WRITTEN_SLOTS_MERKLE, RETRIEVED_BYTECODES_MERKLE +``` + +`poseidon2HashWithSeparator` is exported from `@aztec/foundation/crypto/poseidon`; the `DomainSeparator` enum and the matching `DOM_SEP__*` constants are defined in `@aztec/constants`. The new entries listed above are additions — existing separator names are unchanged. + +For TypeScript consumers, `@aztec/stdlib/hash` exports ready-made helpers that wrap the right separator: `computeMerkleHash` (append-only), `computeNullifierMerkleHash`, and `computePublicDataMerkleHash`. Prefer these over calling `poseidon2HashWithSeparator` directly so the separator choice stays colocated with the tree. + +### [Aztec.nr] `emit_private_log_unsafe` / `emit_raw_note_log_unsafe` are deprecated + +`emit_private_log_unsafe` and `emit_raw_note_log_unsafe` are deprecated and will be removed in a future release. Migrate to the new `emit_private_log_vec_unsafe` / `emit_raw_note_log_vec_unsafe` functions, which take a `BoundedVec<Field, PRIVATE_LOG_CIPHERTEXT_LEN>` instead of the `(log: [Field; PRIVATE_LOG_CIPHERTEXT_LEN], length: u32)` pair. + +```diff +- context.emit_private_log_unsafe(tag, log, length); ++ context.emit_private_log_vec_unsafe(tag, bounded_vec_log); +- context.emit_raw_note_log_unsafe(tag, log, length, note_hash_counter); ++ context.emit_raw_note_log_vec_unsafe(tag, bounded_vec_log, note_hash_counter); +``` + +If you were manually padding an array and passing a shorter length, you can now create a `BoundedVec` from just the meaningful fields: + +```diff +- let padded = payload.concat([0; PRIVATE_LOG_CIPHERTEXT_LEN - 2]); +- context.emit_private_log_unsafe(tag, padded, 2); ++ let log = BoundedVec::from_array(payload); ++ context.emit_private_log_vec_unsafe(tag, log); +``` + +If you were passing the full array, wrap it with `BoundedVec::from_array`: + +```diff +- context.emit_private_log_unsafe(tag, ciphertext, ciphertext.len()); ++ context.emit_private_log_vec_unsafe(tag, BoundedVec::from_array(ciphertext)); +``` + +### [aztec-nr] Nullifier membership witness oracle returns split types + +`get_nullifier_membership_witness` and `get_low_nullifier_membership_witness` now return `(NullifierLeafPreimage, MembershipWitness<NULLIFIER_TREE_HEIGHT>)` instead of the bundled `NullifierMembershipWitness` struct (which has been removed). + +If you were using these oracle functions directly (e.g. in `schnorr_account_contract`'s `lookup_validity`), update your code to destructure the tuple: + +```diff +- let witness = get_low_nullifier_membership_witness(block_header, siloed_nullifier); +- let nullifier_value = witness.leaf_preimage.nullifier; +- let index = witness.index; +- let path = witness.path; ++ let (leaf_preimage, witness) = get_low_nullifier_membership_witness(block_header, siloed_nullifier); ++ let nullifier_value = leaf_preimage.nullifier; ++ let index = witness.leaf_index; ++ let path = witness.sibling_path; +``` + +Note the field renames: `index` is now `leaf_index`, and `path` is now `sibling_path` (matching the protocol circuit's `MembershipWitness` type). + +This has been done because this is the format expected by the functionality in protocol circuits and given that this is sensitive security-wise it made sense to reuse that functionality in Aztec.nr. + +### [L1 Contracts] Empire slasher removed, slasher config simplified + +The empire slashing model has been removed. Only the tally-based slashing model remains, and it has been renamed from `TallySlashingProposer` to `SlashingProposer`. + +**L1 contract changes:** + +- `SlasherFlavor` enum removed from `ISlasher.sol` +- `RollupConfigInput.slasherFlavor` (enum) replaced with `slasherEnabled` (bool) +- `TallySlashingProposer` contract renamed to `SlashingProposer` +- `TallySlasherDeploymentExtLib` library renamed to `SlasherDeploymentExtLib` +- `SlashFactory` periphery contract removed +- `SLASHING_PROPOSER_TYPE` constant removed from `SlashingProposer` +- All `TallySlashingProposer__` error prefixes renamed to `SlashingProposer__` + +**Environment variable changes:** + +```diff +- AZTEC_SLASHER_FLAVOR=tally # was: "tally" | "empire" | "none" ++ AZTEC_SLASHER_ENABLED=true # now a boolean +``` + +**Removed environment variables:** `SLASH_MIN_PENALTY_PERCENTAGE`, `SLASH_MAX_PENALTY_PERCENTAGE` + +**Removed from deploy outputs:** `slashFactoryAddress` + +**Node admin API:** `getSlashPayloads()` method removed. + +**TypeScript config changes:** + +```diff +- slasherFlavor: 'tally' | 'none' ++ slasherEnabled: boolean +``` + +`slashMinPenaltyPercentage` and `slashMaxPenaltyPercentage` removed from `SlasherConfig`. + +### [Aztec Node] `getTxByHash`, `getTxsByHash` and `getPendingTxs` no longer return tx proofs by default + +`AztecNode.getTxByHash`, `AztecNode.getTxsByHash` and `AztecNode.getPendingTxs` (also exposed on the P2P API) now take an optional `GetTxByHashOptions` argument with an `includeProof` flag. The proof is stripped from returned txs unless `includeProof: true` is passed, cutting roughly 35-52KB per tx over the wire. + +**Migration:** + +```diff +- const tx = await node.getTxByHash(txHash); ++ const tx = await node.getTxByHash(txHash, { includeProof: true }); + +- const txs = await node.getPendingTxs(limit, after); ++ const txs = await node.getPendingTxs(limit, after, { includeProof: true }); +``` + +**Impact**: Callers that read the proof off returned txs (eg to re-broadcast or validate them) must now pass `{ includeProof: true }` explicitly; by default the returned txs carry an empty proof. + +### [aztec.js] `DeployMethod.send()` always returns `{ contract, receipt, instance }` + +The `returnReceipt` option in deploy wait options has been removed. `DeployMethod.send()` now always returns an object with `contract`, `receipt`, and `instance` at the top level, provided the user waits for the transaction to be included. + +The `DeployTxReceipt` and `DeployWaitOptions` types have been removed. + +**Migration:** + +```diff +- const { +- receipt: { contract, instance }, +- } = await MyContract.deploy(wallet, ...args).send({ +- from: address, +- wait: { returnReceipt: true }, +- }); + ++ const { contract, instance } = await MyContract.deploy(wallet, ...args).send({ ++ from: address, ++ }); +``` + +### [CLI] `aztec init` now scaffolds a Counter example template + +`aztec init` previously created a blank contract crate. It now scaffolds a runnable **Counter** example contract with a constructor, `increment`, and `get_counter` functions, plus a test suite, so new developers have a working starting point ([#22751](https://github.com/AztecProtocol/aztec-packages/pull/22751)). + +- `aztec init` — scaffolds the Counter example (new default). +- `aztec new <NAME>` — still scaffolds a blank contract, either as a new standalone project or as a new crate added to an existing workspace. + +**Impact**: any scripts, CI jobs, or onboarding docs that ran `aztec init` expecting an empty contract starting point now get the Counter example. Use `aztec new <NAME>` for the blank scaffold. The existing Counter tutorial under [`docs/tutorials/contract_tutorials`](../tutorials/contract_tutorials/counter_contract.md) is unaffected because it uses `aztec new`. + +## 4.3.0 + +### `aztec new` and `aztec init` now create a 2-crate workspace + +`aztec new` and `aztec init` now create a workspace with two crates instead of a single contract crate: + +- A `contract` crate (type = "contract") for your smart contract code +- A `test` crate (type = "lib") for Noir tests, which depends on the contract crate + +The new project structure looks like: + +``` +my_project/ +├── Nargo.toml # [workspace] members = ["contract", "test"] +├── contract/ +│ ├── src/main.nr +│ └── Nargo.toml # type = "contract" +└── test/ + ├── src/lib.nr + └── Nargo.toml # type = "lib" +``` + +**What changed:** + +- The `--contract` and `--lib` flags have been removed from `aztec new` and `aztec init`. These commands now always create a contract workspace. +- Contract code is now at `contract/src/main.nr` instead of `src/main.nr`. +- The `Nargo.toml` in the project root is now a workspace file. Contract dependencies go in `contract/Nargo.toml`. +- Tests should be written in the separate `test` crate (`test/src/lib.nr`) and import the contract by package name (e.g., `use my_contract::MyContract;`) instead of using `crate::`. + +### `aztec new` crate directories are now named after the contract + +`aztec new` and `aztec init` now name the generated crate directories after the contract instead of using generic `contract/` and `test/` names. For example, `aztec new counter` now creates: + +``` +counter/ +├── Nargo.toml # [workspace] members = ["counter_contract", "counter_test"] +├── counter_contract/ +│ ├── src/main.nr +│ └── Nargo.toml # type = "contract" +└── counter_test/ + ├── src/lib.nr + └── Nargo.toml # type = "lib" +``` + +This enables adding multiple contracts to a single workspace. Running `aztec new <name>` inside an existing workspace (a directory with a `Nargo.toml` containing `[workspace]`) now adds a new `<name>_contract` and `<name>_test` crate pair to the workspace instead of creating a new directory. + +**What changed:** + +- Crate directories are now `<name>_contract/` and `<name>_test/` instead of `contract/` and `test/`. +- Contract code is now at `<name>_contract/src/main.nr` instead of `contract/src/main.nr`. +- Contract dependencies go in `<name>_contract/Nargo.toml` instead of `contract/Nargo.toml`. +- Tests import the contract by its new crate name (e.g., `use counter_contract::Main;` instead of `use counter::Main;`). + +### [CLI] `--name` flag removed from `aztec new` and `aztec init` + +The `--name` flag has been removed from both `aztec new` and `aztec init`. For `aztec new`, the positional argument now serves as both the contract name and the directory name. For `aztec init`, the directory name is always used as the contract name. + +**Migration:** + +```diff +- aztec new my_project --name counter ++ aztec new counter +``` + +```diff +- aztec init --name counter ++ aztec init +``` + +**Impact**: If you were using `--name` to set a contract name different from the directory name, rename your directory or use `aztec new` with the desired contract name directly. + +## 4.2.0 + +### [Aztec.js] `GasSettings.default()` renamed to `GasSettings.fallback()` + +`GasSettings.default()` has been renamed to `GasSettings.fallback()` to clarify that these gas limits are not protocol defaults — the protocol has no concept of "default" gas settings. `fallback()` is a convenience for cases where gas estimation is not being used, but callers should prefer estimating gas via simulation for accurate limits. + +The old `DEFAULT_GAS_LIMIT` and `DEFAULT_TEARDOWN_GAS_LIMIT` constants have been removed. Gas limits are now derived from protocol-level maximums (`MAX_PROCESSABLE_L2_GAS`, `MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT`) rather than arbitrary fixed values. + +A new `GasSettings.forEstimation()` method provides intentionally high gas limits for use during simulation. These limits exceed protocol maximums so the simulation doesn't hit gas caps — you must pass `skipTxValidation: true` when simulating with them, then use the results to set accurate gas limits on the actual transaction. `EmbeddedWallet` does this by default. + +**Migration:** + +```diff +- import { DEFAULT_GAS_LIMIT, DEFAULT_TEARDOWN_GAS_LIMIT } from '@aztec/constants'; +- const settings = GasSettings.default({ maxFeesPerGas }); ++ const settings = GasSettings.fallback({ maxFeesPerGas }); +``` + +**Impact**: Any code referencing `GasSettings.default()`, `DEFAULT_GAS_LIMIT`, or `DEFAULT_TEARDOWN_GAS_LIMIT` will fail to compile. + +### [PXE] `simulateTx`, `executeUtility`, `profileTx`, and `proveTx` no longer accept `scopes: 'ALL_SCOPES'` + +The `AccessScopes` type (`'ALL_SCOPES' | AztecAddress[]`) has been removed. The `scopes` field in `SimulateTxOpts`, +`ExecuteUtilityOpts`, and `ProfileTxOpts` now requires an explicit `AztecAddress[]`. Callers that previously passed +`'ALL_SCOPES'` must now specify which addresses will be in scope for the call. + +**Migration:** + +```diff ++ const accounts = await pxe.getRegisteredAccounts(); ++ const scopes = accounts.map(a => a.address); + + // simulateTx +- await pxe.simulateTx(txRequest, { simulatePublic: true, scopes: 'ALL_SCOPES' }); ++ await pxe.simulateTx(txRequest, { simulatePublic: true, scopes }); + + // executeUtility +- await pxe.executeUtility(call, { scopes: 'ALL_SCOPES' }); ++ await pxe.executeUtility(call, { scopes }); + + // profileTx +- await pxe.profileTx(txRequest, { profileMode: 'full', scopes: 'ALL_SCOPES' }); ++ await pxe.profileTx(txRequest, { profileMode: 'full', scopes }); + + // proveTx +- await pxe.proveTx(txRequest, 'ALL_SCOPES'); ++ await pxe.proveTx(txRequest, scopes); +``` + +**Impact**: Any code passing `'ALL_SCOPES'` to `simulateTx`, `executeUtility`, `profileTx`, or `proveTx` will fail to compile. Replace with an explicit array of account addresses. + +### [PXE] Capsule operations are now scope-enforced at the PXE level + +The PXE now enforces that capsule operations can only access scopes that were authorized for the current execution. If a contract attempts to access a capsule scope that is not in its allowed scopes list, the PXE will throw an error: + +``` +Scope 0x1234... is not in the allowed scopes list: [0xabcd...]. +``` + +The zero address (`AztecAddress::zero()`) is always allowed regardless of the scopes list, preserving backwards compatibility for contracts using the global scope. + +**Impact**: Contracts that access capsules scoped to addresses not included in the transaction's authorized scopes will now fail at runtime. Ensure the correct scopes are passed when executing transactions. + +### [aztec.js] `EmbeddedWalletOptions` now uses a unified `pxe` field + +The `pxeConfig` and `pxeOptions` fields on `EmbeddedWalletOptions` have been deprecated in favor of a single `pxe` field that accepts both PXE configuration and dependency overrides (custom prover, store, simulator): + +```diff +const wallet = await EmbeddedWallet.create(nodeUrl, { +- pxeConfig: { proverEnabled: true }, +- pxeOptions: { proverOrOptions: myCustomProver }, ++ pxe: { ++ proverEnabled: true, ++ proverOrOptions: myCustomProver, ++ }, +}); +``` + +The old fields still work but will be removed in a future release. + +### [Aztec.nr] Ephemeral arrays replace capsule arrays in PXE oracle interfaces + +Oracle interfaces between Aztec.nr and PXE now use a new `EphemeralArray` type (`aztec::ephemeral::EphemeralArray`) instead of `CapsuleArray`. Ephemeral arrays live in memory and are scoped by contract call frame, so they no longer need to be addressed by `(contract_address, scope)`. Several public message-discovery and validation functions lost their `recipient`, `scope`, and `contract_address` parameters as a result. + +Most contracts are not affected, as the macro-generated `sync_state` and `process_message` functions handle these APIs automatically. Only contracts that call these functions directly need to update. + +**Migration:** + +```diff + attempt_note_discovery( + contract_address, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, +- recipient, + compute_note_hash, + compute_note_nullifier, + owner, + storage_slot, + randomness, + note_type_id, + packed_note, + ); + +- enqueue_note_for_validation(contract_address, owner, storage_slot, randomness, note_nonce, packed_note, note_hash, nullifier, tx_hash, scope); ++ enqueue_note_for_validation(contract_address, owner, storage_slot, randomness, note_nonce, packed_note, note_hash, nullifier, tx_hash); + +- enqueue_event_for_validation(contract_address, event_type_id, randomness, serialized_event, event_commitment, tx_hash, scope); ++ enqueue_event_for_validation(contract_address, event_type_id, randomness, serialized_event, event_commitment, tx_hash); + +- validate_and_store_enqueued_notes_and_events(contract_address, scope); ++ validate_and_store_enqueued_notes_and_events(scope); +``` + +The `sync_inbox` function and the `OffchainInboxSync` type now return `EphemeralArray<OffchainMessageWithContext>` instead of `CapsuleArray<OffchainMessageWithContext>`. Custom message handlers that bind the returned array to an explicit type must update the type annotation. + +**Impact**: Contracts that call the above functions directly (rather than relying on macro-generated code) will fail to compile until the trailing `recipient`, `scope`, and `contract_address` parameters are removed. + +## 4.2.0-aztecnr-rc.2 + +### [Aztec.js] Removed `SingleKeyAccountContract` + +The `SchnorrSingleKeyAccount` contract and its TypeScript wrapper `SingleKeyAccountContract` have been removed. This contract was insecure: it used `ivpk_m` (incoming viewing public key) as its Schnorr signing key, meaning anyone who received a user's viewing key could sign transactions on their behalf. + +**Migration:** + +```diff +- import { SingleKeyAccountContract } from '@aztec/accounts/single_key'; +- const contract = new SingleKeyAccountContract(signingKey); ++ import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; ++ const contract = new SchnorrAccountContract(signingKey); +``` + +**Impact**: If you were using `@aztec/accounts/single_key`, switch to `@aztec/accounts/schnorr` which uses separate keys for encryption and authentication. + +### Custom token FPCs removed from default public setup allowlist + +Token contract functions (like `transfer_in_public` and `_increase_public_balance`) have been removed from the default public setup allowlist. FPCs that accept custom tokens (like the reference `FPC` contract) will not work on public networks, because their setup-phase calls to these functions will be rejected. Token class IDs change with each aztec-nr release, making it impractical to maintain them in the allowlist. + +FPCs that use only Fee Juice still work on all networks, since FeeJuice is a protocol contract with a fixed address in the allowlist. Custom FPCs should only call protocol contract functions (AuthRegistry, FeeJuice) during setup. + +`PublicFeePaymentMethod` and `PrivateFeePaymentMethod` in aztec.js are affected, since they use the reference `FPC` contract which calls Token functions during setup. Switch to `FeeJuicePaymentMethodWithClaim` (after [bridging Fee Juice from L1](../aztec-js/how_to_pay_fees.md#bridge-fee-juice-from-l1)) or write an FPC that uses Fee Juice natively. + +**Migration:** + +```diff +- import { PublicFeePaymentMethod } from '@aztec/aztec.js/fee'; +- const paymentMethod = new PublicFeePaymentMethod(fpcAddress, senderAddress, wallet, gasSettings); ++ import { FeeJuicePaymentMethodWithClaim } from '@aztec/aztec.js/fee'; ++ const paymentMethod = new FeeJuicePaymentMethodWithClaim(senderAddress, claim); +``` + +Similarly, the `fpc-public` and `fpc-private` CLI wallet payment methods use the reference Token-based FPC and will not work on public networks. Use `fee_juice` for direct Fee Juice payment, or `fpc-sponsored` on devnet and local network. + +### [Aztec.nr] Domain-separated tags on log emission + +All logs emitted through the Aztec.nr framework now include a domain-separated tag at `fields[0]`. Each log category uses its own domain separator via `compute_log_tag(raw_tag, dom_sep)`: + +- **Events** (`DOM_SEP__EVENT_LOG_TAG`): the event type ID is the raw tag. +- **Message delivery** (`DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG`): the discovery tag is the raw tag. +- **Partial note completion logs** (`DOM_SEP__NOTE_COMPLETION_LOG_TAG`): the partial note's `commitment` field is the raw tag. + +The low-level emit methods now take `tag` as an explicit first parameter and have been renamed with an `_unsafe` suffix. Previously the tag was included as `log[0]` — it has now been extracted into its own parameter, and `log` no longer contains it: + +```diff +- context.emit_private_log(log, length); ++ context.emit_private_log_unsafe(tag, log, length); +- context.emit_raw_note_log(log, length, note_hash_counter); ++ context.emit_raw_note_log_unsafe(tag, log, length, note_hash_counter); +- context.emit_public_log(log); ++ context.emit_public_log_unsafe(tag, log); +``` + +Prefer the higher-level APIs (`emit` for events, `MessageDelivery` for messages) which handle tagging automatically. + +### [Aztec.nr] Public events no longer include the event type selector at the end of the payload + +`emit_event_in_public` previously appended the event type selector as the last field. It now prepends a domain-separated tag at `fields[0]` instead. The payload after the tag contains only the serialized event fields. + +If you were reading public event directly from node logs (i.e. via `node.getPublicLogs` and not via `wallet.getPublicEvents`), update your parsing: + +```diff +- // Old: fields = [serialized_event..., event_type_selector] +- const selector = EventSelector.fromField(fields[fields.length - 1]); +- const event = decodeFromAbi([abiType], fields); ++ // New: fields = [domain_separated_tag, serialized_event...] ++ const eventFields = log.getEmittedFieldsWithoutTag(); ++ const event = decodeFromAbi([abiType], eventFields); +``` + +### [Aztec.nr] Capsule operations are now addressed by scope + +All capsule operations (`store`, `load`, `delete`, `copy`) and `CapsuleArray` now require a `scope: AztecAddress` parameter. This scopes capsule storage by address, providing isolation between different accounts within the same PXE. + +Contracts that use `CapsuleArray` directly also need to update. + +**Migration:** + +```diff +- let array: CapsuleArray<Field> = CapsuleArray::at(contract_address, slot); ++ let array: CapsuleArray<Field> = CapsuleArray::at(contract_address, slot, scope); +``` + +The low-level capsule functions are similarly affected: + +```diff +- capsules::store(contract_address, slot, value); ++ capsules::store(contract_address, slot, value, scope); + +- capsules::load(contract_address, slot); ++ capsules::load(contract_address, slot, scope); + +- capsules::delete(contract_address, slot); ++ capsules::delete(contract_address, slot, scope); + +- capsules::copy(contract_address, src_slot, dst_slot, num_entries); ++ capsules::copy(contract_address, src_slot, dst_slot, num_entries, scope); +``` + +If you need to stick the old, scope-less behavior, and you are really sure that that's what you need to use, you can use `scope = AztecAddress::zero()`. + +### [Aztec.nr] `process_message` utility function removed + +The auto-generated `process_message` utility function has been removed. If you need to deliver offchain messages (messages not broadcast via onchain logs), use the `offchain_receive` utility function instead. This function is automatically injected by the `#[aztec]` macro and accepts messages into a persistent inbox scoped by recipient. These messages are then picked up and processed during `sync_state`. + +**Impact**: Contracts that explicitly called `process_message` must switch to delivering messages via `offchain_receive` and letting `sync_state` handle processing. + +### [Aztec.nr] `CustomMessageHandler` type signature changed + +The `CustomMessageHandler` function type now receives an additional `scope: AztecAddress` parameter: + +```diff + type CustomMessageHandler = unconstrained fn( + AztecAddress, // contract_address + u64, // msg_type_id + u64, // msg_metadata + BoundedVec<Field, MAX_MESSAGE_CONTENT_LEN>, // msg_content + MessageContext, // message_context ++ AztecAddress, // scope + ); +``` + +**Impact**: Contracts that implement a custom message handler must update the function signature. + +### [aztec.js] `isContractInitialized` is now `initializationStatus` tri-state enum + +`ContractMetadata.isContractInitialized` has been renamed to `ContractMetadata.initializationStatus` and changed from `boolean | undefined` to a `ContractInitializationStatus` enum with values `INITIALIZED`, `UNINITIALIZED`, and `UNKNOWN`. + +- `INITIALIZED`: the contract has been initialized (initialization nullifier found) +- `UNINITIALIZED`: the contract instance is registered but has not been initialized +- `UNKNOWN`: the instance is not registered and no public initialization nullifier was found + +When the instance is not registered, the wallet now attempts to check the public initialization nullifier (computed from address alone) before returning `UNKNOWN`. Previously this case returned `undefined`. + +**Migration:** + +```diff ++ import { ContractInitializationStatus } from '@aztec/aztec.js/wallet'; + + const metadata = await wallet.getContractMetadata(address); +- if (metadata.isContractInitialized) { ++ if (metadata.initializationStatus === ContractInitializationStatus.INITIALIZED) { + // contract is initialized + } +``` + +### [Aztec.js] Use `NO_FROM` instead of `AztecAddress.ZERO` to bypass account contract entrypoint + +When sending transactions that should not be mediated by an account contract (e.g., account contract self-deployments), use the explicit `NO_FROM` sentinel instead of `AztecAddress.ZERO`. + +`NO_FROM` signals that the transaction should be executed directly via the `DefaultEntrypoint`. This replaces the brittle convention of passing `AztecAddress.ZERO` as the `from` field. + +**Migration:** + +```diff +- import { AztecAddress } from '@aztec/aztec.js'; ++ import { NO_FROM } from '@aztec/aztec.js/account'; + + await contract.methods.my_method().send({ +- from: AztecAddress.ZERO, ++ from: NO_FROM, + }); +``` + +Note that `DefaultEntrypoint` only accepts a single call. If you need to execute multiple calls without account contract mediation (e.g., deploying an account contract and paying a fee in the same transaction), wrap them through `DefaultMultiCallEntrypoint` on the app side before sending: + +```typescript +import { NO_FROM } from "@aztec/aztec.js/account"; +import { DefaultMultiCallEntrypoint } from "@aztec/entrypoints/multicall"; +import { mergeExecutionPayloads } from "@aztec/stdlib/tx"; + +// Merge multiple execution payloads into one +const merged = mergeExecutionPayloads([deployPayload, feePayload]); + +// Wrap through multicall so it becomes a single call for DefaultEntrypoint +const multicall = new DefaultMultiCallEntrypoint(); +const chainInfo = await wallet.getChainInfo(); +const wrappedPayload = await multicall.wrapExecutionPayload(merged, chainInfo); + +// Send without account contract mediation +await wallet.sendTx(wrappedPayload, { from: NO_FROM }); +``` + +Using other contracts for wrapping (for example, supporting more calls) is also supported, as long as the contract is registered in the wallet. This opens the door to different flows that do not use account entrypoints as the first call in the chain, including app sponsored FPCs. + +**Impact**: Any code that passes `AztecAddress.ZERO` as the `from` option in `.send()`, `.simulate()`, or deploy options must switch to `NO_FROM`. Wallets use `DefaultEntrypoint` directly for `NO_FROM` transactions, instead of the `DefaultMultiCallEntrypoint` that was used internally before when specifying `AztecAddress.ZERO`. + +### [Aztec.js] `ExecuteUtilityOptions.scope` renamed to `scopes` and type changed to `AztecAddress[]` + +The `scope` field in `ExecuteUtilityOptions` has been renamed to `scopes` and changed from a single `AztecAddress` to `AztecAddress[]`. This aligns the wallet's `executeUtility` API with the PXE API and `sendTx` in `Wallet`, which both accept an array of scopes. + +**Migration:** + +```diff + wallet.executeUtility(call, { +- scope: myAddress, ++ scopes: [myAddress], + }); +``` + +**Impact**: Any code that calls `wallet.executeUtility` directly must update the options object. Wallets must update to adapt to the new interface + +### [Aztec.nr] `attempt_note_discovery` now takes two separate functions instead of one + +The `attempt_note_discovery` function (and related discovery functions like `do_sync_state`, `process_message_ciphertext`) now takes separate `compute_note_hash` and `compute_note_nullifier` arguments instead of a single combined `compute_note_hash_and_nullifier`. The corresponding type aliases are now `ComputeNoteHash` and `ComputeNoteNullifier` (instead of `ComputeNoteHashAndNullifier`). + +This split improves performance during nonce discovery: the note hash only needs to be computed once, while the old combined function recomputed it for every candidate nonce. + +Most contracts are not affected, as the macro-generated `sync_state` and `process_message` functions handle this automatically. Only contracts that call `attempt_note_discovery` directly need to update. + +**Migration:** + +```diff + attempt_note_discovery( + contract_address, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, +- _compute_note_hash_and_nullifier, ++ _compute_note_hash, ++ _compute_note_nullifier, + owner, + storage_slot, + randomness, + note_type_id, + packed_note, + ); +``` + +**Impact**: Contracts that call `attempt_note_discovery` or related discovery functions directly with a custom `_compute_note_hash_and_nullifier` argument. The old combined function is still generated (deprecated) but is no longer used by the framework. Additionally, if you had a custom `_compute_note_hash_and_nullifier` function then compilation will now fail as you'll need to also produce the corresponding `_compute_note_hash` and `_compute_note_nullifier` functions. + +### Private initialization nullifier now includes `init_hash` + +The private initialization nullifier is no longer derived from just the contract address. It is now computed as a Poseidon2 hash of `[address, init_hash]` using a dedicated domain separator. This prevents observers from determining whether a fully private contract has been initialized by simply knowing its address. + +Note that `Wallet.getContractMetadata` now returns `initializationStatus: ContractInitializationStatus.UNKNOWN` when the wallet does not have the contract instance registered, since `init_hash` is needed to compute the nullifier and initialization status cannot be determined. Previously, this check worked for any address. Callers should check the enum value before branching on the initialization state. + +If you use `assert_contract_was_initialized_by` or `assert_contract_was_not_initialized_by` from `aztec::history::deployment`, these now require an additional `init_hash: Field` parameter: + +```diff ++ let instance = get_contract_instance(contract_address); + assert_contract_was_initialized_by( + block_header, + contract_address, ++ instance.initialization_hash, + ); +``` + +### Two separate init nullifiers for private and public + +Contract initialization now emits two separate nullifiers instead of one: a **private init nullifier** and a **public init nullifier**. Each nullifier gates its respective execution domain: + +- Private external functions check the private init nullifier. +- Public external functions check the public init nullifier. + +**How initializers work:** + +- **Private initializers** emit the private init nullifier. If the contract has any external public functions, the protocol auto-enqueues a public call to emit the public init nullifier. +- **Public initializers** emit both nullifiers directly. +- Contracts with no public functions only emit the private init nullifier. + +**`only_self` functions no longer have init checks.** They behave as if marked `noinitcheck`. + +**External functions called during private initialization must be `#[only_self]`.** Init nullifiers are emitted at the end of the initializer, so any external functions called on the initializing contract (e.g. via `enqueue_self` or `call_self`) during initialization will fail the init check unless they skip it. + +**Breaking change for deployment:** If your contract has external public functions and a private initializer, the class must be registered onchain before initialization. You can no longer pass `skipClassPublication: true`, because the auto-enqueued public call requires the class to be available. + +```diff + const deployed = await MyContract.deploy(wallet, ...args).send({ +- skipClassPublication: true, + }).deployed(); +``` + +### [Aztec.nr] Made `compute_note_hash_for_nullification` unconstrained + +This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous (constrained computation of nullifiers can be performed only on the `ConfirmedNote` type). If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe. + +### [Aztec.nr] Changes to standard note hash computation + +Note hashes used to be computed with the storage slot being the last value of the preimage, it is now the first. This is to make it easier to ensure all note hashes have proper domain separation. + +This change requires no input from your side unless you were testing or relying on hardcoded note hashes. + +## 4.1.3 + +### [Aztec.js] `TxReceipt` now includes `epochNumber` + +`TxReceipt` now includes an `epochNumber` field that indicates which epoch the transaction was included in. + +### [Aztec.js] Use `getL2ToL1MembershipWitness` for L2-to-L1 message witnesses + +The node now computes L2-to-L1 membership witnesses directly, resolving the epoch and Outbox root internally from the transaction hash. Use `getL2ToL1MembershipWitness` instead of fetching raw epoch messages and computing the witness client-side. + +**Migration:** + +```diff +- const messages = await aztecNode.getL2ToL1Messages(epochNumber); +- // compute the witness client-side from the epoch messages ++ const witness = await aztecNode.getL2ToL1MembershipWitness(txHash, messageHash); ++ const epoch = witness.epochNumber; +``` + +The return type `L2ToL1MembershipWitness` includes `epochNumber`. An optional `messageIndexInTx` parameter can be passed as the third argument to disambiguate when a transaction emits multiple identical L2-to-L1 messages. + +**Impact**: Call sites that compute L2-to-L1 membership witnesses should use the node method and extract `epochNumber` from the result. + +### [Aztec.js] `getPublicEvents` now returns an object instead of an array + +`getPublicEvents` now returns a `GetPublicEventsResult<T>` object with `events` and `maxLogsHit` fields instead of a plain array. This enables pagination through large result sets using the new `afterLog` filter option. + +```diff +- const events = await getPublicEvents<MyEvent>(node, MyContract.events.MyEvent, filter); ++ const { events } = await getPublicEvents<MyEvent>(node, MyContract.events.MyEvent, filter); +``` + +The `maxLogsHit` flag indicates whether the log limit was reached, meaning more results may be available. You can use `afterLog` in the filter to fetch the next page. + +### [Aztec.nr] Removed `get_random_bytes` + +The `get_random_bytes` unconstrained function has been removed from `aztec::utils::random`. If you were using it, you can replace it with direct calls to the `random` oracle from `aztec::oracle::random` and convert to bytes yourself. + +## 4.1.0-rc.2 + +### [Aztec.js] `simulate()`, `send()`, and deploy return types changed to always return objects + +All SDK interaction methods now return structured objects that include offchain output alongside the primary result. This affects `.simulate()`, `.send()`, deploy `.send()`, and `Wallet.sendTx()`. + +**Impact**: Every call site that uses `.simulate()`, `.send()`, or deploy must destructure the result. This is a mechanical transformation. Custom wallet implementations must update `sendTx()` to return the new object shapes, using `extractOffchainOutput` to decode offchain messages from raw effects. + +The offchain output includes two fields: + +- `offchainEffects` — raw offchain effects emitted during execution, other than `offchainMessages` +- `offchainMessages` — decoded messages intended for specific recipients + +We are making this change now so in the future we can add more fields to the responses of this APIs without breaking backwards compatibility, +so this won't ever happen again. + +**`simulate()` — always returns `{ result, offchainEffects, offchainMessages }` object:** + +```diff +- const value = await contract.methods.foo(args).simulate({ from: sender }); ++ const { result: value } = await contract.methods.foo(args).simulate({ from: sender }); +``` + +When using `includeMetadata` or `fee.estimateGas`, `stats` and `estimatedGas` are also available as optional fields on the same object: + +```diff +- const { stats, estimatedGas } = await contract.methods.foo(args).simulate({ ++ const sim = await contract.methods.foo(args).simulate({ + from: sender, + includeMetadata: true, + }); ++ const stats = sim.stats!; ++ const estimatedGas = sim.estimatedGas!; +``` + +`SimulationReturn` is no longer a generic conditional type — it's a single flat type with optional `stats` and `estimatedGas` fields. + +**`send()` — returns `{ receipt, offchainEffects, offchainMessages }` object:** + +```diff +- const receipt = await contract.methods.foo(args).send({ from: sender }); ++ const { receipt } = await contract.methods.foo(args).send({ from: sender }); +``` + +When using `NO_WAIT`, returns `{ txHash, offchainEffects, offchainMessages }` instead of a bare `TxHash`: + +```diff +- const txHash = await contract.methods.foo(args).send({ from: sender, wait: NO_WAIT }); ++ const { txHash } = await contract.methods.foo(args).send({ from: sender, wait: NO_WAIT }); +``` + +Offchain messages emitted by the transaction are available on the result: + +```typescript +const { receipt, offchainMessages } = await contract.methods + .foo(args) + .send({ from: sender }); +for (const msg of offchainMessages) { + console.log( + `Message for ${msg.recipient} from contract ${msg.contractAddress}:`, + msg.payload, + ); +} +``` + +**Deploy — returns `{ contract, receipt, offchainEffects, offchainMessages }` object:** + +```diff +- const myContract = await MyContract.deploy(wallet, ...args).send({ from: sender }); ++ const { contract: myContract } = await MyContract.deploy(wallet, ...args).send({ from: sender }); +``` + +The deploy receipt is also available via `receipt` if needed (e.g. for `receipt.txHash` or `receipt.transactionFee`). + +**Custom wallet implementations — `sendTx()` must return objects:** + +If you implement the `Wallet` interface (or extend `BaseWallet`), the `sendTx()` method must now return objects that include offchain output. Use `extractOffchainOutput` to split raw effects into decoded messages and remaining effects: + +```diff ++ import { extractOffchainOutput } from '@aztec/aztec.js/contracts'; + + async sendTx(executionPayload, opts) { + const provenTx = await this.pxe.proveTx(...); ++ const offchainOutput = extractOffchainOutput(provenTx.getOffchainEffects()); + const tx = await provenTx.toTx(); + const txHash = tx.getTxHash(); + await this.aztecNode.sendTx(tx); + + if (opts.wait === NO_WAIT) { +- return txHash; ++ return { txHash, ...offchainOutput }; + } + const receipt = await waitForTx(this.aztecNode, txHash, opts.wait); +- return receipt; ++ return { receipt, ...offchainOutput }; + } +``` + +### Scope enforcement for private state access (TXE and PXE) + +Scope enforcement is now active across both TXE (test environment) and PXE (client). Previously, private execution could implicitly access any account's keys and notes. Now, only the caller (`from`) address is in scope by default, and accessing another address's private state requires explicitly granting scope. + +#### Noir developers (TXE) + +TXE now enforces scope isolation, matching PXE behavior. During private execution, only the caller's keys and notes are accessible. If a Noir test accesses private state of an address other than `from`, it will fail. When `from` is the zero address, scopes are empty (deny-all). + +If your TXE tests fail with key or note access errors, ensure the test is calling from the correct address, or restructure the test to match the expected access pattern. + +#### Aztec.js developers (PXE/Wallet) + +The wallet now passes scopes to PXE, and only the `from` address is in scope by default. Auto-expansion of scopes for nested calls to registered accounts has been removed. A new `additionalScopes` option is available on `send()`, `simulate()`, and `deploy()` for cases where private execution needs access to another address's keys or notes. + +**When do you need `additionalScopes`?** + +1. **Deploying contracts whose constructor initializes private storage** (e.g., account contracts, or any contract using `SinglePrivateImmutable`/`SinglePrivateMutable` in the constructor). The contract's own address must be in scope so its nullifier key is accessible during initialization. + +2. **Operations that access another contract's private state** (e.g., withdrawing from an escrow contract that nullifies the contract's own token notes). + +```` + +**Example: deploying a contract with private storage (e.g., `PrivateToken`)** + +```diff + const tokenDeployment = PrivateTokenContract.deployWithPublicKeys( + tokenPublicKeys, wallet, initialBalance, sender, + ); + const tokenInstance = await tokenDeployment.getInstance(); + await wallet.registerContract(tokenInstance, PrivateTokenContract.artifact, tokenSecretKey); + const token = await tokenDeployment.send({ + from: sender, ++ additionalScopes: [tokenInstance.address], + }); +```` + +**Example: withdrawing from an escrow contract** + +```diff + await escrowContract.methods + .withdraw(token.address, amount, recipient) +- .send({ from: owner }); ++ .send({ from: owner, additionalScopes: [escrowContract.address] }); +``` + +### `simulateUtility` renamed to `executeUtility` + +The `simulateUtility` method and related types have been renamed to `executeUtility` across the entire stack to better reflect that utility functions are executed, not simulated. + +**TypeScript:** + +```diff +- import { SimulateUtilityOptions, UtilitySimulationResult } from '@aztec/aztec.js'; ++ import { ExecuteUtilityOptions, UtilityExecutionResult } from '@aztec/aztec.js'; + +- const result: UtilitySimulationResult = await wallet.simulateUtility(functionCall, opts); ++ const result: UtilityExecutionResult = await wallet.executeUtility(functionCall, opts); +``` + +**Noir (test environment):** + +```diff +- let result = env.simulate_utility(my_contract_address, selector); ++ let result = env.execute_utility(my_contract_address, selector); +``` + +## 4.0.0-devnet.2-patch.0 + +### [Protocol] `include_by_timestamp` renamed to `expiration_timestamp` + +The `include_by_timestamp` field has been renamed to `expiration_timestamp` across the protocol to better convey its meaning. +**Noir:** + +```diff +- context.set_tx_include_by_timestamp(123456789); ++ context.set_expiration_timestamp(123456789); +``` + +### [CLI] Dockerless CLI Installation + +The Aztec CLI is now installed without Docker. The installation command has changed: + +**Old installation (deprecated):** + +```bash +bash -i <(curl -sL https://install.aztec.network) +aztec-up <version> +``` + +**New installation:** + +```bash +VERSION=<version> bash -i <(curl -sL https://install.aztec.network/<version>) +``` + +For example, to install version `#include_version_without_prefix`: + +```bash +VERSION=#include_version_without_prefix bash -i <(curl -sL https://install.aztec.network/#include_version_without_prefix) +``` + +**Key changes:** + +- Docker is no longer required to run the Aztec CLI tools +- The `VERSION` environment variable must be set in the installation command +- The version must also be included in the URL path + +**aztec-up is now a version manager:** + +After installation, `aztec-up` functions as a version manager with the following commands: + +| Command | Description | +| ---------------------------- | ------------------------------------------- | +| `aztec-up install <version>` | Install a specific version and switch to it | +| `aztec-up use <version>` | Switch to an already installed version | +| `aztec-up list` | List all installed versions | +| `aztec-up self-update` | Update aztec-up itself | + +### `@aztec/test-wallet` replaced by `@aztec/wallets` + +The `@aztec/test-wallet` package has been removed. Use `@aztec/wallets` instead, which provides `EmbeddedWallet` with a `static create()` factory: + +```diff +- import { TestWallet, registerInitialLocalNetworkAccountsInWallet } from '@aztec/test-wallet/server'; ++ import { EmbeddedWallet } from '@aztec/wallets/embedded'; ++ import { registerInitialLocalNetworkAccountsInWallet } from '@aztec/wallets/testing'; + +- const wallet = await TestWallet.create(node); ++ const wallet = await EmbeddedWallet.create(node); +``` + +For browser environments, the same import resolves to a browser-specific implementation automatically via conditional exports:X + +The `EmbeddedWallet.create()` factory accepts an optional second argument for logger injection and ephemeral storage: + +```typescript +const wallet = await EmbeddedWallet.create(node, { + logger: myLogger, // custom logger; child loggers derived via createChild() + ephemeral: true, // use in-memory stores (no persistence) +}); +``` + +### [Aztec.nr] `debug_log` module renamed to `logging` + +The `debug_log` module has been renamed to `logging` to avoid naming collisions with per-level logging functions that were introduced in this PR (`warn_log`, `info_log`, `debug_log`... and the "format" versions `warn_log_format`, `debug_log_format`). Update all import paths accordingly: + +```diff +- use aztec::oracle::debug_log::debug_log; +- use aztec::oracle::debug_log::debug_log_format; ++ use aztec::oracle::logging::debug_log; ++ use aztec::oracle::logging::debug_log_format; +``` + +For inline paths: + +```diff +- aztec::oracle::debug_log::debug_log_format("msg: {}", [value]); ++ aztec::oracle::logging::debug_log_format("msg: {}", [value]); +``` + +The function names themselves (`debug_log`, `debug_log_format`, `debug_log_with_level`, `debug_log_format_with_level`) are unchanged. + +Additionally, `debug_log_format_slice` has been removed. Use `debug_log_format` instead, which accepts a fixed-size array of fields: + +```diff +- debug_log_format_slice("values: {}", &[value1, value2]); ++ debug_log_format("values: {}", [value1, value2]); +``` + +This has been done as usage of Noir slices is discouraged and the function was unused in the aztec codebase. + +### [AztecNode] Sentinel validator status values renamed + +The `ValidatorStatusInSlot` values returned by `getValidatorsStats` and `getValidatorStats` have been updated to reflect the multi-block-per-slot model, where blocks and checkpoints are distinct concepts: + +```diff +- 'block-mined' ++ 'checkpoint-mined' + +- 'block-proposed' ++ 'checkpoint-proposed' + +- 'block-missed' ++ 'checkpoint-missed' // blocks were proposed but checkpoint was not attested ++ 'blocks-missed' // no block proposals were sent at all +``` + +The `attestation-sent` and `attestation-missed` values are unchanged but now explicitly refer to checkpoint attestations. + +The `ValidatorStatusType` used for categorizing statuses has also changed from `'block' | 'attestation'` to `'proposer' | 'attestation'`. + +### [aztec.js] `getDecodedPublicEvents` renamed to `getPublicEvents` with new signature + +The `getDecodedPublicEvents` function has been renamed to `getPublicEvents` and now uses a filter object instead of positional parameters: + +```diff +- import { getDecodedPublicEvents } from '@aztec/aztec.js/events'; ++ import { getPublicEvents } from '@aztec/aztec.js/events'; + +- const events = await getDecodedPublicEvents(node, eventMetadata, fromBlock, limit); ++ const events = await getPublicEvents(node, eventMetadata, { ++ fromBlock, ++ toBlock, ++ contractAddress, // optional ++ txHash, // optional ++ }); +``` + +The new function returns richer metadata including `contractAddress`, `txHash`, `l2BlockNumber`, and `l2BlockHash` for each event: + +```typescript +import { getPublicEvents } from "@aztec/aztec.js/events"; +import { MyContract } from "./artifacts/MyContract.js"; + +// Query events from a contract +const events = await getPublicEvents<{ amount: bigint; sender: AztecAddress }>( + aztecNode, + MyContract.events.Transfer, + { contractAddress: myContractAddress, fromBlock: BlockNumber(1) }, +); + +// Each event includes decoded data and metadata +for (const { event, metadata } of events) { + console.log(`Transfer of ${event.amount} from ${event.sender}`); + console.log(` Block: ${metadata.l2BlockNumber}, Tx: ${metadata.txHash}`); + console.log(` Contract: ${metadata.contractAddress}`); +} +``` + +### [Aztec.nr] `nophasecheck` renamed as `allow_phase_change` + +### [AztecNode] Removed sibling path RPC methods + +The following methods have been removed from the `AztecNode` interface: + +- `getNullifierSiblingPath` +- `getNoteHashSiblingPath` +- `getArchiveSiblingPath` +- `getPublicDataSiblingPath` + +These methods were not used by PXE and returned a subset of the information already available through the corresponding membership witness methods: + +| Removed Method | Use Instead | +| -------------------------- | ------------------------------- | +| `getNullifierSiblingPath` | `getNullifierMembershipWitness` | +| `getNoteHashSiblingPath` | `getNoteHashMembershipWitness` | +| `getArchiveSiblingPath` | `getBlockHashMembershipWitness` | +| `getPublicDataSiblingPath` | `getPublicDataWitness` | + +The membership witness methods return both the sibling path and additional context (leaf index, preimage data) needed for proofs. + +### [Protocol] "Nullifier secret key" renamed to "nullifier hiding key" (nsk → nhk) + +The nullifier secret key (`nsk_m` / `nsk_app`) has been renamed to nullifier hiding key (`nhk_m` / `nhk_app`). This is a protocol-breaking change: the domain separator string changes from `"az_nsk_m"` to `"az_nhk_m"`, producing a different constant value. + +**Noir changes:** + +```diff +- context.request_nsk_app(npk_m_hash) ++ context.request_nhk_app(npk_m_hash) + +- get_nsk_app(npk_m_hash) ++ get_nhk_app(npk_m_hash) +``` + +**TypeScript changes:** + +```diff +- import { computeAppNullifierSecretKey, deriveMasterNullifierSecretKey } from '@aztec/stdlib/keys'; ++ import { computeAppNullifierHidingKey, deriveMasterNullifierHidingKey } from '@aztec/stdlib/keys'; + +- const masterNullifierSecretKey = deriveMasterNullifierSecretKey(secret); ++ const masterNullifierHidingKey = deriveMasterNullifierHidingKey(secret); + +- const nskApp = await computeAppNullifierSecretKey(masterNullifierSecretKey, contractAddress); ++ const nhkApp = await computeAppNullifierHidingKey(masterNullifierHidingKey, contractAddress); +``` + +The `GeneratorIndex.NSK_M` enum member is now `GeneratorIndex.NHK_M`. + +### [AztecNode/Aztec.nr] `getArchiveMembershipWitness` renamed to `getBlockHashMembershipWitness` + +The `getArchiveMembershipWitness` method has been renamed to `getBlockHashMembershipWitness` to better reflect its purpose. Block hashes are the leaves of the archive tree - each time a new block is added to the chain, its block hash is appended as a new leaf. This rename clarifies that the method finds a membership witness for a block hash in the archive tree. + +**TypeScript (AztecNode interface):** + +```diff +- const witness = await aztecNode.getArchiveMembershipWitness(blockNumber, archiveLeaf); ++ const witness = await aztecNode.getBlockHashMembershipWitness(blockNumber, blockHash); +``` + +The second parameter type has also changed from `Fr` to `BlockHash`. + +**Noir (aztec-nr):** + +```diff +- use dep::aztec::oracle::get_membership_witness::get_archive_membership_witness; ++ use dep::aztec::oracle::get_membership_witness::get_block_hash_membership_witness; + +- let witness = get_archive_membership_witness(block_header, leaf_value); ++ let witness = get_block_hash_membership_witness(anchor_block_header, block_hash); +``` + +### [Aztec.nr] `protocol_types` renamed to `protocol` + +The `protocol_types` re-export from the `aztec` crate has been renamed to `protocol`. Update all imports accordingly: + +```diff +- use dep::aztec::protocol_types::address::AztecAddress; ++ use dep::aztec::protocol::address::AztecAddress; +``` + +### Protocol contract interface separate from protocol contracts + +We've stripped protocol contract of `aztec-nr` macros in order for auditors to not need to audit them (protocol contracts are to be audited during the protocol circuits audit). +This results in the nice Noir interface no longer being generated. + +For context, this is the interface I am talking about: + +```noir +let update_delay = self.view(MyContract::at(my_contract_address).my_fn()); +``` + +where the macros generate the `MyContract` struct. + +For this reason we've created place holder protocol contracts in `noir-projects/noir-contracts/contracts/protocol_interface` that still have these macros applied and hence you can use them to get the interface. + +On your side all you need to do is update the dependency in `Nargo.toml`: + +```diff +-instance_contract = { path = "../../protocol/contract_instance_registry" } ++instance_contract = { path = "../../protocol_interface/contract_instance_registry_interface" } +``` + +### [aztec-nr] History module refactored to use standalone functions + +The `aztec::history` module has been refactored to use standalone functions instead of traits. This changes the calling convention from method syntax to function syntax. + +```diff +- use dep::aztec::history::note_inclusion::ProveNoteInclusion; ++ use dep::aztec::history::note::assert_note_existed_by; + +let block_header = context.get_anchor_block_header(); +- let confirmed_note = block_header.prove_note_inclusion(hinted_note); ++ let confirmed_note = assert_note_existed_by(block_header, hinted_note); +``` + +**Function name and module mapping:** + +| Old (trait method) | New (standalone function) | +| ----------------------------------------------------------------- | -------------------------------------------------------------------- | +| `history::note_inclusion::prove_note_inclusion` | `history::note::assert_note_existed_by` | +| `history::note_validity::prove_note_validity` | `history::note::assert_note_was_valid_by` | +| `history::nullifier_inclusion::prove_nullifier_inclusion` | `history::nullifier::assert_nullifier_existed_by` | +| `history::nullifier_inclusion::prove_note_is_nullified` | `history::note::assert_note_was_nullified_by` | +| `history::nullifier_non_inclusion::prove_nullifier_non_inclusion` | `history::nullifier::assert_nullifier_did_not_exist_by` | +| `history::nullifier_non_inclusion::prove_note_not_nullified` | `history::note::assert_note_was_not_nullified_by` | +| `history::contract_inclusion::prove_contract_deployment` | `history::deployment::assert_contract_bytecode_was_published_by` | +| `history::contract_inclusion::prove_contract_non_deployment` | `history::deployment::assert_contract_bytecode_was_not_published_by` | +| `history::contract_inclusion::prove_contract_initialization` | `history::deployment::assert_contract_was_initialized_by` | +| `history::contract_inclusion::prove_contract_non_initialization` | `history::deployment::assert_contract_was_not_initialized_by` | +| `history::public_storage::public_storage_historical_read` | `history::storage::public_storage_historical_read` | + +### [Aztec.js] Transaction sending API redesign + +The old chained `.send().wait()` pattern has been replaced with a single `.send(options)` call that handles both sending and waiting. + +```diff ++ import { Contract, NO_WAIT } from '@aztec/aztec.js/contracts'; + +- const receipt = await contract.methods.transfer(recipient, amount).send().wait(); + +// Send now waits by default ++ const receipt = await contract.methods.transfer(recipient, amount).send({ from: sender }); + +// getTxHash() would confusingly send the transaction too +- const txHash = await contract.methods.transfer(recipient, amount).send().getTxHash(); + +// NO_WAIT to send the transaction and return TxHash immediately ++ const txHash = await contract.methods.transfer(recipient, amount).send({ ++ from: sender, ++ wait: NO_WAIT ++ }); +``` + +#### Deployment changes + +The old `.send().deployed()` method has been removed. Deployments now return the contract instance by default, or you can request the full receipt with `returnReceipt: true`: + +```diff +- const contract = await MyContract.deploy(wallet, ...args).send().deployed(); +- const { contract, instance } = await MyContract.deploy(wallet, ...args).send().wait(); + ++ const contract = await MyContract.deploy(wallet, ...args).send({ from: deployer }); + ++ const { contract, instance } = await MyContract.deploy(wallet, ...args).send({ ++ from: deployer, ++ wait: { returnReceipt: true }, ++ }); +``` + +#### Breaking changes to `Wallet` interface + +`getTxReceipt()` has been removed from the interface. + +`sendTx` method signature has changed to support the new wait behavior: + +```diff +- sendTx(payload: ExecutionPayload, options: SendOptions): Promise<TxReceipt> + ++ sendTx<W extends InteractionWaitOptions = undefined>( ++ payload: ExecutionPayload, ++ options: SendOptions<W> ++ ): Promise<SendReturn<W>> +``` + +#### Manual waiting with `waitForTx` + +When using `NO_WAIT` to send transactions, you can manually wait for confirmation using the `waitForTx` utility: + +```typescript +import { waitForTx } from "@aztec/aztec.js/node"; + +const txHash = await contract.methods.transfer(recipient, amount).send({ + from: sender, + wait: NO_WAIT, +}); + +const receipt = await waitForTx(node, txHash, { + timeout: 60000, // Optional: timeout in ms + interval: 1000, // Optional: polling interval in ms + dontThrowOnRevert: true, // Optional: return receipt even if tx reverted +}); +``` + +### [aztec-nr] Removal of intermediate modules + +Lots of unnecessary modules have been removed from the API, making imports shorter. These are the modules that contain just a single struct, in which the module has the same name as the struct. + +```diff +- use aztec::state_vars::private_mutable::PrivateMutable; ++ use aztec::state_vars::PrivateMutable; +``` + +Affected structs include all state variables, notes, contexts, messages, etc. + +### [L1 Contracts] Fee asset pricing direction inverted + +The fee model now uses `ethPerFeeAsset` instead of the previous `feeAssetPerEth`. This change inverts how the exchange rate is represented: values now express how much ETH one fee asset (AZTEC) is worth, with 1e12 precision. + +**Key changes:** + +- `FeeHeader.feeAssetPerEth` → `FeeHeader.ethPerFeeAsset` +- `RollupConfigInput` now requires `initialEthPerFeeAsset` parameter at deployment +- Default value: `1e7` (0.00001 ETH per AZTEC) +- Valid range: `100` (1e-10 ETH/AZTEC) to `1e11` (0.1 ETH/AZTEC) + +**New environment variable for node operators:** + +- `AZTEC_INITIAL_ETH_PER_FEE_ASSET` - Sets the initial ETH per fee asset price with 1e12 precision + +### [L1 Contracts] Fee asset price modifier now in basis points + +The `OracleInput.feeAssetPriceModifier` field now expects values in basis points (BPS) instead of the previous representation. The modifier is applied as a percentage change to the ETH/AZTEC price each checkpoint. + +**Key changes:** + +- Valid range: `-100` to `+100` BPS (±1% max change per checkpoint) +- A value of `+100` increases the price by 1%, `-100` decreases by 1% +- Validated by `MAX_FEE_ASSET_PRICE_MODIFIER_BPS = 100` + +### [Aztec.js] Wallet batching now supports all methods + +The `BatchedMethod` type is now a discriminated union that ensures type safety: the `args` must match the specific method `name`. This prevents runtime errors from mismatched arguments. + +```diff +- // Before: Only 5 methods could be batched +- const results = await wallet.batch([ +- { name: "registerSender", args: [address, "alias"] }, +- { name: "sendTx", args: [payload, options] }, +- ]); ++ // After: All methods can be batched ++ const results = await wallet.batch([ ++ { name: "getChainInfo", args: [] }, ++ { name: "getContractMetadata", args: [contractAddress] }, ++ { name: "registerSender", args: [address, "alias"] }, ++ { name: "simulateTx", args: [payload, options] }, ++ { name: "sendTx", args: [payload, options] }, ++ ]); +``` + +### [Aztec.js] Refactored `getContractMetadata` and `getContractClassMetadata` in Wallet + +The contract metadata methods in the `Wallet` interface have been refactored to provide more granular information and avoid expensive round-trips. + +**`ContractMetadata`:** + +```diff + { +- contractInstance?: ContractInstanceWithAddress, ++ instance?: ContractInstanceWithAddress; // Instance registered in the Wallet, if any + isContractInitialized: boolean; // Is the init nullifier onchain? (already there) + isContractPublished: boolean; // Has the contract been published? (already there) ++ isContractUpdated: boolean; // Has the contract been updated? ++ updatedContractClassId?: Fr; // If updated, the new class ID + } +``` + +**`ContractClassMetadata`:** + +This method loses the ability to request the contract artifact via the `includeArtifact` flag + +```diff + { +- contractClass?: ContractClassWithId; +- artifact?: ContractArtifact; + isContractClassPubliclyRegistered: boolean; // Is the class registered onchain? ++ isArtifactRegistered: boolean; // Does the Wallet know about this artifact? + } +``` + +- Removes expensive artifact/class transfers between wallet and app +- Separates PXE storage info (`instance`, `isArtifactRegistered`) from public chain info (`isContractPublished`, `isContractClassPubliclyRegistered`) +- Makes it easier to determine if actions like `registerContract` are needed + +### [Aztec.js] Removed `UnsafeContract` and protocol contract helper functions + +The `UnsafeContract` class and async helper functions (`getFeeJuice`, `getClassRegistryContract`, `getInstanceRegistryContract`) have been removed. Protocol contracts are now accessed via auto-generated type-safe wrappers with only the ABI (no bytecode). Since PXE always has protocol contract artifacts available, importing and using these contracts from `aztec.js` is very lightweight and follows the same pattern as regular user contracts. + +**Migration:** + +```diff +- import { getFeeJuice, getClassRegistryContract, getInstanceRegistryContract } from '@aztec/aztec.js/contracts'; ++ import { FeeJuiceContract, ContractClassRegistryContract, ContractInstanceRegistryContract } from '@aztec/aztec.js/protocol'; + +- const feeJuice = await getFeeJuice(wallet); ++ const feeJuice = FeeJuiceContract.at(wallet); + await feeJuice.methods.check_balance(feeLimit).send().wait(); + +- const classRegistry = await getClassRegistryContract(wallet); ++ const classRegistry = ContractClassRegistryContract.at(wallet); + await classRegistry.methods.publish(...).send().wait(); + +- const instanceRegistry = await getInstanceRegistryContract(wallet); ++ const instanceRegistry = ContractInstanceRegistryContract.at(wallet); + await instanceRegistry.methods.publish_for_public_execution(...).send().wait(); +``` + +**Note:** The higher-level utilities like `publishInstance`, `publishContractClass`, and `broadcastPrivateFunction` from `@aztec/aztec.js/deployment` are still available and unchanged. These utilities use the new wrappers internally. + +### [Aztec.nr] Renamed Router contract + +`Router` contract has been renamed as `PublicChecks` contract. +The name of the contract became stale as its use changed from routing public calls through it to simply having public functions that can be called by anyone. +Having these "standard checks" on one contract results in a potentially large privacy set for apps that use it. + +### [Aztec Node] `getBlockByHash` and `getBlockHeaderByHash` removed + +The `getBlockByHash` and `getBlockHeaderByHash` methods have been removed. Use `getBlock` and `getBlockHeader` with a block hash instead. + +**Migration:** + +```diff +- const block = await node.getBlockByHash(blockHash); ++ const block = await node.getBlock(blockHash); + +- const header = await node.getBlockHeaderByHash(blockHash); ++ const header = await node.getBlockHeader(blockHash); +``` + +### [Aztec.nr] Oracle functions now take `BlockHeader` instead of block number + +The low-level oracle functions for fetching membership witnesses and storage now take a `BlockHeader` instead of a `block_number: u32`. This change improves type safety and ensures the correct block state is queried. + +**Affected functions:** + +- `get_note_hash_membership_witness(block_header, leaf_value)` - was `(block_number, leaf_value)` +- `get_archive_membership_witness(block_header, leaf_value)` - was `(block_number, leaf_value)` +- `get_nullifier_membership_witness(block_header, nullifier)` - was `(block_number, nullifier)` +- `get_low_nullifier_membership_witness(block_header, nullifier)` - was `(block_number, nullifier)` +- `get_public_data_witness(block_header, public_data_tree_index)` - was `(block_number, public_data_tree_index)` +- `storage_read(block_header, address, storage_slot)` - was `(address, storage_slot, block_number)` + +**Migration:** + +If you were calling these oracle functions directly (which is uncommon), update your code to pass a `BlockHeader` instead of a block number: + +```diff +- let witness = get_note_hash_membership_witness(self.global_variables.block_number, note_hash); ++ let witness = get_note_hash_membership_witness(self, note_hash); + +- let witness = get_nullifier_membership_witness(block_number, nullifier); ++ let witness = get_nullifier_membership_witness(block_header, nullifier); + +- let value: T = storage_read(address, slot, block_number); ++ let value: T = storage_read(block_header, address, slot); +``` + +Note: The high-level history proof functions on `BlockHeader` (such as `prove_note_inclusion`, `prove_nullifier_inclusion`, etc.) are **not affected** by this change. They continue to work the same way. + +### [Toolchain] Node.js upgraded to v24 + +Node.js minimum version changed from v22 to v24.12.0. + +### [L1 Contracts] Renamed base fee to min fee + +The L1 rollup contract functions and types related to fee calculation have been renamed from "base fee" to "min fee" to better reflect their purpose. + +**Renamed functions:** + +- `getManaBaseFeeAt` → `getManaMinFeeAt` +- `getManaBaseFeeComponentsAt` → `getManaMinFeeComponentsAt` + +**Renamed types:** + +- `ManaBaseFeeComponents` → `ManaMinFeeComponents` + +**Renamed errors:** + +- `Rollup__InvalidManaBaseFee` → `Rollup__InvalidManaMinFee` + +**Migration:** + +```diff +- uint256 fee = rollup.getManaBaseFeeAt(timestamp, true); ++ uint256 fee = rollup.getManaMinFeeAt(timestamp, true); + +- ManaBaseFeeComponents memory components = rollup.getManaBaseFeeComponentsAt(timestamp, true); ++ ManaMinFeeComponents memory components = rollup.getManaMinFeeComponentsAt(timestamp, true); +``` + +### [Aztec.js] Renamed base fee to min fee + +The Aztec Node API method for getting current fees has been renamed: + +- `getCurrentBaseFees` → `getCurrentMinFees` + +**Migration:** + +```diff +- const fees = await node.getCurrentBaseFees(); ++ const fees = await node.getCurrentMinFees(); +``` + +### [Aztec.nr] Renamed fee context methods + +The context methods for accessing fee information have been renamed: + +- `context.base_fee_per_l2_gas()` → `context.min_fee_per_l2_gas()` +- `context.base_fee_per_da_gas()` → `context.min_fee_per_da_gas()` + +**Migration:** + +```diff +- let l2_fee = context.base_fee_per_l2_gas(); +- let da_fee = context.base_fee_per_da_gas(); ++ let l2_fee = context.min_fee_per_l2_gas(); ++ let da_fee = context.min_fee_per_da_gas(); +``` + +### [Aztec.nr] Cleaning up message sender functions + +There has been a design decision made to have low-level API exposed on `self.context` and a nicer higher-level API exposed directly on `self`. +Currently the `msg_sender` function on `self` was a copy of that same function on `self.context`. +The `msg_sender` function on `self` got modified to return the message sender address directly instead of having it be wrapped in an `Option<...>`. +In case the underlying message sender is none the function panics. + +You need to update your code to no longer trigger the unwrap on the return value: + +```diff +- let message_sender: AztecAddress = self.msg_sender().unwrap(); ++ let message_sender: AztecAddress = self.msg_sender(); +``` + +If you want to handle the `null` case use the lower level API of context: + +```diff +- let maybe_message_sender: Option<AztecAddress> = self.msg_sender(); ++ let maybe_message_sender: Option<AztecAddress> = self.context.maybe_msg_sender(); +``` + +The `self.context.msg_sender_unsafe` method has been dropped as its use can be replaced with the standard `self.context.maybe_msg_sender` function. + +### [Aztec.nr] Renamed message delivery options + +The following terms have been renamed: + +- `MessageDelivery::UNCONSTRAINED_OFFCHAIN` -> `MessageDelivery::OFFCHAIN` +- `MessageDelivery::UNCONSTRAINED_ONCHAIN` -> `MessageDelivery::ONCHAIN_UNCONSTRAINED` +- `MessageDelivery::CONSTRAINED_ONCHAIN` -> `MessageDelivery::ONCHAIN_CONSTRAINED` + +We believe these names will better convey the meaning of the concepts. + +### [Aztec Node] changes to `getLogsByTags` endpoint + +`getLogsByTags` endpoint has been optimized for our new log sync algorithm and these are the changes: + +- The `logsPerTag` pagination argument has been removed. Pagination was unnecessary here, since multiple logs per tag typically only occur if several devices are sending logs from the same sender to a recipient, which is unlikely to generate enough logs to require pagination. +- The structure of `TxScopedL2Log` has been revised to meet the requirements of our new log sync algorithm. +- The endpoint has been separated into two versions: `getPrivateLogsByTags` and `getPublicLogsByTagsFromContract`. This change was made because it was never desirable in PXE to mix public and private logs. The public version requires both a `Tag` and a contract address as input. In contrast to the private version—which uses `SiloedTag` (a tag that hashes the raw tag with the emitting contract's address)—the public version uses the raw `Tag` type, since kernels do not hash the tag with the contract address for public logs. + +### [AVM] Gas cost multipliers for public execution to reach simulation/proving parity + +Gas costs for several AVM opcodes have been adjusted with multipliers to better align public simulation costs with actual proving costs. + +| Opcode | Multiplier | Previous Cost | New Cost | +| ------------------- | ---------- | ------------- | -------- | +| FDIV | 25x | 9 | 225 | +| SLOAD | 10x | 129 | 1,290 | +| SSTORE | 20x | 1,657 | 33,140 | +| NOTEHASHEXISTS | 4x | 126 | 504 | +| EMITNOTEHASH | 15x | 1,285 | 19,275 | +| NULLIFIEREXISTS | 7x | 132 | 924 | +| EMITNULLIFIER | 20x | 1,540 | 30,800 | +| L1TOL2MSGEXISTS | 5x | 108 | 540 | +| SENDL2TOL1MSG | 2x | 209 | 418 | +| CALL | 3x | 3,312 | 9,936 | +| STATICCALL | 3x | 3,312 | 9,936 | +| GETCONTRACTINSTANCE | 4x | 1,527 | 6,108 | +| POSEIDON2 | 15x | 24 | 360 | +| ECADD | 10x | 27 | 270 | + +**Impact**: Contracts with public bytecode performing any of these operations will see increased gas consumption. + +### [PXE] deprecated `getNotes` + +This function serves only for debugging purposes so we are taking it out of the main PXE API. If you still need to consume it, you can +do so through the new `debug` sub-module. + +```diff +- this.pxe.getNotes(filter); ++ this.pxe.debug.getNotes(filter); +``` + +## 3.0.0-devnet.20251212 + +### [Aztec node, archiver] Deprecated `getPrivateLogs` + +Aztec node no longer offers a `getPrivateLogs` method. If you need to process the logs of a block, you can instead use `getBlock` and call `getPrivateLogs` on an `L2BlockNew` instance. See the diff below for before/after equivalent code samples. + +```diff +- const logs = await aztecNode.getPrivateLogs(blockNumber, 1); ++ const logs = (await aztecNode.getBlock(blockNumber))?.toL2Block().getPrivateLogs(); +``` + +### [Aztec.nr] Private event emission API changes + +Private events are still emitted via the `emit` function, but this now returns an `EventMessage` type that must have `deliver_to` called on it in order to deliver the event message to the intended recipients. This allows for multiple recipients to receive the same event. + +```diff +- self.emit(event, recipient, delivery_method) ++ self.emit(event).delivery(recipient, delivery_method) +``` + +### [Aztec.nr] History proof functions no longer require `storage_slot` parameter + +The `HintedNote` struct now includes a `storage_slot` field, making it self-contained for proving note inclusion and validity. As a result, the history proof functions in the `aztec::history` module no longer require a separate `storage_slot` parameter. + +**Affected functions:** + +- `BlockHeader::prove_note_inclusion` - removed `storage_slot: Field` parameter +- `BlockHeader::prove_note_validity` - removed `storage_slot: Field` parameter +- `BlockHeader::prove_note_is_nullified` - removed `storage_slot: Field` parameter +- `BlockHeader::prove_note_not_nullified` - removed `storage_slot: Field` parameter + +**Migration:** + +The `storage_slot` is now read from `hinted_note.storage_slot` internally. Simply remove the `storage_slot` argument from all calls to these functions: + +```diff + let header = context.get_anchor_block_header(); +- header.prove_note_inclusion(hinted_note, storage_slot); ++ header.prove_note_inclusion(hinted_note); + + let header = context.get_anchor_block_header(); +- header.prove_note_validity(hinted_note, storage_slot, context); ++ header.prove_note_validity(hinted_note, context); + + let header = context.get_anchor_block_header(); +- header.prove_note_is_nullified(hinted_note, storage_slot, context); ++ header.prove_note_is_nullified(hinted_note, context); + + let header = context.get_anchor_block_header(); +- header.prove_note_not_nullified(hinted_note, storage_slot, context); ++ header.prove_note_not_nullified(hinted_note, context); +``` + +### [Aztec.nr] Note fields are now public + +All note struct fields are now public, and the `new()` constructor methods and getter methods have been removed. Notes should be instantiated using struct literal syntax, and fields should be accessed directly. + +The motivation for this change has been enshrining of randomness which lead to the `new` method being unnecessary boilerplate. + +**Affected notes:** + +- `UintNote` - `value` is now public, `new()` and `get_value()` removed +- `AddressNote` - `address` is now public, `new()` and `get_address()` removed +- `FieldNote` - `value` is now public, `new()` and `value()` removed + +**Migration:** + +```diff +- let note = UintNote::new(100); ++ let note = UintNote { value: 100 }; + +- let value = note.get_value(); ++ let value = note.value; + +- let address_note = AddressNote::new(owner); ++ let address_note = AddressNote { address: owner }; + +- let address = address_note.get_address(); ++ let address = address_note.address; + +- let field_note = FieldNote::new(42); ++ let field_note = FieldNote { value: 42 }; + +- let value = field_note.value(); ++ let value = field_note.value; +``` + +### [Aztec.nr] `emit` renamed to `deliver` + +Private state variable functions that created notes and returned their messages no longer return a `NoteEmission` but instead a `NoteMessage`. These messages are delivered to their owner via `deliver` instead of `emit`. The verb 'emit' remains for things like emitting events. + +```diff +- self.storage.balances.at(owner).add(5).emit(owner); ++ self.storage.balances.at(owner).add(5).deliver(); +``` + +To deliver a message to a different recipient, use `deliver_to`: + +```diff +- self.storage.balances.at(owner).add(5).emit(other); ++ self.storage.balances.at(owner).add(5).deliver_to(other); +``` + +### [Aztec.nr] `ValueNote` renamed to `FieldNote` and `value-note` crate renamed to `field-note` + +The `ValueNote` struct has been renamed to `FieldNote` to better reflect that it stores a `Field` value. The crate has also been renamed from `value-note` to `field-note`. + +**Migration:** + +- Update your `Nargo.toml` dependencies: `value_note = { path = "..." }` → `field_note = { path = "..." }` +- Update imports: `use value_note::value_note::ValueNote` → `use field_note::field_note::FieldNote` +- Update type references: `ValueNote` → `FieldNote` +- Update generic parameters: `PrivateSet<ValueNote, ...>` → `PrivateSet<FieldNote, ...>` + +### [Aztec.nr] New `balance-set` library for managing token balances + +A new `balance-set` library has been created that provides `BalanceSet<Context>` for managing u128 token balances with `UintNote`. This consolidates balance management functionality that was previously duplicated across contracts. + +**Features:** + +- `add(amount: u128)` - Add to balance +- `sub(amount: u128)` - Subtract from balance (with change note) +- `try_sub(amount: u128, max_notes: u32)` - Attempt to subtract with configurable note limit +- `balance_of()` - Get total balance (unconstrained) + +**Usage:** + +```rust +use balance_set::BalanceSet; + +#[storage] +struct Storage<Context> { + balances: Owned<BalanceSet<Context>, Context>, +} + +// In a private function: +self.storage.balances.at(owner).add(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN); +self.storage.balances.at(owner).sub(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN); + +// In an unconstrained function: +let balance = self.storage.balances.at(owner).balance_of(); +``` + +### [Aztec.nr] `EasyPrivateUint` deprecated and removed + +The `EasyPrivateUint` type and `easy-private-state` crate have been deprecated and removed. Use `BalanceSet` from the `balance-set` crate instead. + +**Migration:** + +- Remove `easy_private_state` dependency from `Nargo.toml` +- Add `balance_set = { path = "../../../../aztec-nr/balance-set" }` to `Nargo.toml` +- Update storage: `EasyPrivateUint<Context>` → `Owned<BalanceSet<Context>, Context>` +- Update method calls: + - `add(amount, owner)` → `at(owner).add(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN)` + - `sub(amount, owner)` → `at(owner).sub(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN)` + - `get_value(owner)` → `at(owner).balance_of()` (returns `u128` instead of `Field`) + +### [Aztec.nr] `balance_utils` removed from `value-note` (now `field-note`) + +The `balance_utils` module has been removed from the `field-note` crate (formerly `value-note`). If you need similar functionality, implement it locally in your contract or use `BalanceSet` for u128 balances. + +### [Aztec.nr] `filter_notes_min_sum` removed from `value-note` (now `field-note`) + +The `filter_notes_min_sum` function has been removed from the `field-note` crate (formerly in `value-note`). If you need this functionality, copy it to your contract locally. This function was only used in specific test contracts and doesn't belong in the general-purpose note library. + +### [Aztec.nr] `derive_ecdh_shared_secret_using_aztec_address` removed + +This function made it annoying to deal with invalid addresses in circuits. If you were using it, replace it with `derive_ecdh_shared_secret` instead: + +```diff +-let shared_secret = derive_ecdh_shared_secret_using_aztec_address(secret, address).unwrap(); ++let shared_secret = derive_ecdh_shared_secret(secret, address.to_address_point().unwrap().inner); +``` + +### [Aztec.nr] Note owner is now enshrined + +It turns out that in all cases a note always has a logical owner. +For this reason we have decided to enshrine the concept of a note owner and you should drop the field from your note: + +```diff +#[derive(Deserialize, Eq, Packable, Serialize)] +#[note] +pub struct ValueNote { + value: Field, +- owner: AztecAddress, +} +``` + +The owner being enshrined means that our API explicitly expects it on the input. +The `NoteHash` trait got modified as follows: + +```diff +pub trait NoteHash { + fn compute_note_hash( + self, ++ owner: AztecAddress, + storage_slot: Field, + randomness: Field, + ) -> Field; + + fn compute_nullifier( + self, + context: &mut PrivateContext, ++ owner: AztecAddress, + note_hash_for_nullification: Field, + ) -> Field; + + unconstrained fn compute_nullifier_unconstrained( + self, ++ owner: AztecAddress, + note_hash_for_nullification: Field, + ) -> Field; +} +``` + +Our low-level note utilities now also accept owner as a parameter: + +```diff +pub fn create_note<Note>( + context: &mut PrivateContext, ++ owner: AztecAddress, + storage_slot: Field, + note: Note, +) -> NoteEmission<Note> +where + Note: NoteType + NoteHash + Packable, +{ +... +} +``` + +Signature of some functions like `destroy_note_unsafe` is unchanged: + +```diff +pub fn destroy_note_unsafe<Note>( + context: &mut PrivateContext, + hinted_note: HintedNote<Note>, + note_hash_read: NoteHashRead, +) +where + Note: NoteHash, +{ +... +} +``` + +because `HintedNote` now contains owner. + +`PrivateImmutable`, `PrivateMutable` and `PrivateSet` got modified to directly contain the owner instead of implicitly "containing it" by including it in the storage slot via a `Map`. +These state variables now implement a newly introduced `OwnedStateVariable` trait (see docs of `OwnedStateVariable` for explanation of what it is). +These changes make the state variables incompatible with `Map` and now instead these should be wrapped in new `Owned` state variable: + +```diff +#[storage] +struct Storage<Context> { +- private_nfts: Map<AztecAddress, PrivateSet<NFTNote, Context>, Context>, ++ private_nfts: Owned<PrivateSet<NFTNote, Context>, Context>, +} +``` + +Note that even though the types of your state variables are changing from `Map<AztecAddress, T, Context>` to `Owned<T, Context>`, usage remains unchanged: + +``` +let nft_notes = self.storage.private_nfts.at(from).pop_notes(NoteGetterOptions::new().select(NFTNote::properties().token_id, Comparator.EQ, token_id).set_limit(1)); +``` + +With this change the underlying notes will inherit the storage slot of the `Owned` state variable. +This is unlike `Map` where the nested state variable got the storage slot computed as `hash([map_storage_slot, key])`. + +if you had `PrivateImmutable` or `PrivateMutable` defined out of a `Map`, e.g.: + +```rust +#[storage] +struct Storage<Context> { + signing_public_key: PrivateImmutable<PublicKeyNote, Context>, +} +``` + +you were most likely dealing with some kind of admin flow where only the admin can modify the state variable. +Now, unfortunately, there is a bit of a regression and you will need to wrap the state variable in `Owned` and call `at` on the state var: + +```diff ++ use aztec::state_vars::Owned; + +#[storage] +struct Storage<Context> { +- signing_public_key: PrivateImmutable<PublicKeyNote, Context>, ++ signing_public_key: Owned<PrivateImmutable<PublicKeyNote, Context>, Context>, +} + +#[external("private")] +fn my_external_function() { +- self.storage.signing_public_key.initialize(pub_key_note) ++ self.storage.signing_public_key.at(self.address).initialize(pub_key_note) + .emit(self.address, MessageDelivery.CONSTRAINED_ONCHAIN); +} +``` + +We are likely to come up with a concept of admin state variables in the future. + +None of the reference notes now contain the owner so if you manually construct `AddressNote`, `UintNote` or `ValueNote` you need to update the call to `new` method: + +```diff +- let note = UintNote::new(156, owner); ++ let note = UintNote::new(156); +``` + +### [Aztec.nr] Note randomness is now handled internally + +In order to prevent pre-image attacks, it is necessary to inject randomness to notes. Aztec.nr users were previously expected to add said randomness to their custom note types. From now on, Aztec.nr takes care of handling randomness as built-in note metadata, making it impossible to miss for library users. This change breaks backwards compatibility as we'll discuss below. + +#### Changes to Aztec.nr note types + +If you're using any of the following note types, please be aware that `randomness` no longer is an explicit attribute in them. + +- ValueNote +- UintNote +- NFTNote +- AddressNote + +#### Migrating your custom note types: refer to UintNote as an example of how to migrate + +We show the changes to `UintNote` below since it serves as a good example of the adjustments you will need to make to your own custom note types, including those that need to support partial notes. + +##### Remove `randomness` from note struct + +```diff +pub struct UintNote { + /// The owner of the note, i.e. the account whose nullifier secret key is required to compute the nullifier. + owner: AztecAddress, +- /// Random value, protects against note hash preimage attacks. +- randomness: Field, + /// The number stored in the note. + value: u128, +} + +impl UintNote { + pub fn new(value: u128, owner: AztecAddress) -> Self { +- let randomness = unsafe { random() }; +- Self { value, owner, randomness } ++ Self { value, owner } + } +``` + +##### Add `randomness` to `compute_note_hash` implementation + +The `NoteHash` trait now requires `compute_note_hash` to receive a `randomness` field. This impacts + +```diff +pub trait NoteHash { + /// ... +- fn compute_note_hash(self, storage_slot: Field) -> Field; ++ fn compute_note_hash(self, storage_slot: Field, randomness: Field) -> Field; +``` + +Then in trait implementations: + +```diff +impl NoteHash for UintNote { +- fn compute_note_hash(self, storage_slot: Field) -> Field { ++ fn compute_note_hash(self, storage_slot: Field, randomness: Field) -> Field { + /// ... +- let private_content = +- UintPartialNotePrivateContent { owner: self.owner, randomness: self.randomness }; +- let partial_note = PartialUintNote { +- commitment: private_content.compute_partial_commitment(storage_slot), +- }; + ++ let private_content = ++ UintPartialNotePrivateContent { owner: self.owner }; ++ let partial_note = PartialUintNote { ++ commitment: private_content.compute_partial_commitment(storage_slot, randomness), ++ }; +``` + +It's worth noting that this change also affects how partial notes are structured and handled. + +```diff +pub fn partial( + owner: AztecAddress, + storage_slot: Field, + randomness: Field, + context: &mut PrivateContext, + recipient: AztecAddress, + completer: AztecAddress, + ) -> PartialUintNote { +- let commitment = UintPartialNotePrivateContent { owner, randomness } +- .compute_partial_commitment(storage_slot); ++ let commitment = UintPartialNotePrivateContent { owner } ++ .compute_partial_commitment(storage_slot, randomness); + + let private_log_content = +- UintPartialNotePrivateLogContent { owner, randomness, public_log_tag: commitment }; ++ UintPartialNotePrivateLogContent { owner, public_log_tag: commitment }; + let encrypted_log = note::compute_partial_note_private_content_log( + private_log_content, + storage_slot, ++ randomness, + recipient, + ); + /// ... +} + +struct UintPartialNotePrivateContent { + owner: AztecAddress, +- randomness: Field, +} + +impl UintPartialNotePrivateContent { +- fn compute_partial_commitment(self, storage_slot: Field) -> Field { ++ fn compute_partial_commitment(self, storage_slot: Field, randomness: Field) -> Field { + poseidon2_hash_with_separator( +- self.pack().concat([storage_slot]), ++ self.pack().concat([storage_slot, randomness]), + DOM_SEP__NOTE_HASH, + ) + } +} + +struct UintPartialNotePrivateLogContent { + public_log_tag: Field, + owner: AztecAddress, +- randomness: Field, +} +``` + +##### Note size + +As a result of this change, the maximum packed length of the content of a note is 11 fields, down from 12. This is a direct consequence of moving the randomness field from the note content structure to the note's metadata. + +#### HintedNote now includes randomness field + +```diff +pub struct HintedNote<Note> { + pub note: Note, + pub contract_address: AztecAddress, ++ pub randomness: Field, + pub metadata: NoteMetadata, +} +``` + +### [L1 Contracts] `Block` is now `Checkpoint` + +A `checkpoint` is now the primary unit handled by the L1 contracts. + +A checkpoint may contain one or more L2 blocks. The protocol circuits already support producing multiple blocks per checkpoint. Updating the L1 contracts to operate on checkpoints allow L2 blockchain to advance faster. + +Below are the API and event renames reflecting this change: + +```diff +- event L2BlockProposed ++ event CheckpointProposed +``` + +```diff +- event BlockInvalidated ++ event CheckpointInvalidated +``` + +```diff +- function getEpochForBlock(uint256 _blockNumber) external view returns (Epoch); ++ function getEpochForCheckpoint(uint256 _checkpointNumber) external view returns (Epoch); +``` + +```diff +- function getProvenBlockNumber() external view returns (uint256); ++ function getProvenCheckpointNumber() external view returns (uint256); +``` + +```diff +- function getPendingBlockNumber() external view returns (uint256); ++ function getPendingCheckpointNumber() external view returns (uint256); +``` + +```diff +- function getBlock(uint256 _blockNumber) external view returns (BlockLog memory); ++ function getCheckpoint(uint256 _checkpointNumber) external view returns (CheckpointLog memory); +``` + +```diff +- function getBlockReward() external view returns (uint256); ++ function getCheckpointReward() external view returns (uint256); +``` + +Additionally, any function or struct that previously referenced an L2 block number now uses a checkpoint number instead: + +```diff +- function status(uint256 _blockNumber) external view returns ( ++ function status(uint256 _checkpointNumber) external view returns ( +- uint256 provenBlockNumber, ++ uint256 provenCheckpointNumber, + bytes32 provenArchive, +- uint256 pendingBlockNumber, ++ uint256 pendingCheckpointNumber, + bytes32 pendingArchive, + bytes32 archiveOfMyBlock, + Epoch provenEpochNumber +); +``` + +Note: current node softwares still produce exactly one L2 block per checkpoint, so for now checkpoint numbers and L2 block numbers remain equal. This may change once multi-block checkpoints are enabled. + +### [L1 Contracts] L2-to-L1 messages are now grouped by epoch. + +L2-to-L1 messages are aggregated and organized per epoch rather than per block, but callers should now ask the node to compute the membership witness directly from the emitting transaction hash. The node resolves the epoch and Outbox root internally. + +**Note**: This is only an API change. The protocol behavior remains the same - messages can still only be consumed once an epoch is proven as before. + +#### What changed + +Previously, callers fetched epoch messages and computed the witness client-side. Now, call `getL2ToL1MembershipWitness` on the node: + +```typescript +const witness = await node.getL2ToL1MembershipWitness( + l2TxReceipt.txHash, + l2ToL1Message, +); +``` + +### [Aztec.js] Wallet interface changes + +#### `simulateTx` is now batchable + +The `simulateTx` method on the `Wallet` interface is now batchable, meaning it can be called as part of a batch operation using `wallet.batch()`. This allows you to batch simulations together with other wallet operations like `registerContract`, `sendTx`, and `registerSender`. + +```diff +- // Could not batch simulations +- const simulationResult = await wallet.simulateTx(executionPayload, options); ++ // Can now batch simulations with other operations ++ const results = await wallet.batch([ ++ { name: 'registerContract', args: [instance, artifact] }, ++ { name: 'simulateTx', args: [executionPayload, options] }, ++ { name: 'sendTx', args: [anotherPayload, sendOptions] }, ++ ]); +``` + +#### `ExecutionPayload` moved to `@aztec/stdlib/tx` + +The `ExecutionPayload` type has been moved from `@aztec/aztec.js` to `@aztec/stdlib/tx`. Update your imports accordingly. + +```diff +- import { ExecutionPayload } from '@aztec/aztec.js'; ++ import { ExecutionPayload } from '@aztec/stdlib/tx'; ++ // Or import from the re-export in aztec.js/tx: ++ import { ExecutionPayload } from '@aztec/aztec.js/tx'; +``` + +#### `ExecutionPayload` now includes `feePayer` property + +The `ExecutionPayload` class now includes an optional `feePayer` property that specifies which address is paying for the fee in the execution payload (if any) + +```diff + const payload = new ExecutionPayload( + calls, + authWitnesses, + capsules, + extraHashedArgs, ++ feePayer // optional AztecAddress + ); +``` + +This was previously provided as part of the `SendOptions` (and others) in the wallet interface, which could cause problems if a payload was assembled with a payment method and the parameter was later omitted. This means `SendOptions` now loses `embeddedPaymentMethodFeePayer` + +```diff +-wallet.simulateTx(executionPayload, { from: address, embeddedFeePaymentMethodFeePayer: feePayer }); ++wallet.simulateTx(executionPayload, { from: address }); +``` + +#### `simulateUtility` signature and return type changed + +The `simulateUtility` method signature has changed to accept a `FunctionCall` object instead of separate `functionName`, `args`, and `to` parameters. Additionally, the return type has changed from `AbiDecoded` to `Fr[]`. + +```diff +- const result: AbiDecoded = await wallet.simulateUtility(functionName, args, to, authWitnesses); ++ const result: UtilitySimulationResult = await wallet.simulateUtility(functionCall, authWitnesses?); ++ // result.result is now Fr[] instead of AbiDecoded +``` + +The new signature takes: + +- `functionCall`: A `FunctionCall` object containing `name`, `args`, `to`, `selector`, `type`, `isStatic`, `hideMsgSender`, and `returnTypes` +- `authWitnesses` (optional): An array of `AuthWitness` objects + +The first argument is exactly the same as what goes into `ExecutionPayload.calls`. As such, the data is already encoded. The return value is now `UtilitySimulationResult` with `result: Fr[]` instead of returning an `AbiDecoded` value directly. You'll need to decode the `Fr[]` array yourself if you need typed results. + +#### `Contract.at()` is now synchronous and no longer calls `registerContract` + +The `Contract.at()` method (and generated contract `.at()` methods) is now synchronous and no longer automatically registers the contract with the wallet. This reduces unnecessary artifact storage and RPC calls. + +```diff +- const contract = await TokenContract.at(address, wallet); ++ const contract = TokenContract.at(address, wallet); +``` + +**Important:** You now need to explicitly call `registerContract` if you want the wallet to store the contract instance and artifact. This is only necessary when: + +- An app first registers a contract +- An app tries to update a contract's artifact + +If you need to register the contract, do so explicitly: + +```typescript +// Get the instance from deployment +const { contract, instance } = await TokenContract.deploy(wallet, ...args) + .send({ from: address }) + .wait(); + +// wallet already has it registered, since the deploy method does it by default +// to avoid it, set skipContractRegistration: true in the send options. + +// Register it with another wallet +await otherWallet.registerContract(instance, TokenContract.artifact); + +// Now you can use the contract +const otherContract = TokenContract.at(instance.address, otherWallet); +``` + +Publicly deployed contract instances can be retrieved via `node.getContract(address)`. Otherwise and if deployment parameters are known, an instance can be computed via the `getContractInstanceFromInstantiationParams` from `@aztec/aztec.js/contracts` + +#### `registerContract` signature simplified + +The `registerContract` method now takes a `ContractInstanceWithAddress` instead of a `Contract` object, and the `artifact` parameter is now optional. If the artifact is not provided, the wallet will attempt to look it up from its contract class storage. + +```diff +- await wallet.registerContract(contract); ++ await wallet.registerContract(instance, artifact?); +``` + +The method now only accepts: + +- `instance`: A `ContractInstanceWithAddress` object +- `artifact` (optional): A `ContractArtifact` object +- `secretKey` (optional): A secret key for privacy keys registration + +#### Return value of `getNotes` no longer contains a recipient and it contains some other additional info + +Return value of `getNotes` used to be defined as `Promise<UniqueNote[]>` and is now defined as `Promise<NoteDao[]>`. +`NoteDao` is mostly a super-set of `UniqueNote` but it doesn't contain a `recipient`. +Having the recipient in the return value has been redundant as the same outcome can be achieved by populating the `scopes` array in `NoteFilter` with the `recipient` value. + +#### Changes to `getPrivateEvents` + +The signature of `getPrivateEvents` has changed for two reasons: + +1. To align it with how other query methods that include filtering by block range work (for example, `AztecNode#getPublicLogs`) +2. To enrich the returned private events with metadata. + +```diff +getPrivateEvents<T>( +- contractAddress: AztecAddress, +- eventMetadata: EventMetadataDefinition, +- from: number, +- numBlocks: number, +- recipients: AztecAddress[], +- ): Promise<T[]>; ++ eventFilter: PrivateEventFilter, ++ ): Promise<PrivateEvent<T>[]>; +``` + +`PrivateEvent<T>` bundles together an ABI decoded event of type `T`, with `metadata` of type `InTx`: + +```ts +export type InBlock = { + l2BlockNumber: BlockNumber; + l2BlockHash: L2BlockHash; +}; + +export type InTx = InBlock & { + txHash: TxHash; +}; + +export type PrivateEvent<T> = { + event: T; + metadata: InTx; +}; +``` + +You will need to update any calls to `Wallet#getPrivateEvents` accordingly. See below for before/after comparison which conserves +semantics. + +Pay special attention to the fact that the old method expects a `numBlocks` parameter that instructs it to +return `numBlocks` blocks after `fromBlock`, whereas the new version expects an (exclusive) `toBlock` block number. + +Also note we're replacing _recipient_ terminology with _scope_. While underlying data types are equivalent (they are Aztec addresses), they have different semantics. Messages have a recipient who will be able to receive and process them. As a result of processing messages for a given recipient address, PXE might discover events. Those events are then said to be _in scope_ for that address. + +```diff +- const events = await context.client.getPrivateEvents(contractAddress, eventMetadata, 42, 10, [recipient]); +- doSomethingWithAnEvent(events[0]); + ++ const events = await context.client.getPrivateEvents(eventMetadata, { ++ contractAddress, ++ fromBlock: BlockNumber(42), ++ toBlock: BlockNumber(42 + 10), ++ scopes: [scope], ++ }); ++ doSomethingWithAnEvent(events[0].event); +``` + +Please refer to the wallet interface js-docs for further details. + +### [CLI] Command refactor + +The sandbox command has been renamed and remapped to "local network". We believe this conveys better what is actually being spun up when running it. + +**REMOVED/RENAMED**: + +- `aztec start --sandbox`: now `aztec start --local-network` + +### [Aztec.nr] - Contract API redesign + +In this release we decided to largely redesign our contract API. Most of the changes here are not a breaking change +(only renaming of original `#[internal]` to `#[only_self]` and `storage` now being available on the newly introduced +`self` struct are a breaking change). + +#### 1. Renaming of original #[internal] as #[only_self] + +We want for internal to mean the same as in Solidity where internal function can be called only from the same contract +and is also inlined (EVM JUMP opcode and not EVM CALL). The original implementation of our `#[internal]` macro also +results in the function being callable only from the same contract but it results in a different call (hence it doesn't +map to EVM JUMP). This is very confusing for people that know Solidity hence we are doing the rename. A true +`#[internal]` will be introduced in the future. + +To migrate your contracts simply rename all the occurrences of `#[internal]` with `#[only_self]` and update the imports: + +```diff +- use aztec::macros::functions::internal; ++ use aztec::macros::functions::only_self; +``` + +```diff +#[external("public")] +- #[internal] ++ #[only_self] +fn _deduct_public_balance(owner: AztecAddress, amount: u64) { + ... +} +``` + +#### 2. Introducing of new #[internal] + +Same as in Solidity internal functions are functions that are callable from inside the contract. Unlike #[only_self] +functions, internal functions are inlined (e.g. akin to EVM's JUMP and not EVM's CALL). + +Internal function can be called using the following API which leverages the new `self` struct (see change 3 below for +details): + +```noir +self.internal.my_internal_function(...) +``` + +Private internal functions can only be called from other private external or internal functions. +Public internal functions can only be called from other public external or internal functions. + +#### 3. Introducing `self` in contracts and a new call interface + +Aztec contracts now automatically inject a `self` parameter into every contract function, providing a unified interface +for accessing the contract's address, storage, calling of function and an execution context. + +##### What is `self`? + +`self` is an instance of `ContractSelf<Context, Storage>` that provides: + +- `self.address` - The contract's own address +- `self.storage` - Access to your contract's storage +- `self.context` - The execution context (private, public, or utility) +- `self.msg_sender()` - Get the address of the caller +- `self.emit(...)` - Emit events +- `self.call(...)` - Call an external function +- `self.view(...)` - Call an external function statically +- `self.enqueue(...)` - Enqueue a call to an external function +- `self.enqueue_view(...)` - Enqueue a call to an external function +- `self.enqueue_incognito(...)` - Enqueue a call to an external function but hides the `msg_sender` +- `self.enqueue_view_incognito(...)` - Enqueue a static call to an external function but hides the `msg_sender` +- `self.set_as_teardown(...)` - Enqueue a call to an external public function and sets the call as teardown +- `self.set_as_teardown_incognito(...)` - Enqueue a call to an external public function and sets the call as teardown + and hides the `msg_sender` +- `self.internal.my_internal_fn(...)` - Call an internal function + +`self` also provides you with convenience API to call and enqueue calls to external functions from within the same +contract (this is just a convenience API as `self.call(MyContract::at(self.address).my_external_fn(...))` would also +work): + +- `self.call_self.my_external_fn(...)` - Call external function from within the same contract +- `self.enqueue_self.my_public_external_fn(...)` +- `self.call_self_static.my_static_external_fn(...)` +- `self.enqueue_self_static.my_static_external_public_fn(...)` + +##### How it works + +The `#[external(...)]` macro automatically injects `self` into your function. When you write: + +```noir +#[external("private")] +fn transfer(amount: u128, recipient: AztecAddress) { + let sender = self.msg_sender().unwrap(); + self.storage.balances.at(sender).sub(amount); + self.storage.balances.at(recipient).add(amount); +} +``` + +The macro transforms it to initialize `self` with the context and storage before your code executes. + +##### Migration guide + +**Before:** Access context and storage as separate parameters + +```noir +#[external("private")] +fn old_transfer(amount: u128, recipient: AztecAddress) { + let storage = Storage::init(context); + let sender = context.msg_sender().unwrap(); + storage.balances.at(sender).sub(amount); +} +``` + +**After:** Use `self` to access everything + +```noir +#[external("private")] +fn new_transfer(amount: u128, recipient: AztecAddress) { + let sender = self.msg_sender().unwrap(); + self.storage.balances.at(sender).sub(amount); +} +``` + +##### Key changes + +1. **Storage and context access:** + +Storage and context are no longer injected into the function as standalone variables and instead you need to access them via `self`: + +```diff +- let balance = storage.balances.at(owner).read(); ++ let balance = self.storage.balances.at(owner).read(); +``` + +```diff +- context.push_nullifier(nullifier); ++ self.context.push_nullifier(nullifier); +``` + +Note that `context` is expected to be use only when needing to access a low-level API (like directly emitting a nullifier). + +2. **Getting caller address:** Use `self.msg_sender()` instead of `context.msg_sender()` + + ```diff + - let caller = context.msg_sender().unwrap(); + + let caller = self.msg_sender().unwrap(); + ``` + +3. **Getting contract address:** Use `self.address` instead of `context.this_address()` + + ```diff + - let this_contract = context.this_address(); + + let this_contract = self.address; + ``` + +4. **Emitting events:** + + In private functions: + + ```diff + - emit_event_in_private(event, context, recipient, delivery_mode); + + self.emit(event, recipient, delivery_mode); + ``` + + In public functions: + + ```diff + - emit_event_in_public(event, context); + + self.emit(event); + ``` + +5. **Calling functions:** + + In private functions: + + ```diff + - Token::at(stable_coin).mint_to_public(to, amount).call(&mut context); + + self.call(Token::at(stable_coin).mint_to_public(to, amount)); + ``` + +##### Example: Full contract migration + +**Before:** + +```noir +#[external("private")] +fn withdraw(amount: u128, recipient: AztecAddress) { + let storage = Storage::init(context); + let sender = context.msg_sender().unwrap(); + let token = storage.donation_token.get_note().get_address(); + + // ... withdrawal logic + + emit_event_in_private(Withdraw { withdrawer, amount }, context, withdrawer, MessageDelivery.UNCONSTRAINED_ONCHAIN); +} +``` + +**After:** + +```noir +#[external("private")] +fn withdraw(amount: u128, recipient: AztecAddress) { + let sender = self.msg_sender().unwrap(); + let token = self.storage.donation_token.get_note().get_address(); + + // ... withdrawal logic + + self.emit(Withdraw { withdrawer, amount }, withdrawer, MessageDelivery.UNCONSTRAINED_ONCHAIN); +} +``` + +#### No-longer allowing calling of non-view function statically via the old higher-level API + +We used to allow calling of non-view function statically as follows: + +```noir +MyContract::at(address).my_non_view_function(...).view(context); +MyContract::at(address).my_non_view_function(...).enqueue_view(context); +``` + +This is no-longer allowed and if you will want to call a function statically you will need to mark the function with +`#[view]`. + +### Phase checks + +Now private external functions check by default that no phase change from non revertible to revertible happens during the execution of the function or any of its nested calls. If you're developing a function +that handles phase change (you call `context.end_setup()` or call a function that you expect will change phase) you need to opt out of the phase check using the `#[nophasecheck]` macro. Also, now it's possible to know if you're in the revertible phase of the transaction at any point using `self.context.in_revertible_phase()`. + +### [`aztec` command] Moving functionality of `aztec-nargo` to `aztec` command + +`aztec-nargo` has been deprecated and all workflows should now migrate to the `aztec` command that fully replaces `aztec-nargo`: + +- **For contract initialization:** + + ```bash + aztec init + ``` + + (Behaves like `nargo init`, but defaults to a contract project.) + +- **For testing:** + + ```bash + aztec test + ``` + + (Starts the Aztec TXE and runs your tests.) + +- **For compiling contracts:** + ```bash + aztec compile + ``` + (Transpiles your contracts and generates verification keys.) + +## 3.0.0-devnet.4 + +## [aztec.js] Removal of barrel export + +`aztec.js` is now divided into granular exports, which improves loading performance in node.js and also makes the job of web bundlers easier: + +```diff +-import { AztecAddress, Fr, getContractInstanceFromInstantiationParams, type Wallet } from '@aztec/aztec.js'; ++import { AztecAddress } from '@aztec/aztec.js/addresses'; ++import { getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts'; ++import { Fr } from '@aztec/aztec.js/fields'; ++import type { Wallet } from '@aztec/aztec.js/wallet'; +``` + +Additionally, some general utilities reexported from `foundation` have been removed: + +```diff +-export { toBigIntBE } from '@aztec/foundation/bigint-buffer'; +-export { sha256, Grumpkin, Schnorr } from '@aztec/foundation/crypto'; +-export { makeFetch } from '@aztec/foundation/json-rpc/client'; +-export { retry, retryUntil } from '@aztec/foundation/retry'; +-export { to2Fields, toBigInt } from '@aztec/foundation/serialize'; +-export { sleep } from '@aztec/foundation/sleep'; +-export { elapsed } from '@aztec/foundation/timer'; +-export { type FieldsOf } from '@aztec/foundation/types'; +-export { fileURLToPath } from '@aztec/foundation/url'; +``` + +### `getSenders` renamed to `getAddressBook` in wallet interface + +An app could request "contacts" from the wallet, which don't necessarily have to be senders in the wallet's PXE. This method has been renamed to reflect that fact: + +```diff +-wallet.getSenders(); ++wallet.getAddressBook(); +``` + +### Removal of `proveTx` from `Wallet` interface + +Exposing this method on the interface opened the door for certain types of attacks, were an app could route proven transactions through malicious nodes (that stored them for later decryption, or collected user IPs for example). It also made transactions difficult to track for the wallet, since they could be sent without their knowledge at any time. This change also affects `ContractFunctionInteraction` and `DeployMethod`, which no longer expose a `prove()` method. + +### `msg_sender` is now an `Option<AztecAddress>` type. + +Because Aztec has native account abstraction, the very first function call of a tx has no `msg_sender`. (Recall, the first function call of an Aztec transaction is always a _private_ function call). + +Previously (before this change) we'd been silently setting this first `msg_sender` to be `AztecAddress::from_field(-1);`, and enforcing this value in the protocol's kernel circuits. Now we're passing explicitness to smart contract developers by wrapping `msg_sender` in an `Option` type. We'll explain the syntax shortly. + +We've also added a new protocol feature. Previously (before this change) whenever a public function call was enqueued by a private function (a so-called private->public call), the called public function (and hence the whole world) would be able to see `msg_sender`. For some use cases, visibility of `msg_sender` is important, to ensure the caller executed certain checks in private-land. For `#[only_self]` public functions, visibility of `msg_sender` is unavoidable (the caller of an `#[only_self]` function must be the same contract address by definition). But for _some_ use cases, a visible `msg_sender` is an unnecessary privacy leakage. +We therefore have added a feature where `msg_sender` can be optionally set to `Option<AztecAddress>::none()` for enqueued public function calls (aka private->public calls). We've been colloquially referring to this as "setting msg_sender to null". + +#### Aztec.nr diffs + +> Note: we'll be doing another pass at this aztec.nr syntax in the near future. + +Given the above, the syntax for accessing `msg_sender` in Aztec.nr is slightly different: + +For most public and private functions, to adjust to this change, you can make this change to your code: + +```diff +- let sender: AztecAddress = context.msg_sender(); ++ let sender: AztecAddress = context.msg_sender().unwrap(); +``` + +Recall that `Option::unwrap()` will throw if the Option is "none". + +Indeed, most smart contract functions will require access to a proper contract address (instead of a "null" value), in order to do bookkeeping (allocation of state variables against user addresses), and so in such cases throwing is sensible behaviour. + +If you want to output a useful error message when unwrapping fails, you can use `Option::expect`: + +```diff +- let sender: AztecAddress = context.msg_sender(); ++ let sender: AztecAddress = context.msg_sender().expect(f"Sender must not be none!"); +``` + +For a minority of functions, a "null" msg_sender will be acceptable: + +- A private entrypoint function. +- A public function which doesn't seek to do bookkeeping against `msg_sender`. + +Some apps might even want to _assert_ that the `msg_sender` is "null" to force their users into strong privacy practices: + +```rust +let sender: Option<AztecAddress> = context.msg_sender(); +assert(sender.is_none()); +``` + +##### Enqueueing public function calls + +###### Auto-generated contract interfaces + +When you use the `#[aztec]` macro, it will generate a noir contract interface for your contract, behind the scenes. + +This provides pretty syntax when you come to call functions of that contract. E.g.: + +```rust +Token::at(context.this_address())._increase_public_balance(to, amount).enqueue(&mut context); +``` + +In keeping with this new feature of being able to enqueue public function calls with a hidden `msg_sender`, there are some new methods that can be chained instead of `.enqueue(...)`: + +- `enqueue_incognito` -- akin to `enqueue`, but `msg_sender` is set "null". +- `enqueue_view_incognito` -- akin to `enqueue_view`, but `msg_sender` is "null". +- `set_as_teardown_incognito` -- akin to `set_as_teardown`, but `msg_sender` is "null". + +> The name "incognito" has been chosen to imply "msg_sender will not be visible to observers". + +These new functions enable the _calling_ contract to specify that it wants its address to not be visible to the called public function. This is worth re-iterating: it is the _caller's_ choice. A smart contract developer who uses these functions must be sure that the target public function will accept a "null" `msg_sender`. It would not be good (for example) if the called public function did `context.msg_sender().unwrap()`, because then a public function that is called via `enqueue_incognito` would _always fail_! Hopefully smart contract developers will write sufficient tests to catch such problems during development! + +###### Making lower-level public function calls from the private context + +This is discouraged vs using the auto-generated contract interfaces described directly above. + +If you do use any of these low-level methods of the `PrivateContext` in your contract: + +- `call_public_function` +- `static_call_public_function` +- `call_public_function_no_args` +- `static_call_public_function_no_args` +- `call_public_function_with_calldata_hash` +- `set_public_teardown_function` +- `set_public_teardown_function_with_calldata_hash` + +... there is a new `hide_msg_sender: bool` parameter that you will need to specify. + +#### Aztec.js diffs + +> Note: we'll be doing another pass at this aztec.js syntax in the near future. + +When lining up a new tx, the `FunctionCall` struct has been extended to include a `hide_msg_sender: bool` field. + +- `is_public & hide_msg_sender` -- will make a public call with `msg_sender` set to "null". +- `is_public & !hide_msg_sender` -- will make a public call with a visible `msg_sender`, as was the case before this new feature. +- `!is_public & hide_msg_sender` -- Incompatible flags. +- `!is_public & !hide_msg_sender` -- will make a private call with a visible `msg_sender` (noting that since it's a private function call, the `msg_sender` will only be visible to the called private function, but not to the rest of the world). + +## [cli-wallet] + +The `deploy-account` command now requires the address (or alias) of the account to deploy as an argument, not a parameter + +```diff ++aztec-wallet deploy-account main +-aztec-wallet deploy-account -f main +``` + +This release includes a major architectural change to the system. +The PXE JSON RPC Server has been removed, and PXE is now available only as a library to be used by wallets. + +## [Aztec node] + +Network config. The node now pulls default configuration from the public repository [AztecProtocol/networks](https://github.com/AztecProtocol/networks) after it applies the configuration it takes from the running environment and the configuration values baked into the source code. See associated [Design document](https://github.com/AztecProtocol/engineering-designs/blob/15415a62a7c8e901acb8e523625e91fc6f71dce4/docs/network-config/dd.md) + +## [Aztec.js] + +### Removing Aztec cheatcodes + +The Aztec cheatcodes class has been removed. Its functionality can be replaced by using the `getNotes(...)` function directly available on our `TestWallet`, along with the relevant functions available on the Aztec Node interface (note that the cheatcodes were generally just a thin wrapper around the Aztec Node interface). + +### CLI Wallet commands dropped from `aztec` command + +The following commands used to be exposed by both the `aztec` and the `aztec-wallet` commands: + +- import-test-accounts +- create-account +- deploy-account +- deploy +- send +- simulate +- profile +- bridge-fee-juice +- create-authwit +- authorize-action +- get-tx +- cancel-tx +- register-sender +- register-contract + +These were dropped from `aztec` and now are exposed only by the `cli-wallet` command exposed by the `@aztec/cli-wallet` package. + +### PXE commands dropped from `aztec` command + +The following commands were dropped from the `aztec` command: + +- `add-contract`: use can be replaced with `register-contract` on our `cli-wallet` +- `get-contract-data`: debug-only and not considered important enough to need a replacement +- `get-accounts`: debug-only and can be replaced by loading aliases from `cli-wallet` +- `get-account`: debug-only and can be replaced by loading aliases from `cli-wallet` +- `get-pxe-info`: debug-only and not considered important enough to need a replacement + +## [Aztec.nr] + +### Replacing #[private], #[public], #[utility] with #[external(...)] macro + +The original naming was not great in that it did not sufficiently communicate what the given macro did. +We decided to rename `#[private]` as `#[external("private")]`, `#[public]` as `#[external("public")]`, and `#[utility]` as `#[external("utility")]` to better communicate that these functions are externally callable and to specify their execution context. In this sense, `external` now means the exact same thing as in Solidity, i.e. a function that can be called from other contracts, and that can only be invoked via a contract call (i.e. the `CALL` opcode in the EVM, and a kernel call/AVM `CALL` opcode in Aztec). + +You have to do the following changes in your contracts: + +Update import: + +```diff +- use aztec::macros::functions::private; +- use aztec::macros::functions::public; +- use aztec::macros::functions::utility; ++ use aztec::macros::functions::external; +``` + +Update attributes of your functions: + +```diff +- #[private] ++ #[external("private")] + fn my_private_func() { +``` + +```diff +- #[public] ++ #[external("public")] + fn my_public_func() { +``` + +```diff +- #[utility] ++ #[external("utility")] + fn my_utility_func() { +``` + +### Dropping remote mutable references to public context + +`PrivateContext` generally needs to be passed as a mutable reference to functions because it does actually hold state +we're mutating. This is not the case for `PublicContext`, or `UtilityContext` - these are just marker objects that +indicate the current execution mode and make available the correct subset of the API. For this reason we have dropped +the mutable reference from the API. + +If you've passed the context as an argument to custom functions you will need to do the following migration (example +from our token contract): + +```diff +#[contract_library_method] +fn _finalize_transfer_to_private( + from_and_completer: AztecAddress, + amount: u128, + partial_note: PartialUintNote, +- context: &mut PublicContext, +- storage: Storage<&mut PublicContext>, ++ context: PublicContext, ++ storage: Storage<PublicContext>, +) { + ... +} +``` + +### Authwit Test Helper now takes `env` + +The `add_private_authwit_from_call_interface` test helper available in `test::helpers::authwit` now takes a `TestEnvironment` parameter, mirroring `add_public_authwit_from_call_interface`. This adds some unfortunate verbosity, but there are bigger plans to improve authwit usage in Noir tests in the near future. + +```diff +add_private_authwit_from_call_interface( ++ env, + on_behalf_of, + caller, + call_interface, +); +``` + +### Historical block renamed as anchor block + +A historical block term has been used as a term that denotes the block against which a private part of a tx has been executed. +This name is ambiguous and for this reason we've introduced "anchor block". +This naming change resulted in quite a few changes and if you've access private context's or utility context's block header you will need to update your code: + +```diff +- let header = context.get_block_header(); ++ let header = context.get_anchor_block_header(); +``` + +### Removed ValueNote utils + +The `value_note::utils` module has been removed because it was incorrect to have those in the value note package. + +For the increment function you can easily just insert the note: + +```diff +- use value_note::utils; +- utils::increment(storage.notes.at(owner), value, owner, sender); ++ let note = ValueNote::new(value, owner); ++ storage.notes.at(owner).insert(note).emit(&mut context, owner, MessageDelivery.CONSTRAINED_ONCHAIN); +``` + +### PrivateMutable: replace / initialize_or_replace behaviour change + +**Motivation:** +Updating a note used to require reading it first (via `get_note`, which nullifies and recreates it) and then calling `replace` — effectively proving a note twice. Now, `replace` accepts a callback that transforms the current note directly, and `initialize_or_replace` simply uses this updated `replace` internally. This reduces circuit cost while maintaining exactly one current note. + +**Key points:** + +1. `replace(self, new_note)` (old) → `replace(self, f)` (new), where `f` takes the current note and returns a transformed note. +2. `initialize_or_replace(self, note)` (old) → `initialize_or_replace(self, f)` (new), where `f` takes an `Option` with the current note, or `none` if uninitialized. +3. Previous note is automatically nullified before the new note is inserted. +4. `NoteEmission<Note>` still requires `.emit()` or `.discard()`. + +**Example Migration:** + +```diff +- let current_note = storage.my_var.get_note(); +- let new_note = f(current_note); +- storage.my_var.replace(new_note); ++ storage.my_var.replace(|current_note| f(current_note)); +``` + +```diff +- storage.my_var.initialize_or_replace(new_note); ++ storage.my_var.initialize_or_replace(|_| new_note); +``` + +This makes it easy and efficient to handle both initialization and current value mutation via `initialize_or_replace`, e.g. if implementing a note that simply counts how many times it has been read: + +```diff ++ storage.my_var.initialize_or_replace(|opt_current: Option<Note>| opt_current.unwrap_or(0 /* initial value */) + 1); +``` + +- The callback can be a closure (inline) or a named function. +- Any previous assumptions that replace simply inserts a new_note directly must be updated. + +### Unified oracles into single get_utility_context oracle + +The following oracles: + +1. get_contract_address, +2. get_block_number, +3. get_timestamp, +4. get_chain_id, +5. get_version + +were replaced with a single `get_utility_context` oracle whose return value contains all the values returned from the removed oracles. + +If you have used one of these removed oracles before, update the import, e.g.: + +```diff +- aztec::oracle::execution::get_chain_id; ++ aztec::oracle::execution::get_utility_context +``` + +and get the value out of the returned utility context: + +```diff +- let chain_id = get_chain_id(); ++ let chain_id = get_utility_context().chain_id(); +``` + +### Note emission API changes + +The note emission API has been significantly reworked to provide clearer semantics around message delivery guarantees. The key changes are: + +1. `encode_and_encrypt_note` has been removed in favor of calling `emit` directly with `MessageDelivery.CONSTRAINED_ONCHAIN` +2. `encode_and_encrypt_note_unconstrained` has been removed in favor of calling `emit` directly with `MessageDelivery.UNCONSTRAINED_ONCHAIN` +3. `encode_and_encrypt_note_and_emit_as_offchain_message` has been removed in favor of using `emit` with `MessageDelivery.UNCONSTRAINED_OFFCHAIN` +4. Note emission now takes a `delivery_mode` parameter with the following values: + - `CONSTRAINED_ONCHAIN`: For onchain delivery with cryptographic guarantees that recipients can discover and decrypt messages. Uses constrained encryption but is slower to prove. Best for critical messages that contracts need to verify. + - `UNCONSTRAINED_ONCHAIN`: For onchain delivery without encryption constraints. Faster proving but trusts the sender. Good when the sender is incentivized to perform encryption correctly (e.g. they are buying something and will only get it if the recipient sees the note). No guarantees that recipients will be able to find or decrypt messages. + - `UNCONSTRAINED_OFFCHAIN`: For offchain delivery (e.g. cloud storage) without constraints. Lowest cost since no onchain storage needed. Requires custom infrastructure for delivery. No guarantees that messages will be delivered or that recipients will ever find them. +5. The `context` object no longer needs to be passed to these functions + +Example migration: + +First you need to update imports in your contract: + +```diff +- aztec::messages::logs::note::encode_and_encrypt_note; +- aztec::messages::logs::note::encode_and_encrypt_note_unconstrained; +- aztec::messages::logs::note::encode_and_encrypt_note_and_emit_as_offchain_message; ++ aztec::messages::message_delivery::MessageDelivery; +``` + +Then update the emissions: + +```diff +- storage.balances.at(from).sub(from, amount).emit(encode_and_encrypt_note(&mut context, from)); ++ storage.balances.at(from).sub(from, amount).emit(&mut context, from, MessageDelivery.CONSTRAINED_ONCHAIN); +``` + +```diff +- storage.balances.at(from).add(from, change).emit(encode_and_encrypt_note_unconstrained(&mut context, from)); ++ storage.balances.at(from).add(from, change).emit(&mut context, from, MessageDelivery.UNCONSTRAINED_ONCHAIN); +``` + +```diff +- storage.balances.at(owner).insert(note).emit(encode_and_encrypt_note_and_emit_as_offchain_message(&mut context, context.msg_sender()); ++ storage.balances.at(owner).insert(note).emit(&mut context, context.msg_sender(), MessageDelivery.UNCONSTRAINED_OFFCHAIN); +``` + +## 2.0.2 + +## [Public functions] + +The L2 gas cost of the different AVM opcodes have been updated to reflect more realistic proving costs. Developers should review the L2 gas costs of executing public functions and reevaluate any hardcoded L2 gas limits. + +## [Aztec Tools] + +### Contract compilation now requires two steps + +The `aztec-nargo` command is now a direct pass-through to vanilla nargo, without any special compilation flags or postprocessing. Contract compilation for Aztec now requires two explicit steps: + +1. Compile your contracts with `aztec-nargo compile` +2. Run postprocessing with the new `aztec-postprocess-contract` command + +The postprocessing step includes: + +- Transpiling functions for the Aztec VM +- Generating verification keys for private functions +- Caching verification keys for faster subsequent compilations + +Update your build scripts accordingly: + +```diff +- aztec-nargo compile ++ aztec-nargo compile ++ aztec-postprocess-contract +``` + +If you're using the `aztec-up` installer, the `aztec-postprocess-contract` command will be automatically installed alongside `aztec-nargo`. + +## [Aztec.js] Mandatory `from` + +As we prepare for a bigger `Wallet` interface refactor and the upcoming `WalletSDK`, a new parameter has been added to contract interactions, which now should indicate _explicitly_ the address of the entrypoint (usually the account contract) that will be used to authenticate the request. This will be checked in runtime against the current `this.wallet.getAddress()` value, to ensure consistent behavior while the rest of the API is reworked. + +```diff +- await contract.methods.my_func(arg).send().wait(); ++ await contract.methods.my_func(arg).send({ from: account1Address }).wait(); +``` + +## [Aztec.nr] + +### `emit_event_in_public_log` function renamed as `emit_event_in_public` + +This change was done to make the naming consistent with the private counterpart (`emit_event_in_private`). + +### Private event emission API changes + +The private event emission API has been significantly reworked to provide clearer semantics around message delivery guarantees. The key changes are: + +1. `emit_event_in_private_log` has been renamed to `emit_event_in_private` and now takes a `delivery_mode` parameter instead of `constraints` +2. `emit_event_as_offchain_message` has been removed in favor of using `emit_event_in_private` with `MessageDelivery.UNCONSTRAINED_OFFCHAIN` +3. `PrivateLogContent` enum has been replaced with `MessageDelivery` enum with the following values: + - `CONSTRAINED_ONCHAIN`: For onchain delivery with cryptographic guarantees that recipients can discover and decrypt messages. Uses constrained encryption but is slower to prove. Best for critical messages that contracts need to verify. + - `UNCONSTRAINED_ONCHAIN`: For onchain delivery without encryption constraints. Faster proving but trusts the sender. Good when the sender is incentivized to perform encryption correctly (e.g. they are buying something and will only get it if the recipient sees the note). No guarantees that recipients will be able to find or decrypt messages. + - `UNCONSTRAINED_OFFCHAIN`: For offchain delivery (e.g. cloud storage) without constraints. Lowest cost since no onchain storage needed. Requires custom infrastructure for delivery. No guarantees that messages will be delivered or that recipients will ever find them. + +### Contract functions can no longer be `pub` or `pub(crate)` + +With the latest changes to `TestEnvironment`, making contract functions have public visibility is no longer required given the new `call_public` and `simulate_utility` functions. To avoid accidental direct invocation, and to reduce confusion with the autogenerated interfaces, we're forbidding them being public. + +```diff +- pub(crate) fn balance_of_private(account: AztecAddress) -> 128 { ++ fn balance_of_private(account: AztecAddress) -> 128 { +``` + +### Notes require you to manually implement or derive Packable + +We have decided to drop auto-derivation of `Packable` from the `#[note]` macro because we want to make the macros less magical. +With this change you will be forced to either apply `#[derive(Packable)` on your notes: + +```diff ++use aztec::protocol::traits::Packable; + ++#[derive(Packable)] +#[note] +pub struct UintNote { + owner: AztecAddress, + randomness: Field, + value: u128, +} +``` + +or to implement it manually yourself: + +```rust +impl Packable for UintNote { + let N: u32 = 3; + + fn pack(self) -> [Field; Self::N] { + [self.owner.to_field(), randomness, value as Field] + } + + fn unpack(fields: [Field; Self::N]) -> Self { + let owner = AztecAddress::from_field(fields[0]); + let randomness = fields[1]; + let value = fields[2] as u128; + UintNote { owner, randomness, value } + } +} +``` + +### Tagging sender now managed via oracle functions + +Now, instead of manually needing to pass a tagging sender as an argument to log emission functions (e.g. `encode_and_encrypt_note`, `encode_and_encrypt_note_unconstrained`, `emit_event_in_private_log`, ...) we automatically load the sender via the `get_sender_for_tags()` oracle. +This value is expected to be populated by account contracts that should call `set_sender_for_tags()` in their entry point functions. + +The changes you need to do in your contracts are quite straightforward. +You simply need to drop the `sender` arg from the callsites of the log emission functions. +E.g. note emission: + +```diff +storage.balances.at(from).sub(from, amount).emit(encode_and_encrypt_note( + &mut context, + from, +- tagging_sender, +)); +``` + +E.g. private event emission: + +```diff +emit_event_in_private_log( + Transfer { from, to, amount }, + &mut context, +- tagging_sender, + to, + PrivateLogContent.NO_CONSTRAINTS, +); +``` + +This change affected arguments `prepare_private_balance_increase` and `mint_to_private` functions on the `Token` contract. +Drop the `from` argument when calling these. + +Example in TypeScript test: + +```diff +- await token.methods.mint_to_private(fundedWallet.getAddress(), alice, mintAmount).send().wait(); ++ await token.methods.mint_to_private(alice, mintAmount).send().wait(); +``` + +Example when + +```diff +let token_out_partial_note = Token::at(token_out).prepare_private_balance_increase( + sender, +- tagging_sender +).call(&mut context); +``` + +### SharedMutable -> DelayedPublicMutable + +The `SharedMutable` state variable has been renamed to `DelayedPublicMutable`. It is a public mutable with a delay before state changes take effect. It can be read in private during the delay period. The name "shared" confuses developers who actually wish to work with so-called "shared private state". Also, we're working on a `DelayedPrivateMutable` which will have similar properties, except writes will be scheduled from private instead. With this new state variable in mind, the new name works nicely. + +## [TXE] - Testing Aztec Contracts using Noir + +### Full `TestEnvironment` API overhaul + +As part of a broader effort to make Noir tests that leverage TXE easier to use and reason about, large parts of it were changed or adapted, resulting in the API now being quite different. No functionality was lost, so it should be possible to migrate any older Noir test to use the new API. + +#### Network State Manipulation + +- `committed_timestamp` removed: this function did not work correctly +- `private_at_timestamp`: this function was not really meaningful: private contexts are built from block numbers, not timestamps +- `pending_block_number` was renamed to `next_block_number`. `pending_timestamp` was removed since it was confusing and not useful +- `committed_block_number` was renamed to `last_block_number` +- `advance_timestamp_to` and `advance_timestamp_by` were renamed to `set_next_block_timestamp` and `advance_next_block_timestamp_by` respectively +- `advance_block_to` was renamed to `mine_block_at`, which takes a timestamp instead of a target block number +- `advance_block_by` was renamed to `mine_block`, which now mines a single block + +#### Account Management + +- `create_account` was renamed to `create_light_account` +- `create_account_contract` was renamed to `create_contract_account` + +#### Contract Deployment + +- `deploy_self` removed: merged into `deploy` +- `deploy` now accepts both local and external contracts + +#### Contract Interactions + +The old way of calling contract functions is gone. Contract functions are now invoked via the `call_private`, `view_private`, `call_public`, `view_public` and `simulate_utility` `TestEnvironment` methods. These take a `CallInterface`, like their old counterparts, but now also take an explicit `from` parameter (for the `call` variants - this is left out of the `view` and `simulate` methods for simplicity). + +#### Raw Context Access + +The `private` and `public` methods are gone. Private, public and utility contexts can now be crated with the `private_context`, `public_context` and `utility_context` functions, all of which takes a callback function that is called with the corresponding context. This functions are expected to be defined in-line as lambdas, and contain the user-defined test logic. This helps delineate where contexts begin and end. Contexts automatically mine blocks on closing, when appropriate. + +#### Error-expecting Functions + +`assert_public_call_revert` and variants have been removed. Use `#[test(should_fail_with = "message")]` instead. + +#### Example Migration + +The following are two tests using the older version of `TestEnvironment`: + +```rust +#[test] +unconstrained fn initial_empty_value() { + let mut env = TestEnvironment::new(); + + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let admin = env.create_account(1); + + let initializer_call_interface = Auth::interface().constructor(admin); + + let auth_contract = + env.deploy_self("Auth").with_public_void_initializer(admin, initializer_call_interface); + let auth_contract_address = auth_contract.to_address(); + + env.impersonate(admin); + let authorized = Auth::at(auth_contract_address).get_authorized().view(&mut env.public()); + assert_eq(authorized, AztecAddress::from_field(0)); +} + +#[test] +unconstrained fn non_admin_cannot_set_authorized() { + let mut env = TestEnvironment::new(); + + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let admin = env.create_account(1); + let other = env.create_account(2); + + let initializer_call_interface = Auth::interface().constructor(admin); + + let auth_contract = + env.deploy_self("Auth").with_public_void_initializer(admin, initializer_call_interface); + let auth_contract_address = auth_contract.to_address(); + + env.impersonate(other); + env.assert_public_call_fails(Auth::at(auth_contract_address).set_authorized(to_authorize)); +} +``` + +These now look like this: + +```rust +#[test] +unconstrained fn authorized_initially_unset() { + let mut env = TestEnvironment::new(); + + let admin = env.create_light_account(); // Manual secret management gone + + let auth_contract_address = + env.deploy("Auth").with_public_initializer(admin, Auth::interface().constructor(admin)); // deploy_self replaced + let auth = Auth::at(auth_contract_address); + + assert_eq(env.view_public(auth.get_authorized()), AztecAddress::zero()); // .view_public() instead of .public() +} + +#[test(should_fail_with = "caller is not admin")] +unconstrained fn non_admin_cannot_set_unauthorized() { + let mut env = TestEnvironment::new(); + + let admin = env.create_light_account(); + let other = env.create_light_account(); + + let auth_contract_address = + env.deploy("Auth").with_public_initializer(admin, Auth::interface().constructor(admin)); // deploy_self replaced + let auth = Auth::at(auth_contract_address); + + env.call_public(other, auth.set_authorized(other)); // .call_public(), should_fail_with +} +``` + +## [Aztec.js] + +### Cheatcodes + +Cheatcodes where moved out of the `@aztec/aztec.js` package to `@aztec/ethereum` and `@aztec/aztec` packages. +While all of the cheatcodes can be imported from the `@aztec/aztec` package `EthCheatCodes` and `RollupCheatCodes` reside in `@aztec/ethereum` package and if you need only those importing only that package should result in a lighter build. + +### Note exports dropped from artifact + +Notes are no longer exported in the contract artifact. +Exporting notes was technical debt from when we needed to interpret notes in TypeScript. + +The following code will no longer work since `notes` is no longer available on the artifact: + +```rust +const valueNoteTypeId = StatefulTestContractArtifact.notes['ValueNote'].id; +``` + +## [core protocol, Aztec.nr, Aztec.js] Max block number property changed to be seconds based + +### `max_block_number` -> `include_by_timestamp` + +The transaction expiration mechanism has been updated to use seconds rather than number of blocks. +As part of this change, the transaction property `max_block_number` has been renamed to `include_by_timestamp`. + +This change significantly impacts the `SharedMutable` state variable in `Aztec.nr`, which now operates on a seconds instead of number of blocks. +If your contract uses `SharedMutable`, you'll need to: + +1. Update the `INITIAL_DELAY` numeric generic to use seconds instead of blocks +2. Modify any related logic to account for timestamp-based timing +3. Note that timestamps use `u64` values while block numbers use `u32` + +### Removed `prelude`, so your `dep::aztec::prelude::...` imports will need to be amended. + +Instead of importing common types from `dep::aztec::prelude...`, you'll now need to import them from their lower-level locations. +The Noir Language Server vscode extension is now capable of autocompleting imports: just type some of the import and press 'tab' when it pops up with the correct item, and the import will be inserted at the top of the file. + +As a quick reference, here are the paths to the types that were previously in the `prelude`. +So, for example, if you were previously using `dep::aztec::prelude::AztecAddress`, you'll need to replace it with `dep::aztec::protocol::address::AztecAddress`. +Apologies for any pain this brings. The reasoning is that these types were somewhat arbitrary, and it was unclear which types were worthy enough to be included here. + +```rust +use dep::aztec::{ + context::{PrivateCallInterface, PrivateContext, PublicContext, UtilityContext, ReturnsHash}, + note::{ + note_getter_options::NoteGetterOptions, + note_interface::{NoteHash, NoteType}, + note_viewer_options::NoteViewerOptions, + hinted_note::HintedNote, + }, + state_vars::{ + map::Map, private_immutable::PrivateImmutable, private_mutable::PrivateMutable, + private_set::PrivateSet, public_immutable::PublicImmutable, public_mutable::PublicMutable, + shared_mutable::SharedMutable, + }, +}; + +use dep::aztec::protocol::{ + abis::function_selector::FunctionSelector, + address::{AztecAddress, EthAddress}, + point::Point, + traits::{Deserialize, Serialize}, +}; +``` + +### `include_by_timestamp` is now mandatory + +Each transaction must now include a valid `include_by_timestamp` that satisfies the following conditions: + +- It must be greater than the historical block’s timestamp. +- The duration between the `include_by_timestamp` and the historical block’s timestamp must not exceed the maximum allowed (currently 24 hours). +- It must be greater than or equal to the timestamp of the block in which the transaction is included. + +The protocol circuits compute the `include_by_timestamp` for contract updates during each private function iteration. If a contract does not explicitly specify a value, the default will be the maximum allowed duration. This ensures that `include_by_timestamp` is never left unset. + +No client-side changes are required. However, please note that transactions now have a maximum lifespan of 24 hours and will be removed from the transaction pool once expired. + +## 0.88.0 + +## [Aztec.nr] Deprecation of the `authwit` library + +It is now included in `aztec-nr`, so imports must be updated: + +```diff +-dep::authwit::... ++dep::aztec::authwit... +``` + +and stale dependencies removed from `Nargo.toml` + +```diff +-authwit = { path = "../../../../aztec-nr/authwit" } +``` + +## 0.87.0 + +## [Aztec.js/TS libraries] + +We've bumped our minimum supported node version to v20, as v18 is now EOL. As a consequence, the deprecated type assertion syntax has been replaced with modern import attributes whenever contract artifact JSONs are loaded: + +```diff +-import ArtifactJson from '../artifacts/contract-Contract.json' assert { type: 'json' }; ++import ArtifactJson from '../artifacts/contract-Contract.json' with { type: 'json' }; +``` + +## [Aztec.js/PXE] `simulateUtility` return type + +`pxe.simulateUtility()` now returns a complex object (much like `.simulateTx()`) so extra information can be provided such as simulation timings. + +This information can be accessed setting the `includeMetadata` flag in `SimulateMethodOptions` to `true`, but not providing it (which is the default) will NOT change the behavior of the current code. + +```diff +-const result = await pxe.simulateUtility(...); ++const { meta, result } = await pxe.simulateUtility(...); + +const result = await Contract.methods.myFunction(...).simulate(); +const { result, meta} = await Contract.methods.myFunction(...).simulate({ includeMetadata: true }); + +``` + +## [Aztec.js] Removed mandatory simulation before proving in contract interfaces + +Previously, our autogenerated contract classes would perform a simulation when calling `.prove` or `.send` on them. This could potentially catch errors earlier, but took away control from the app/wallets on how to handle network interactions. Now this process has to be triggered manually, which means just proving an interaction (or proving and sending it to the network in one go via `.send`) is much faster. + +_WARNING:_ This means users can incurr in network fees if a transaction that would otherwise be invalid is sent without sanity checks. To ensure this, it is recommended to do: + +```diff ++await Contract.method.simulate(); +await Contract.method.send().wait(); +``` + +## 0.86.0 + +### [PXE] Removed PXE_L2_STARTING_BLOCK environment variable + +PXE now fast-syncs by skipping finalized blocks and never downloads all blocks, so there is no longer a need to specify a starting block. + +### [Aztec.nr] Logs and messages renaming + +The following renamings have taken place: + +- `encrypted_logs` to `messages`: this module now handles much more than just encrypted logs (including unconstrained message delivery, message encoding, etc.) +- `log_assembly_strategies` to `logs` +- `discovery` moved to `messages`: given that what is discovered are messages +- `default_aes128` removed + +Most contracts barely used these modules directly. The frequently used `encode_and_encrypt` function imports remain unchanged: + +```rust +use dep::aztec::messages::logs::note::encode_and_encrypt_note; +``` + +### [noir-contracts] Reference Noir contracts directory structure change + +`noir-projects/noir-contracts/contracts` directory became too cluttered so we grouped contracts into `account`, `app`, `docs`, `fees`, `libs`, `protocol` and `test` dirs. +If you import contract from the directory make sure to update the paths accordingly. +E.g. for a token contract: + +```diff +#[dependencies] +-token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v0.83.0", directory = "noir-projects/noir-contracts/contracts/src/token_contract" } ++token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v0.83.0", directory = "noir-projects/noir-contracts/contracts/app/src/token_contract" } +``` + +### [Aztec.nr] #[utility] contract functions + +Aztec contracts have three kinds of functions: `#[private]`, `#[public]` and what was sometimes called 'top-level unconstrained': an unmarked unconstrained function in the contract module. These are now called `[#utility]` functions, and must be explicitly marked as such: + +```diff ++ #[utility] + unconstrained fn balance_of_private(owner: AztecAddress) -> u128 { + storage.balances.at(owner).balance_of() + } +``` + +Utility functions are standalone unconstrained functions that cannot be called from private or public functions: they are meant to be called by _applications_ to perform auxiliary tasks: query contract state (e.g. a token balance), process messages received offchain, etc. + +All functions in a `contract` block must now be marked as one of either `#[private]`, `#[public]`, `#[utility]`, `#[contract_library_method]`, or `#[test]`. + +Additionally, the `UnconstrainedContext` type has been renamed to `UtilityContext`. This led us to rename the `unkonstrained` method on `TestEnvironment` to `utility`, so any tests using it also need updating: + +```diff +- SharedMutable::new(env.unkonstrained(), storage_slot) ++ SharedMutable::new(env.utility(), storage_slot) +``` + +### [AuthRegistry] function name change + +As part of the broader transition from "top-level unconstrained" to "utility" name (detailed in the note above), the `unconstrained_is_consumable` function in AuthRegistry has been renamed to `utility_is_consumable`. The function's signature and behavior remain unchanged - only the name has been updated to align with the new convention. If you're currently using this function, a simple rename in your code will suffice. + +## 0.83.0 + +### [aztec.js] AztecNode.getPrivateEvents API change + +The `getPrivateEvents` method signature has changed to require an address of a contract that emitted the event and use recipient addresses instead of viewing public keys: + +```diff +- const events = await wallet.getPrivateEvents<Transfer>(TokenContract.events.Transfer, 1, 1, [recipient.getCompleteAddress().publicKeys.masterIncomingViewingPublicKey()]); ++ const events = await wallet.getPrivateEvents<Transfer>(token.address, TokenContract.events.Transfer, 1, 1, [recipient.getAddress()]); +``` + +### [portal contracts] Versions and Non-following message boxes + +The version number is no longer hard-coded to be `1` across all deployments (it not depends on where it is deployed to and with what genesis and logic). +This means that if your portal were hard-coding `1` it will now fail when inserting into the `inbox` or consuming from the `outbox` because of a version mismatch. +Instead you can get the real version (which don't change for a deployment) by reading the `VERSION` on inbox and outbox, or using `getVersion()` on the rollup. + +New Deployments of the protocol do not preserve former state/across each other. +This means that after a new deployment, any "portal" following the registry would try to send messages into this empty rollup to non-existent contracts. +To solve, the portal should be linked to a specific deployment, e.g., a specific inbox. +This can be done by storing the inbox/outbox/version at the time of deployment or initialize and not update them. + +Both of these issues were in the token portal and the uniswap portal, so if you used them as a template it is very likely that you will also have it. + +## 0.82.0 + +### [aztec.js] AztecNode.findLeavesIndexes returns indexes with block metadata + +It's common that we need block metadata of a block in which leaves were inserted when querying indexes of these tree leaves. +For this reason we now return that information along with the indexes. +This allows us to reduce the number of individual AztecNode queries. + +Along with this change, `findNullifiersIndexesWithBlock` and `findBlockNumbersForIndexes` functions were removed as all their uses can now be replaced with the newly modified `findLeavesIndexes` function. + +### [aztec.js] AztecNode.getPublicDataTreeWitness renamed as AztecNode.getPublicDataWitness + +This change was done to have consistent naming across codebase. + +### [aztec.js] Wallet interface and Authwit management + +The `Wallet` interface in `aztec.js` is undergoing transformations, trying to be friendlier to wallet builders and reducing the surface of its API. This means `Wallet` no longer extends `PXE`, and instead just implements a subset of the methods of the former. This is NOT going to be its final form, but paves the way towards better interfaces and starts to clarify what the responsibilities of the wallet are: + +```typescript +/** + * The wallet interface. + */ +export type Wallet = AccountInterface & + Pick< + PXE, + // Simulation + | "simulateTx" + | "simulateUnconstrained" + | "profileTx" + // Sending + | "sendTx" + // Contract management (will probably be collapsed in the future to avoid instance and class versions) + | "getContractClassMetadata" + | "getContractMetadata" + | "registerContract" + | "registerContractClass" + // Likely to be removed + | "proveTx" + // Will probably be collapsed + | "getNodeInfo" + | "getPXEInfo" + // Fee info + | "getCurrentMinFees" + // Still undecided, kept for the time being + | "updateContract" + // Sender management + | "registerSender" + | "getSenders" + | "removeSender" + // Tx status + | "getTxReceipt" + // Events. Kept since events are going to be reworked and changes will come when that's done + | "getPrivateEvents" + | "getPublicEvents" + > & { + createAuthWit(intent: IntentInnerHash | IntentAction): Promise<AuthWitness>; + }; +``` + +As a side effect, a few debug only features have been removed + +```diff +// Obtain tx effects +const { txHash, debugInfo } = await contract.methods + .set_constant(value) + .send() +-- .wait({ interval: 0.1, debug: true }); +++ .wait({ interval: 0.1 }) + +-- // check that 1 note hash was created +-- expect(debugInfo!.noteHashes.length).toBe(1); +++ const txEffect = await aztecNode.getTxEffect(txHash); +++ const noteHashes = txEffect?.data.noteHashes; +++ // check that 1 note hash was created +++ expect(noteHashes?.length).toBe(1); + +// Wait for a tx to be proven +-- tx.wait({ timeout: 300, interval: 10, proven: true, provenTimeout: 3000 }))); +++ const receipt = await tx.wait({ timeout: 300, interval: 10 }); +++ await waitForProven(aztecNode, receipt, { provenTimeout: 3000 }); +``` + +Authwit management has changed, and PXE no longer stores them. This is unnecessary because now they can be externally provided to simulations and transactions, making sure no stale authorizations are kept inside PXE's db. + +```diff +const witness = await wallet.createAuthWit({ caller, action }); +--await callerWallet.addAuthWitness(witness); +--await action.send().wait(); +++await action.send({ authWitnesses: [witness] }).wait(); +``` + +Another side effect of this is that the interface of the `lookupValidity` method has changed, and now the authwitness has to be provided: + +```diff +const witness = await wallet.createAuthWit({ caller, action }); +--await callerWallet.addAuthWitness(witness); +--await wallet.lookupValidity(wallet.getAddress(), { caller, action }); +++await wallet.lookupValidity(wallet.getAddress(), { caller, action }, witness); +``` + +## 0.80.0 + +### [PXE] Concurrent contract function simulation disabled + +PXE is no longer be able to execute contract functions concurrently (e.g. by collecting calls to `simulateTx` and then using `await Promise.all`). They will instead be put in a job queue and executed sequentially in order of arrival. + +## 0.79.0 + +### [aztec.js] Changes to `BatchCall` and `BaseContractInteraction` + +The constructor arguments of `BatchCall` have been updated to improve usability. Previously, it accepted an array of `FunctionCall`, requiring users to manually set additional data such as `authwit` and `capsules`. Now, `BatchCall` takes an array of `BaseContractInteraction`, which encapsulates all necessary information. + +```diff +class BatchCall extends BaseContractInteraction { +- constructor(wallet: Wallet, protected calls: FunctionCall[]) { ++ constructor(wallet: Wallet, protected calls: BaseContractInteraction[]) { + ... + } +``` + +The `request` method of `BaseContractInteraction` now returns `ExecutionPayload`. This object includes all the necessary data to execute one or more functions. `BatchCall` invokes this method on all interactions to aggregate the required information. It is also used internally in simulations for fee estimation. + +Declaring a `BatchCall`: + +```diff +new BatchCall(wallet, [ +- await token.methods.transfer(alice, amount).request(), +- await token.methods.transfer_to_private(bob, amount).request(), ++ token.methods.transfer(alice, amount), ++ token.methods.transfer_to_private(bob, amount), +]) +``` + +## 0.77.0 + +### [aztec-nr] `TestEnvironment::block_number()` refactored + +The `block_number` function from `TestEnvironment` has been expanded upon with two extra functions, the first being `pending_block_number`, and the second being `committed_block_number`. `pending_block_number` now returns what `block_number` does. In other words, it returns the block number of the block we are currently building. `committed_block_number` returns the block number of the last committed block, i.e. the block number that gets used to execute the private part of transactions when your PXE is successfully synced to the tip of the chain. + +```diff ++ `TestEnvironment::pending_block_number()` ++ `TestEnvironment::committed_block_number()` +``` + +### [aztec-nr] `compute_nullifier_without_context` renamed + +The `compute_nullifier_without_context` function from `NoteHash` (ex `NoteInterface`) is now called `compute_nullifier_unconstrained`, and instead of taking storage slot, contract address and nonce it takes a note hash for nullification (same as `compute_note_hash`). This makes writing this +function simpler: + +```diff +- unconstrained fn compute_nullifier_without_context(self, storage_slot: Field, contract_address: AztecAddress, nonce: Field) -> Field { +- let note_hash_for_nullify = ...; ++ unconstrained fn compute_nullifier_unconstrained(self, note_hash_for_nullify: Field) -> Field { + ... + } +``` + +### `U128` type replaced with native `u128` + +The `U128` type has been replaced with the native `u128` type. This means that you can no longer use the `U128` type in your code. Instead, you should use the `u128` type. +Doing the changes is as straightforward as: + +```diff + #[public] + #[view] +- fn balance_of_public(owner: AztecAddress) -> U128 { ++ fn balance_of_public(owner: AztecAddress) -> u128 { + storage.public_balances.at(owner).read() + } +``` + +`UintNote` has also been updated to use the native `u128` type. + +### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifier` + +This function is no longer mandatory for contracts, and the `#[aztec]` macro no longer injects it. + +### [PXE] Removed `addNote` and `addNullifiedNote` + +These functions have been removed from PXE and the base `Wallet` interface. If you need to deliver a note manually because its creation is not being broadcast in an encrypted log, then create an unconstrained contract function to process it and simulate execution of it. The `aztec::discovery::private_logs::do_process_log` function can be used to perform note discovery and add to it to PXE. + +See an example of how to handle a `TransparentNote`: + +```rust + unconstrained fn deliver_transparent_note( + contract_address: AztecAddress, + amount: Field, + secret_hash: Field, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec<Field, MAX_NOTE_HASHES_PER_TX>, + first_nullifier_in_tx: Field, + recipient: AztecAddress, + ) { + // do_process_log expects a standard aztec-nr encoded note, which has the following shape: + // [ storage_slot, note_type_id, ...packed_note ] + let note = TransparentNote::new(amount, secret_hash); + let log_plaintext = BoundedVec::from_array(array_concat( + [ + MyContract::storage_layout().my_state_variable.slot, + TransparentNote::get_note_type_id(), + ], + note.pack(), + )); + + do_process_log( + contract_address, + log_plaintext, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, + _compute_note_hash_and_nullifier, + ); + } +``` + +The note is then processed by calling this function: + +```typescript +const txEffects = await wallet.getTxEffect(txHash); +await contract.methods + .deliver_transparent_note( + contract.address, + new Fr(amount), + secretHash, + txHash.hash, + toBoundedVec(txEffects!.data.noteHashes, MAX_NOTE_HASHES_PER_TX), + txEffects!.data.nullifiers[0], + wallet.getAddress(), + ) + .simulate(); +``` + +### Fee is mandatory + +All transactions must now pay fees. Previously, the default payment method was `NoFeePaymentMethod`; It has been changed to `FeeJuicePaymentMethod`, with the wallet owner as the fee payer. + +For example, the following code will still work: + +``` +await TokenContract.at(address, wallet).methods.transfer(recipient, 100n).send().wait(); +``` + +However, the wallet owner must have enough fee juice to cover the transaction fee. Otherwise, the transaction will be rejected. + +The 3 test accounts deployed in the sandbox are pre-funded with 10 ^ 22 fee juice, allowing them to send transactions right away. + +In addition to the native fee juice, users can pay the transaction fees using tokens that have a corresponding FPC contract. The sandbox now includes `BananaCoin` and `BananaFPC`. Users can use a funded test account to mint banana coin for a new account. The new account can then start sending transactions and pay fees with banana coin. + +```typescript +import { getDeployedTestAccountsWallets } from "@aztec/accounts/testing"; +import { + getDeployedBananaCoinAddress, + getDeployedBananaFPCAddress, +} from "@aztec/aztec"; + +// Fetch the funded test accounts. +const [fundedWallet] = await getDeployedTestAccountsWallets(pxe); + +// Create a new account. +const secret = Fr.random(); +const signingKey = GrumpkinScalar.random(); +const alice = await getSchnorrAccount(pxe, secret, signingKey); +const aliceWallet = await alice.getWallet(); +const aliceAddress = alice.getAddress(); + +// Deploy the new account using the pre-funded test account. +await alice.deploy({ deployWallet: fundedWallet }).wait(); + +// Mint banana coin for the new account. +const bananaCoinAddress = await getDeployedBananaCoinAddress(pxe); +const bananaCoin = await TokenContract.at(bananaCoinAddress, fundedWallet); +const mintAmount = 10n ** 20n; +await bananaCoin.methods + .mint_to_private(fundedWallet.getAddress(), aliceAddress, mintAmount) + .send() + .wait(); + +// Use the new account to send a tx and pay with banana coin. +const transferAmount = 100n; +const bananaFPCAddress = await getDeployedBananaFPCAddress(pxe); +const paymentMethod = new PrivateFeePaymentMethod( + bananaFPCAddress, + aliceWallet, +); +const receipt = await bananaCoin + .withWallet(aliceWallet) + .methods.transfer(recipient, transferAmount) + .send({ fee: { paymentMethod } }) + .wait(); +const transactionFee = receipt.transactionFee!; + +// Check the new account's balance. +const aliceBalance = await bananaCoin.methods + .balance_of_private(aliceAddress) + .simulate(); +expect(aliceBalance).toEqual(mintAmount - transferAmount - transactionFee); +``` + +### The tree of protocol contract addresses is now an indexed tree + +This is to allow for non-membership proofs for non-protocol contract addresses. As before, the canonical protocol contract addresses point to the index of the leaf of the 'real' computed protocol address. + +For example, the canonical `DEPLOYER_CONTRACT_ADDRESS` is a constant `= 2`. This is used in the kernels as the `contract_address`. We calculate the `computed_address` (currently `0x1665c5fbc1e58ba19c82f64c0402d29e8bbf94b1fde1a056280d081c15b0dac1`) and check that this value exists in the indexed tree at index `2`. This check already existed and ensures that the call cannot do 'special' protocol contract things unless it is a real protocol contract. + +The new check an indexed tree allows is non-membership of addresses of non protocol contracts. This ensures that if a call is from a protocol contract, it must use the canonical address. For example, before this check a call could be from the deployer contract and use `0x1665c5fbc1e58ba19c82f64c0402d29e8bbf94b1fde1a056280d081c15b0dac1` as the `contract_address`, but be incorrectly treated as a 'normal' call. + +```diff +- let computed_protocol_contract_tree_root = if is_protocol_contract { +- 0 +- } else { +- root_from_sibling_path( +- computed_address.to_field(), +- protocol_contract_index, +- private_call_data.protocol_contract_sibling_path, +- ) +- }; + ++ conditionally_assert_check_membership( ++ computed_address.to_field(), ++ is_protocol_contract, ++ private_call_data.protocol_contract_leaf, ++ private_call_data.protocol_contract_membership_witness, ++ protocol_contract_tree_root, ++ ); +``` + +### [Aztec.nr] Changes to note interfaces and note macros + +In this releases we decided to do a large refactor of notes which resulted in the following changes: + +1. We removed `NoteHeader` and we've introduced a `HintedNote` struct that contains a note and the information originally stored in the `NoteHeader`. +2. We removed the `pack_content` and `unpack_content` functions from the `NoteInterface`and made notes implement the standard `Packable` trait. +3. We renamed the `NullifiableNote` trait to `NoteHash` and we've moved the `compute_note_hash` function to this trait from the `NoteInterface` trait. +4. We renamed `NoteInterface` trait as `NoteType` and `get_note_type_id` function as `get_id`. +5. The `#[note]` and `#[partial_note]` macros now generate both the `NoteType` and `NoteHash` traits. +6. `#[custom_note_interface]` macro has been renamed to `#[custom_note]` and it now implements the `NoteInterface` trait. + +This led us to do the following changes to the interfaces: + +```diff +-pub trait NoteInterface<let N: u32> { ++pub trait NoteType { + fn get_id() -> Field; +- fn pack_content(self) -> [Field; N]; +- fn unpack_content(fields: [Field; N]) -> Self; +- fn get_header(self) -> NoteHeader; +- fn set_header(&mut self, header: NoteHeader) -> (); +- fn compute_note_hash(self) -> Field; +} + +pub trait NoteHash { ++ fn compute_note_hash(self, storage_slot: Field) -> Field; + + fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field; + +- unconstrained fn compute_nullifier_without_context(self) -> Field; ++ unconstrained fn fn compute_nullifier_without_context(self, storage_slot: Field, contract_address: AztecAddress, note_nonce: Field) -> Field; +} +``` + +If you are using `#[note]` or `#[partial_note(...)]` macros you will need to delete the implementations of the `NullifiableNote` (now `NoteHash`) trait as it now gets auto-generated. +Your note will also need to have an `owner` (a note struct field called owner) as its used in the auto-generated nullifier functions. + +If you need a custom implementation of the `NoteHash` interface use the `#[custom_note]` macro. + +If you used `#[note_custom_interface]` macro before you will need to update your notes by using the `#[custom_note]` macro and implementing the `compute_note_hash` function. +If you have no need for a custom implementation of the `compute_note_hash` function copy the default one: + +``` +fn compute_note_hash(self, storage_slot: Field) -> Field { + let inputs = aztec::protocol::utils::arrays::array_concat(self.pack(), [storage_slot]); + aztec::protocol::hash::poseidon2_hash_with_separator(inputs, aztec::protocol::constants::DOM_SEP__NOTE_HASH) +} +``` + +If you need to keep the custom implementation of the packing functionality, manually implement the `Packable` trait: + +```diff ++ use dep::aztec::protocol::traits::Packable; + ++impl Packable<N> for YourNote { ++ fn pack(self) -> [Field; N] { ++ ... ++ } ++ ++ fn unpack(fields: [Field; N]) -> Self { ++ ... ++ } ++} +``` + +If you don't provide a custom implementation of the `Packable` trait, a default one will be generated. + +### [Aztec.nr] Changes to state variables + +Since we've removed `NoteHeader` from notes we no longer need to modify the header in the notes when working with state variables. +This means that we no longer need to be passing a mutable note reference which led to the following changes in the API. + +#### PrivateImmutable + +For `PrivateImmutable` the changes are fairly straightforward. +Instead of passing in a mutable reference `&mut note` just pass in `note`. + +```diff +impl<Note> PrivateImmutable<Note, &mut PrivateContext> { +- pub fn initialize<let N: u32>(self, note: &mut Note) -> NoteEmission<Note> ++ pub fn initialize<let N: u32>(self, note: Note) -> NoteEmission<Note> + where + Note: NoteInterface<N> + NullifiableNote, + { + ... + } +} +``` + +#### PrivateSet + +For `PrivateSet` the changes are a bit more involved than the changes in `PrivateImmutable`. +Instead of passing in a mutable reference `&mut note` to the `insert` function just pass in `note`. +The `remove` function now takes in a `HintedNote<Note>` instead of a `Note` and the `get_notes` function +now returns a vector `HintedNote`s instead of a vector `Note`s. +Note getters now generally return `HintedNote`s so getting a hold of the `HintedNote` for removal should be straightforward. + +```diff +impl<Note, let N: u32> PrivateSet<Note, &mut PrivateContext> +where + Note: NoteInterface<N> + NullifiableNote + Eq, +{ +- pub fn insert(self, note: &mut Note) -> NoteEmission<Note> { ++ pub fn insert(self, note: Note) -> NoteEmission<Note> { + ... + } + +- pub fn remove(self, note: Note) { ++ pub fn remove(self, hinted_note: HintedNote<Note>) { + ... + } + + pub fn get_notes<PREPROCESSOR_ARGS, FILTER_ARGS>( + self, + options: NoteGetterOptions<Note, N, PREPROCESSOR_ARGS, FILTER_ARGS>, +- ) -> BoundedVec<Note, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL> { ++ ) -> BoundedVec<HintedNote<Note>, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL> { + ... + } +} + +- impl<Note, let N: u32> PrivateSet<Note, &mut PublicContext> +- where +- Note: NoteInterface<N> + NullifiableNote, +- { +- pub fn insert_from_public(self, note: &mut Note) { +- create_note_hash_from_public(self.context, self.storage_slot, note); +- } +- } +``` + +#### PrivateMutable + +For `PrivateMutable` the changes are similar to the changes in `PrivateImmutable`. + +```diff +impl<Note, let N: u32> PrivateMutable<Note, &mut PrivateContext> +where + Note: NoteInterface<N> + NullifiableNote, +{ +- pub fn initialize(self, note: &mut Note) -> NoteEmission<Note> { ++ pub fn initialize(self, note: Note) -> NoteEmission<Note> { + ... + } + +- pub fn replace(self, new_note: &mut Note) -> NoteEmission<Note> { ++ pub fn replace(self, new_note: Note) -> NoteEmission<Note> { + ... + } + +- pub fn initialize_or_replace(self, note: &mut Note) -> NoteEmission<Note> { ++ pub fn initialize_or_replace(self, note: Note) -> NoteEmission<Note> { + ... + } +} +``` + +## 0.75.0 + +### Changes to `TokenBridge` interface + +`get_token` and `get_portal_address` functions got merged into a single `get_config` function that returns a struct containing both the token and portal addresses. + +### [Aztec.nr] `SharedMutable` can store size of packed length larger than 1 + +`SharedMutable` has been modified such that now it can store type `T` which packs to a length larger than 1. +This is a breaking change because now `SharedMutable` requires `T` to implement `Packable` trait instead of `ToField` and `FromField` traits. + +To implement the `Packable` trait for your type you can use the derive macro: + +```diff ++ use std::meta::derive; + ++ #[derive(Packable)] +pub struct YourType { + ... +} +``` + +### [Aztec.nr] Introduction of `WithHash<T>` + +`WithHash<T>` is a struct that allows for efficient reading of value `T` from public storage in private. +This is achieved by storing the value with its hash, then obtaining the values via an oracle and verifying them against the hash. +This results in in a fewer tree inclusion proofs for values `T` that are packed into more than a single field. + +`WithHash<T>` is leveraged by state variables like `PublicImmutable`. +This is a breaking change because now we require values stored in `PublicImmutable` and `SharedMutable` to implement the `Eq` trait. + +To implement the `Eq` trait you can use the `#[derive(Eq)]` macro: + +```diff ++ use std::meta::derive; + ++ #[derive(Eq)] +pub struct YourType { + ... +} +``` + +## 0.73.0 + +### [Token, FPC] Moving fee-related complexity from the Token to the FPC + +There was a complexity leak of fee-related functionality in the token contract. +We've came up with a way how to achieve the same objective with the general functionality of the Token contract. +This lead to the removal of `setup_refund` and `complete_refund` functions from the Token contract and addition of `complete_refund` function to the FPC. + +### [Aztec.nr] Improved storage slot allocation + +State variables are no longer assumed to be generic over a type that implements the `Serialize` trait: instead, they must implement the `Storage` trait with an `N` value equal to the number of slots they need to reserve. + +For the vast majority of state variables, this simply means binding the serialization length to this trait: + +```diff ++ impl<T, let N: u32> Storage<N> for MyStateVar<T> where T: Serialize<N> { }; +``` + +### [Aztec.nr] Introduction of `Packable` trait + +We have introduced a `Packable` trait that allows types to be serialized and deserialized with a focus on minimizing the size of the resulting Field array. +This is in contrast to the `Serialize` and `Deserialize` traits, which follows Noir's intrinsic serialization format. +This is a breaking change because we now require `Packable` trait implementation for any type that is to be stored in contract storage. + +Example implementation of Packable trait for `U128` type from `noir::std`: + +```rust +use crate::traits::{Packable, ToField}; + +let U128_PACKED_LEN: u32 = 1; + +impl Packable<U128_PACKED_LEN> for U128 { + fn pack(self) -> [Field; U128_PACKED_LEN] { + [self.to_field()] + } + + fn unpack(fields: [Field; U128_PACKED_LEN]) -> Self { + U128::from_integer(fields[0]) + } +} +``` + +### Logs for notes, partial notes, and events have been refactored. + +We're preparing to make log assembly more customisable. These paths have changed. + +```diff +- use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note, ++ use dep::aztec::messages::logs::note::encode_and_encrypt_note, +``` + +And similar paths for `encode_and_encrypt_note_unconstrained`, and for events and partial notes. + +The way in which logs are assembled in this "default_aes128" strategy is has also changed. I repeat: **Encrypted log layouts have changed**. The corresponding typescript for note discovery has also been changed, but if you've rolled your own functions for parsing and decrypting logs, those will be broken by this change. + +### `NoteInferface` and `EventInterface` no-longer have a `to_be_bytes` method. + +You can remove this method from any custom notes or events that you've implemented. + +### [Aztec.nr] Packing notes resulting in changes in `NoteInterface` + +Note interface implementation generated by our macros now packs note content instead of serializing it +With this change notes are being less costly DA-wise to emit when some of the note struct members implements the `Packable` trait (this is typically the `UintNote` which represents `value` as `U128` that gets serialized as 2 fields but packed as 1). +This results in the following changes in the `NoteInterface`: + +```diff +pub trait NoteInterface<let N: u32> { +- fn serialize_content(self) -> [Field; N]; ++ fn pack_content(self) -> [Field; N]; + +- fn deserialize_content(fields: [Field; N]) -> Self; ++ fn unpack_content(fields: [Field; N]) -> Self; + + fn get_header(self) -> NoteHeader; + fn set_header(&mut self, header: NoteHeader) -> (); + fn get_note_type_id() -> Field; + fn compute_note_hash(self) -> Field; +} +``` + +### [PXE] Cleanup of Contract and ContractClass information getters + +```diff +- pxe.isContractInitialized +- pxe.getContractInstance +- pxe.isContractPubliclyDeployed ++ pxe.getContractMetadata +``` + +have been merged into getContractMetadata + +```diff +- pxe.getContractClass +- pxe.isContractClassPubliclyRegistered +- pxe.getContractArtifact ++ pxe.getContractClassMetadata +``` + +These functions have been merged into `pxe.getContractMetadata` and `pxe.getContractClassMetadata`. + +## 0.72.0 + +### Some functions in `aztec.js` and `@aztec/accounts` are now async + +In our efforts to make libraries more browser-friendly and providing with more bundling options for `bb.js` (like a non top-level-await version), some functions are being made async, in particular those that access our cryptographic functions. + +```diff +- AztecAddress.random(); ++ await AztecAddress.random(); + +- getSchnorrAccount(); ++ await getSchnorrAccount(); +``` + +### Public logs replace unencrypted logs + +Any log emitted from public is now known as a public log, rather than an unencrypted log. This means methods relating to these logs have been renamed e.g. in the pxe, archiver, txe: + +```diff +- getUnencryptedLogs(filter: LogFilter): Promise<GetUnencryptedLogsResponse> +- getUnencryptedEvents<T>(eventMetadata: EventMetadataDefinition, from: number, limit: number): Promise<T[]> ++ getPublicLogs(filter: LogFilter): Promise<GetPublicLogsResponse> ++ getPublicEvents<T>(eventMetadata: EventMetadataDefinition, from: number, limit: number): Promise<T[]> +``` + +The context method in aztec.nr is now: + +```diff +- context.emit_unencrypted_log(log) ++ context.emit_public_log(log) +``` + +These logs were treated as bytes in the node and as hashes in the protocol circuits. Now, public logs are treated as fields everywhere: + +```diff +- unencryptedLogs: UnencryptedTxL2Logs +- unencrypted_logs_hashes: [ScopedLogHash; MAX_UNENCRYPTED_LOGS_PER_TX] ++ publicLogs: PublicLog[] ++ public_logs: [PublicLog; MAX_PUBLIC_LOGS_PER_TX] +``` + +A `PublicLog` contains the log (as an array of fields) and the app address. + +This PR also renamed encrypted events to private events: + +```diff +- getEncryptedEvents<T>(eventMetadata: EventMetadataDefinition, from: number, limit: number, vpks: Point[]): Promise<T[]> ++ getPrivateEvents<T>(eventMetadata: EventMetadataDefinition, from: number, limit: number, vpks: Point[]): Promise<T[]> +``` + +## 0.70.0 + +### [Aztec.nr] Removal of `getSiblingPath` oracle + +Use `getMembershipWitness` oracle instead that returns both the sibling path and index. + +## 0.68.0 + +### [archiver, node, pxe] Remove contract artifacts in node and archiver and store function names instead + +Contract artifacts were only in the archiver for debugging purposes. Instead function names are now (optionally) emitted +when registering contract classes + +Function changes in the Node interface and Contract Data source interface: + +```diff +- addContractArtifact(address: AztecAddress, artifact: ContractArtifact): Promise<void>; ++ registerContractFunctionNames(address: AztecAddress, names: Record<string, string>): Promise<void>; +``` + +So now the PXE registers this when calling `registerContract()` + +``` +await this.node.registerContractFunctionNames(instance.address, functionNames); +``` + +Function changes in the Archiver + +```diff +- addContractArtifact(address: AztecAddress, artifact: ContractArtifact) +- getContractArtifact(address: AztecAddress) ++ registerContractFunctionNames(address: AztecAddress, names: Record<string, string>): Promise<void> +``` + +### [fees, fpc] Changes in setting up FPC as fee payer on AztecJS and method names in FPC + +On AztecJS, setting up `PrivateFeePaymentMethod` and `PublicFeePaymentMethod` are now the same. The don't need to specify a sequencer address or which coin to pay in. The coins are set up in the FPC contract! + +```diff +- paymentMethod: new PrivateFeePaymentMethod(bananaCoin.address,bananaFPC.address,aliceWallet,sequencerAddress), ++ paymentMethod: new PrivateFeePaymentMethod(bananaFPC.address, aliceWallet), + +- paymentMethod: new PublicFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), ++ paymentMethod: new PublicFeePaymentMethod(bananaFPC.address, aliceWallet), +``` + +Changes in `FeePaymentMethod` class in AztecJS + +```diff +- getAsset(): AztecAddress; ++ getAsset(): Promise<AztecAddress>; +``` + +Changes in the token contract: +FPC specific methods, `setup_refund()` and `complete_refund()` have minor args rename. + +Changes in FPC contract: +Rename of args in all of FPC functions as FPC now stores the accepted token address and admin and making it clearer the amounts are corresponding to the accepted token and not fee juice. +Also created a public function `pull_funds()` for admin to clawback any money in the FPC + +Expect more changes in FPC in the coming releases! + +### Name change from `contact` to `sender` in PXE API + +`contact` has been deemed confusing because the name is too similar to `contract`. +For this reason we've decided to rename it: + +```diff +- await pxe.registerContact(address); ++ await pxe.registerSender(address); +- await pxe.getContacts(); ++ await pxe.getSenders(); +- await pxe.removeContact(address); ++ await pxe.removeSender(address); +``` + +## 0.67.1 + +### Noir contracts package no longer exposes artifacts as default export + +To reduce loading times, the package `@aztec/noir-contracts.js` no longer exposes all artifacts as its default export. Instead, it exposes a `ContractNames` variable with the list of all contract names available. To import a given artifact, use the corresponding export, such as `@aztec/noir-contracts.js/FPC`. + +### Blobs + +We now publish the majority of DA in L1 blobs rather than calldata, with only contract class logs remaining as calldata. This replaces all code that touched the `txsEffectsHash`. +In the rollup circuits, instead of hashing each child circuit's `txsEffectsHash` to form a tree, we track tx effects by absorbing them into a sponge for blob data (hence the name: `spongeBlob`). This sponge is treated like the state trees in that we check each rollup circuit 'follows' the next: + +```diff +- let txs_effects_hash = sha256_to_field(left.txs_effects_hash, right.txs_effects_hash); ++ assert(left.end_sponge_blob.eq(right.start_sponge_blob)); ++ let start_sponge_blob = left.start_sponge_blob; ++ let end_sponge_blob = right.end_sponge_blob; +``` + +This sponge is used in the block root circuit to confirm that an injected array of all `txEffects` does match those rolled up so far in the `spongeBlob`. Then, the `txEffects` array is used to construct and prove opening of the polynomial representing the blob commitment on L1 (this is done efficiently thanks to the Barycentric formula). +On L1, we publish the array as a blob and verify the above proof of opening. This confirms that the tx effects in the rollup circuit match the data in the blob: + +```diff +- bytes32 txsEffectsHash = TxsDecoder.decode(_body); ++ bytes32 blobHash = _validateBlob(blobInput); +``` + +Where `blobInput` contains the proof of opening and evaluation calculated in the block root rollup circuit. It is then stored and used as a public input to verifying the epoch proof. + +## 0.67.0 + +### L2 Gas limit of 6M enforced for public portion of TX + +A 12M limit was previously enforced per-enqueued-public-call. The protocol now enforces a stricter limit that the entire public portion of a transaction consumes at most 6,000,000 L2 gas. + +### [aztec.nr] Renamed `Header` and associated helpers + +The `Header` struct has been renamed to `BlockHeader`, and the `get_header()` family of functions have been similarly renamed to `get_block_header()`. + +```diff +- let header = context.get_header_at(block_number); ++ let header = context.get_block_header_at(block_number); +``` + +### Outgoing Events removed + +Previously, every event which was emitted included: + +- Incoming Header (to convey the app contract address to the recipient) +- Incoming Ciphertext (to convey the note contents to the recipient) +- Outgoing Header (served as a backup, to convey the app contract address to the "outgoing viewer" - most likely the sender) +- Outgoing Ciphertext (served as a backup, encrypting the symmetric key of the incoming ciphertext to the "outgoing viewer" - most likely the sender) + +The latter two have been removed from the `.emit()` functions, so now only an Incoming Header and Incoming Ciphertext will be emitted. + +The interface for emitting a note has therefore changed, slightly. No more ovpk's need to be derived and passed into `.emit()` functions. + +```diff +- nfts.at(to).insert(&mut new_note).emit(encode_and_encrypt_note(&mut context, from_ovpk_m, to, from)); ++ nfts.at(to).insert(&mut new_note).emit(encode_and_encrypt_note(&mut context, to, from)); +``` + +The `getOutgoingNotes` function is removed from the PXE interface. + +Some aztec.nr library methods' arguments are simplified to remove an `outgoing_viewer` parameter. E.g. `ValueNote::increment`, `ValueNote::decrement`, `ValueNote::decrement_by_at_most`, `EasyPrivateUint::add`, `EasyPrivateUint::sub`. + +Further changes are planned, so that: + +- Outgoing ciphertexts (or any kind of abstract ciphertext) can be emitted by a contract, and on the other side discovered and then processed by the contract. +- Headers will be removed, due to the new tagging scheme. + +## 0.66 + +### DEBUG env var is removed + +The `DEBUG` variable is no longer used. Use `LOG_LEVEL` with one of `silent`, `fatal`, `error`, `warn`, `info`, `verbose`, `debug`, or `trace`. To tweak log levels per module, add a list of module prefixes with their overridden level. For example, LOG_LEVEL="info; verbose: aztec:sequencer, aztec:archiver; debug: aztec:kv-store" sets `info` as the default log level, `verbose` for the sequencer and archiver, and `debug` for the kv-store. Module name match is done by prefix. + +### `tty` resolve fallback required for browser bundling + +When bundling `aztec.js` for web, the `tty` package now needs to be specified as an empty fallback: + +```diff +resolve: { + plugins: [new ResolveTypeScriptPlugin()], + alias: { './node/index.js': false }, + fallback: { + crypto: false, + os: false, + fs: false, + path: false, + url: false, ++ tty: false, + worker_threads: false, + buffer: require.resolve('buffer/'), + util: require.resolve('util/'), + stream: require.resolve('stream-browserify'), + }, +}, +``` + +## 0.65 + +### [aztec.nr] Removed SharedImmutable + +The `SharedImmutable` state variable has been removed, since it was essentially the exact same as `PublicImmutable`, which now contains functions for reading from private: + +```diff +- foo: SharedImmutable<T, Context>. ++ foo: PublicImmutable<T, Context>. +``` + +### [aztec.nr] SharedImmutable renamings + +`SharedImmutable::read_private` and `SharedImmutable::read_public` were renamed to simply `read`, since only one of these versions is ever available depending on the current context. + +```diff +// In private +- let value = storage.my_var.read_private(); ++ let value = storage.my_var.read(); + +// In public +- let value = storage.my_var.read_public(); ++ let value = storage.my_var.read(); +``` + +### [aztec.nr] SharedMutable renamings + +`SharedMutable` getters (`get_current_value_in_public`, etc.) were renamed by dropping the `_in<public|private|unconstrained>` suffix, since only one of these versions is ever available depending on the current context. + +```diff +// In private +- let value = storage.my_var.get_current_value_in_private(); ++ let value = storage.my_var.get_current_value(); + +// In public +- let value = storage.my_var.get_current_value_in_public(); ++ let value = storage.my_var.get_current_value(); +``` + +### [aztec.js] Random addresses are now valid + +The `AztecAddress.random()` function now returns valid addresses, i.e. addresses that can receive encrypted messages and therefore have notes be sent to them. `AztecAddress.isValid()` was also added to check for validity of an address. + +## 0.63.0 + +### [PXE] Note tagging and discovery + +PXE's trial decryption of notes has been replaced in favor of a tagging and discovery approach. It is much more efficient and should scale a lot better as the network size increases, since +notes can now be discovered on-demand. For the time being, this means that accounts residing _on different PXE instances_ should add senders to their contact list, so notes can be discovered +(accounts created on the same PXE instance will be added as senders for each other by default) + +```diff ++pxe.registerContact(senderAddress) +``` + +The note discovery process is triggered automatically whenever a contract invokes the `get_notes` oracle, meaning no contract changes are expected. Just in case, every contract has now a utility method +`sync_notes` that can trigger the process manually if necessary. This can be useful since now the `DebugInfo` object that can be obtained when sending a tx with the `debug` flag set to true +no longer contains the notes that were generated in the transaction: + +```diff +const receipt = await inclusionsProofsContract.methods.create_note(owner, 5n).send().wait({ debug: true }); +-const { visibleIncomingNotes } = receipt.debugInfo!; +-expect(visibleIncomingNotes.length).toEqual(1); ++await inclusionsProofsContract.methods.sync_notes().simulate(); ++const incomingNotes = await wallet.getIncomingNotes({ txHash: receipt.txHash }); ++expect(incomingNotes.length).toEqual(1); +``` + +### [Token contract] Partial notes related refactor + +We've decided to replace the old "shield" flow with one leveraging partial notes. +This led to a removal of `shield` and `redeem_shield` functions and an introduction of `transfer_to_private`. +An advantage of the new approach is that only 1 tx is required and the API of partial notes is generally nicer. +For more information on partial notes refer to docs. + +### [Token contract] Function naming changes + +There have been a few naming changes done for improved consistency. +These are the renamings: +`transfer_public` --> `transfer_in_public` +`transfer_from` --> `transfer_in_private` +`mint_public` --> `mint_to_public` +`burn` --> `burn_private` + +## 0.62.0 + +### [TXE] Single execution environment + +Thanks to recent advancements in Brillig TXE performs every single call as if it was a nested call, spawning a new ACVM or AVM simulator without performance loss. +This ensures every single test runs in a consistent environment and allows for clearer test syntax: + +```diff +-let my_call_interface = MyContract::at(address).my_function(args); +-env.call_private(my_contract_interface) ++MyContract::at(address).my_function(args).call(&mut env.private()); +``` + +This implies every contract has to be deployed before it can be tested (via `env.deploy` or `env.deploy_self`) and of course it has to be recompiled if its code was changed before TXE can use the modified bytecode. + +### Uniqueness of L1 to L2 messages + +L1 to L2 messages have been updated to guarantee their uniqueness. This means that the hash of an L1 to L2 message cannot be precomputed, and must be obtained from the `MessageSent` event emitted by the `Inbox` contract, found in the L1 transaction receipt that inserted the message: + +```solidity +event MessageSent(uint256 indexed l2BlockNumber, uint256 index, bytes32 indexed hash); +``` + +This event now also includes an `index`. This index was previously required to consume an L1 to L2 message in a public function, and now it is also required for doing so in a private function, since it is part of the message hash preimage. The `PrivateContext` in aztec-nr has been updated to reflect this: + +```diff +pub fn consume_l1_to_l2_message( + &mut self, + content: Field, + secret: Field, + sender: EthAddress, ++ leaf_index: Field, +) { +``` + +This change has also modified the internal structure of the archiver database, making it incompatible with previous ones. Last, the API for obtaining an L1 to L2 message membership witness has been simplified to leverage message uniqueness: + +```diff +getL1ToL2MessageMembershipWitness( + blockNumber: L2BlockNumber, + l1ToL2Message: Fr, +- startIndex: bigint, +): Promise<[bigint, SiblingPath<typeof L1_TO_L2_MSG_TREE_HEIGHT>] | undefined>; +``` + +### Address is now a point + +The address now serves as someone's public key to encrypt incoming notes. An address point has a corresponding address secret, which is used to decrypt the notes encrypted with the address point. + +### Notes no longer store a hash of the nullifier public keys, and now store addresses + +Because of removing key rotation, we can now store addresses as the owner of a note. Because of this and the above change, we can and have removed the process of registering a recipient, because now we do not need any keys of the recipient. + +example_note.nr + +```diff +-npk_m_hash: Field ++owner: AztecAddress +``` + +PXE Interface + +```diff +-registerRecipient(completeAddress: CompleteAddress) +``` + +## 0.58.0 + +### [l1-contracts] Inbox's MessageSent event emits global tree index + +Earlier `MessageSent` event in Inbox emitted a subtree index (index of the message in the subtree of the l2Block). But the nodes and Aztec.nr expects the index in the global L1_TO_L2_MESSAGES_TREE. So to make it easier to parse this, Inbox now emits this global index. + +## 0.57.0 + +### Changes to PXE API and `ContractFunctionInteraction`` + +PXE APIs have been refactored to better reflect the lifecycle of a Tx (`execute private -> simulate kernels -> simulate public (estimate gas) -> prove -> send`) + +- `.simulateTx`: Now returns a `TxSimulationResult`, containing the output of private execution, kernel simulation and public simulation (optional). +- `.proveTx`: Now accepts the result of executing the private part of a transaction, so simulation doesn't have to happen again. + +Thanks to this refactor, `ContractFunctionInteraction` has been updated to remove its internal cache and avoid bugs due to its mutable nature. As a result our type-safe interfaces now have to be used as follows: + +```diff +-const action = MyContract.at(address).method(args); +-await action.prove(); +-await action.send().wait(); ++const action = MyContract.at(address).method(args); ++const provenTx = await action.prove(); ++await provenTx.send().wait(); +``` + +It's still possible to use `.send()` as before, which will perform proving under the hood. + +More changes are coming to these APIs to better support gas estimation mechanisms and advanced features. + +### Changes to public calling convention + +Contracts that include public functions (that is, marked with `#[public]`), are required to have a function `public_dispatch(selector: Field)` which acts as an entry point. This will be soon the only public function registered/deployed in contracts. The calling convention is updated so that external calls are made to this function. + +If you are writing your contracts using Aztec-nr, there is nothing you need to change. The `public_dispatch` function is automatically generated by the `#[aztec]` macro. + +### [Aztec.nr] Renamed `unsafe_rand` to `random` + +Since this is an `unconstrained` function, callers are already supposed to include an `unsafe` block, so this function has been renamed for reduced verbosity. + +```diff +-use aztec::oracle::unsafe_rand::unsafe_rand; ++use aztec::oracle::random::random; + +-let random_value = unsafe { unsafe_rand() }; ++let random_value = unsafe { random() }; +``` + +### [Aztec.js] Removed `L2Block.fromFields` + +`L2Block.fromFields` was a syntactic sugar which is causing [issues](https://github.com/AztecProtocol/aztec-packages/issues/8340) so we've removed it. + +```diff +-const l2Block = L2Block.fromFields({ header, archive, body }); ++const l2Block = new L2Block(archive, header, body); +``` + +### [Aztec.nr] Removed `SharedMutablePrivateGetter` + +This state variable was deleted due to it being difficult to use safely. + +### [Aztec.nr] Changes to `NullifiableNote` + +The `compute_nullifier_without_context` function is now `unconstrained`. It had always been meant to be called in unconstrained contexts (which is why it did not receive the `context` object), but now that Noir supports trait functions being `unconstrained` this can be implemented properly. Users must add the `unconstrained` keyword to their implementations of the trait: + +```diff +impl NullifiableNote for MyCustomNote { +- fn compute_nullifier_without_context(self) -> Field { ++ unconstrained fn compute_nullifier_without_context(self) -> Field { +``` + +### [Aztec.nr] Make `TestEnvironment` unconstrained + +All of `TestEnvironment`'s functions are now `unconstrained`, preventing accidentally calling them in a constrained circuit, among other kinds of user error. Becuase they work with mutable references, and these are not allowed to cross the constrained/unconstrained barrier, tests that use `TestEnvironment` must also become `unconstrained`. The recommended practice is to make _all_ Noir tests and test helper functions be `unconstrained: + +```diff +#[test] +-fn test_my_function() { ++unconstrained fn test_my_function() { + let env = TestEnvironment::new(); +``` + +### [Aztec.nr] removed `encode_and_encrypt_note` and renamed `encode_and_encrypt_note_with_keys` to `encode_and_encrypt_note` + +```diff +contract XYZ { +- use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note_with_keys; ++ use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note; +... + +- numbers.at(owner).initialize(&mut new_number).emit(encode_and_encrypt_note_with_keys(&mut context, owner_ovpk_m, owner_ivpk_m, owner)); ++ numbers.at(owner).initialize(&mut new_number).emit(encode_and_encrypt_note(&mut context, owner_ovpk_m, owner_ivpk_m, owner)); +} +``` + +## 0.56.0 + +### [Aztec.nr] Changes to contract definition + +We've migrated the Aztec macros to use the newly introduce meta programming Noir feature. Due to being Noir-based, the new macros are less obscure and can be more easily modified. + +As part of this transition, some changes need to be applied to Aztec contracts: + +- The top level `contract` block needs to have the `#[aztec]` macro applied to it. +- All `#[aztec(name)]` macros are renamed to `#[name]`. +- The storage struct (the one that gets the `#[storage]` macro applied) but be generic over a `Context` type, and all state variables receive this type as their last generic type parameter. + +```diff ++ use dep::aztec::macros::aztec; + +#[aztec] +contract Token { ++ use dep::aztec::macros::{storage::storage, events::event, functions::{initializer, private, view, public}}; + +- #[aztec(storage)] +- struct Storage { ++ #[storage] ++ struct Storage<Context> { +- admin: PublicMutable<AztecAddress>, ++ admin: PublicMutable<AztecAddress, Context>, +- minters: Map<AztecAddress, PublicMutable<bool>>, ++ minters: Map<AztecAddress, PublicMutable<bool, Context>, Context>, + } + +- #[aztec(public)] +- #[aztec(initializer)] ++ #[public] ++ #[initializer] + fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>, decimals: u8) { + ... + } + +- #[aztec(public)] +- #[aztec(view)] +- fn public_get_name() -> FieldCompressedString { ++ #[public] ++ #[view] + fn public_get_name() -> FieldCompressedString { + ... + } +``` + +### [Aztec.nr] Changes to `NoteInterface` + +The new macro model prevents partial trait auto-implementation: they either implement the entire trait or none of it. This means users can no longer implement part of `NoteInterface` and have the rest be auto-implemented. + +For this reason we've separated the methods which are auto-implemented and those which needs to be implemented manually into two separate traits: the auto-implemented ones stay in the `NoteInterface` trace and the manually implemented ones were moved to `NullifiableNote` (name likely to change): + +```diff +-#[aztec(note)] ++#[note] +struct AddressNote { + ... +} + +-impl NoteInterface<ADDRESS_NOTE_LEN, ADDRESS_NOTE_BYTES_LEN> for AddressNote { ++impl NullifiableNote for AddressNote { + fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field { + ... + } + + fn compute_nullifier_without_context(self) -> Field { + ... + } +} +``` + +### [Aztec.nr] Changes to contract interface + +The `Contract::storage()` static method has been renamed to `Contract::storage_layout()`. + +```diff +- let fee_payer_balances_slot = derive_storage_slot_in_map(Token::storage().balances.slot, fee_payer); +- let user_balances_slot = derive_storage_slot_in_map(Token::storage().balances.slot, user); ++ let fee_payer_balances_slot = derive_storage_slot_in_map(Token::storage_layout().balances.slot, fee_payer); ++ let user_balances_slot = derive_storage_slot_in_map(Token::storage_layout().balances.slot, user); +``` + +### Key rotation removed + +The ability to rotate incoming, outgoing, nullifying and tagging keys has been removed - this feature was easy to misuse and not worth the complexity and gate count cost. As part of this, the Key Registry contract has also been deleted. The API for fetching public keys has been adjusted accordingly: + +```diff +- let keys = get_current_public_keys(&mut context, account); ++ let keys = get_public_keys(account); +``` + +### [Aztec.nr] Rework `NoteGetterOptions::select` + +The `select` function in both `NoteGetterOptions` and `NoteViewerOptions` no longer takes an `Option` of a comparator, but instead requires an explicit comparator to be passed. Additionally, the order of the parameters has been changed so that they are `(lhs, operator, rhs)`. These two changes should make invocations of the function easier to read: + +```diff +- options.select(ValueNote::properties().value, amount, Option::none()) ++ options.select(ValueNote::properties().value, Comparator.EQ, amount) +``` + +## 0.53.0 + +### [Aztec.nr] Remove `OwnedNote` and create `UintNote` + +`OwnedNote` allowed having a U128 `value` in the custom note while `ValueNote` restricted to just a Field. + +We have removed `OwnedNote` but are introducing a more genric `UintNote` within aztec.nr + +``` +#[aztec(note)] +struct UintNote { + // The integer stored by the note + value: U128, + // The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent. + npk_m_hash: Field, + // Randomness of the note to hide its contents + randomness: Field, +} +``` + +### [TXE] logging + +You can now use `debug_log()` within your contract to print logs when using the TXE + +Remember to set the following environment variables to activate debug logging: + +```bash +export DEBUG="aztec:*" +export LOG_LEVEL="debug" +``` + +### [Account] no assert in is_valid_impl + +`is_valid_impl` method in account contract asserted if signature was true. Instead now we will return the verification to give flexibility to developers to handle it as they please. + +```diff +- let verification = std::ecdsa_secp256k1::verify_signature(public_key.x, public_key.y, signature, hashed_message); +- assert(verification == true); +- true ++ std::ecdsa_secp256k1::verify_signature(public_key.x, public_key.y, signature, hashed_message) +``` + +## 0.49.0 + +### Key Rotation API overhaul + +Public keys (ivpk, ovpk, npk, tpk) should no longer be fetched using the old `get_[x]pk_m` methods on the `Header` struct, but rather by calling `get_current_public_keys`, which returns a `PublicKeys` struct with all four keys at once: + +```diff ++use dep::aztec::keys::getters::get_current_public_keys; + +-let header = context.header(); +-let owner_ivpk_m = header.get_ivpk_m(&mut context, owner); +-let owner_ovpk_m = header.get_ovpk_m(&mut context, owner); ++let owner_keys = get_current_public_keys(&mut context, owner); ++let owner_ivpk_m = owner_keys.ivpk_m; ++let owner_ovpk_m = owner_keys.ovpk_m; +``` + +If using more than one key per account, this will result in very large circuit gate count reductions. + +Additionally, `get_historical_public_keys` was added to support reading historical keys using a historical header: + +```diff ++use dep::aztec::keys::getters::get_historical_public_keys; + +let historical_header = context.header_at(some_block_number); +-let owner_ivpk_m = header.get_ivpk_m(&mut context, owner); +-let owner_ovpk_m = header.get_ovpk_m(&mut context, owner); ++let owner_keys = get_historical_public_keys(historical_header, owner); ++let owner_ivpk_m = owner_keys.ivpk_m; ++let owner_ovpk_m = owner_keys.ovpk_m; +``` + +## 0.48.0 + +### NoteInterface changes + +`compute_note_hash_and_nullifier*` functions were renamed as `compute_nullifier*` and the `compute_nullifier` function now takes `note_hash_for_nullify` as an argument (this allowed us to reduce gate counts and the hash was typically computed before). Also `compute_note_hash_for_consumption` function was renamed as `compute_note_hash_for_nullification`. + +```diff +impl NoteInterface<VALUE_NOTE_LEN, VALUE_NOTE_BYTES_LEN> for ValueNote { +- fn compute_note_hash_and_nullifier(self, context: &mut PrivateContext) -> (Field, Field) { +- let note_hash_for_nullify = compute_note_hash_for_consumption(self); +- let secret = context.request_nsk_app(self.npk_m_hash); +- let nullifier = poseidon2_hash_with_separator([ +- note_hash_for_nullify, +- secret, +- ], +- DOM_SEP__NOTE_NULLIFIER as Field, +- ); +- (note_hash_for_nullify, nullifier) +- } +- fn compute_note_hash_and_nullifier_without_context(self) -> (Field, Field) { +- let note_hash_for_nullify = compute_note_hash_for_consumption(self); +- let secret = get_nsk_app(self.npk_m_hash); +- let nullifier = poseidon2_hash_with_separator([ +- note_hash_for_nullify, +- secret, +- ], +- DOM_SEP__NOTE_NULLIFIER as Field, +- ); +- (note_hash_for_nullify, nullifier) +- } + ++ fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field { ++ let secret = context.request_nsk_app(self.npk_m_hash); ++ poseidon2_hash_with_separator([ ++ note_hash_for_nullify, ++ secret ++ ], ++ DOM_SEP__NOTE_NULLIFIER as Field, ++ ) ++ } ++ fn compute_nullifier_without_context(self) -> Field { ++ let note_hash_for_nullify = compute_note_hash_for_nullification(self); ++ let secret = get_nsk_app(self.npk_m_hash); ++ poseidon2_hash_with_separator([ ++ note_hash_for_nullify, ++ secret, ++ ], ++ DOM_SEP__NOTE_NULLIFIER as Field, ++ ) ++ } +} +``` + +### Fee Juice rename + +The name of the canonical Gas contract has changed to Fee Juice. Update noir code: + +```diff +-GasToken::at(contract_address) ++FeeJuice::at(contract_address) +``` + +Additionally, `NativePaymentMethod` and `NativePaymentMethodWithClaim` have been renamed to `FeeJuicePaymentMethod` and `FeeJuicePaymentMethodWithClaim`. + +### PrivateSet::pop_notes(...) + +The most common flow when working with notes is obtaining them from a `PrivateSet` via `get_notes(...)` and then removing them via `PrivateSet::remove(...)`. +This is cumbersome and it results in unnecessary constraints due to a redundant note read request checks in the remove function. + +For this reason we've implemented `pop_notes(...)` which gets the notes, removes them from the set and returns them. +This tight coupling of getting notes and removing them allowed us to safely remove the redundant read request check. + +Token contract diff: + +```diff +-let options = NoteGetterOptions::with_filter(filter_notes_min_sum, target_amount).set_limit(max_notes); +-let notes = self.map.at(owner).get_notes(options); +-let mut subtracted = U128::from_integer(0); +-for i in 0..options.limit { +- if i < notes.len() { +- let note = notes.get_unchecked(i); +- self.map.at(owner).remove(note); +- subtracted = subtracted + note.get_amount(); +- } +-} +-assert(minuend >= subtrahend, "Balance too low"); ++let options = NoteGetterOptions::with_filter(filter_notes_min_sum, target_amount).set_limit(max_notes); ++let notes = self.map.at(owner).pop_notes(options); ++let mut subtracted = U128::from_integer(0); ++for i in 0..options.limit { ++ if i < notes.len() { ++ let note = notes.get_unchecked(i); ++ subtracted = subtracted + note.get_amount(); ++ } ++} ++assert(minuend >= subtrahend, "Balance too low"); +``` + +Note that `pop_notes` may not have obtained and removed any notes! The caller must place checks on the returned notes, e.g. in the example above by checking a sum of balances, or by checking the number of returned notes (`assert_eq(notes.len(), expected_num_notes)`). + +## 0.47.0 + +# [Aztec sandbox] TXE deployment changes + +The way simulated deployments are done in TXE tests has changed to avoid relying on TS interfaces. It is now possible to do it by directly pointing to a Noir standalone contract or workspace: + +```diff +-let deployer = env.deploy("path_to_contract_ts_interface"); ++let deployer = env.deploy("path_to_contract_root_folder_where_nargo_toml_is", "ContractName"); +``` + +Extended syntax for more use cases: + +```rust +// The contract we're testing +env.deploy_self("ContractName"); // We have to provide ContractName since nargo isn't ready to support multi-contract files + +// A contract in a workspace +env.deploy("../path/to/workspace@package_name", "ContractName"); // This format allows locating the artifact in the root workspace target folder, regardless of internal code organization +``` + +The deploy function returns a `Deployer`, which requires performing a subsequent call to `without_initializer()`, `with_private_initializer()` or `with_public_initializer()` just like before in order to **actually** deploy the contract. + +### [CLI] Command refactor and unification + `aztec test` + +Sandbox commands have been cleaned up and simplified. Doing `aztec-up` now gets you the following top-level commands: + +`aztec`: All the previous commands + all the CLI ones without having to prefix them with cli. Run `aztec` for help! +`aztec-nargo`: No changes + +**REMOVED/RENAMED**: + +- `aztec-sandbox` and `aztec sandbox`: now `aztec start --sandbox` +- `aztec-builder`: now `aztec codegen` and `aztec update` + +**ADDED**: + +- `aztec test [options]`: runs `aztec start --txe && aztec-nargo test --oracle-resolver http://aztec:8081 --silence-warnings [options]` via docker-compose allowing users to easily run contract tests using TXE + +## 0.45.0 + +### [Aztec.nr] Remove unencrypted logs from private + +They leak privacy so is a footgun! + +## 0.44.0 + +### [Aztec.nr] Autogenerate Serialize methods for events + +```diff +#[aztec(event)] +struct WithdrawalProcessed { + who: Field, + amount: Field, +} + +-impl Serialize<2> for WithdrawalProcessed { +- fn serialize(self: Self) -> [Field; 2] { +- [self.who.to_field(), self.amount as Field] +- } +} +``` + +### [Aztec.nr] rename `encode_and_encrypt_with_keys` to `encode_and_encrypt_note_with_keys` + +```diff +contract XYZ { +- use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_with_keys; ++ use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note_with_keys; +.... + +- numbers.at(owner).initialize(&mut new_number).emit(encode_and_encrypt_with_keys(&mut context, owner_ovpk_m, owner_ivpk_m)); ++ numbers.at(owner).initialize(&mut new_number).emit(encode_and_encrypt_note_with_keys(&mut context, owner_ovpk_m, owner_ivpk_m)); +} +``` + +### [Aztec.nr] changes to `NoteInterface` + +`compute_nullifier` function was renamed to `compute_note_hash_and_nullifier` and now the function has to return not only the nullifier but also the note hash used to compute the nullifier. +The same change was done to `compute_nullifier_without_context` function. +These changes were done because having the note hash exposed allowed us to not having to re-compute it again in `destroy_note` function of Aztec.nr which led to significant decrease in gate counts (see the [optimization PR](https://github.com/AztecProtocol/aztec-packages/pull/7103) for more details). + +```diff +- impl NoteInterface<VALUE_NOTE_LEN, VALUE_NOTE_BYTES_LEN> for ValueNote { +- fn compute_nullifier(self, context: &mut PrivateContext) -> Field { +- let note_hash_for_nullify = compute_note_hash_for_consumption(self); +- let secret = context.request_nsk_app(self.npk_m_hash); +- poseidon2_hash([ +- note_hash_for_nullify, +- secret, +- DOM_SEP__NOTE_NULLIFIER as Field, +- ]) +- } +- +- fn compute_nullifier_without_context(self) -> Field { +- let note_hash_for_nullify = compute_note_hash_for_consumption(self); +- let secret = get_nsk_app(self.npk_m_hash); +- poseidon2_hash([ +- note_hash_for_nullify, +- secret, +- DOM_SEP__NOTE_NULLIFIER as Field, +- ]) +- } +- } ++ impl NoteInterface<VALUE_NOTE_LEN, VALUE_NOTE_BYTES_LEN> for ValueNote { ++ fn compute_note_hash_and_nullifier(self, context: &mut PrivateContext) -> (Field, Field) { ++ let note_hash_for_nullify = compute_note_hash_for_consumption(self); ++ let secret = context.request_nsk_app(self.npk_m_hash); ++ let nullifier = poseidon2_hash([ ++ note_hash_for_nullify, ++ secret, ++ DOM_SEP__NOTE_NULLIFIER as Field, ++ ]); ++ (note_hash_for_nullify, nullifier) ++ } ++ ++ fn compute_note_hash_and_nullifier_without_context(self) -> (Field, Field) { ++ let note_hash_for_nullify = compute_note_hash_for_consumption(self); ++ let secret = get_nsk_app(self.npk_m_hash); ++ let nullifier = poseidon2_hash([ ++ note_hash_for_nullify, ++ secret, ++ DOM_SEP__NOTE_NULLIFIER as Field, ++ ]); ++ (note_hash_for_nullify, nullifier) ++ } ++ } +``` + +### [Aztec.nr] `note_getter` returns `BoundedVec` + +The `get_notes` and `view_notes` function no longer return an array of options (i.e. `[Option<Note>, N_NOTES]`) but instead a `BoundedVec<Note, N_NOTES>`. This better conveys the useful property the old array had of having all notes collapsed at the beginning of the array, which allows for powerful optimizations and gate count reduction when setting the `options.limit` value. + +A `BoundedVec` has a `max_len()`, which equals the number of elements it can hold, and a `len()`, which equals the number of elements it currently holds. Since `len()` is typically not knwon at compile time, iterating over a `BoundedVec` looks slightly different than iterating over an array of options: + +```diff +- let option_notes = get_notes(options); +- for i in 0..option_notes.len() { +- if option_notes[i].is_some() { +- let note = option_notes[i].unwrap_unchecked(); +- } +- } ++ let notes = get_notes(options); ++ for i in 0..notes.max_len() { ++ if i < notes.len() { ++ let note = notes.get_unchecked(i); ++ } ++ } +``` + +To further reduce gate count, you can iterate over `options.limit` instead of `max_len()`, since `options.limit` is guaranteed to be larger or equal to `len()`, and smaller or equal to `max_len()`: + +```diff +- for i in 0..notes.max_len() { ++ for i in 0..options.limit { +``` + +### [Aztec.nr] static private authwit + +The private authwit validation is now making a static call to the account contract instead of passing over control flow. This is to ensure that it cannot be used for re-entry. + +To make this change however, we cannot allow emitting a nullifier from the account contract, since that would break the static call. Instead, we will be changing the `spend_private_authwit` to a `verify_private_authwit` and in the `auth` library emit the nullifier. This means that the "calling" contract will now be emitting the nullifier, and not the account. For example, for a token contract, the nullifier is now emitted by the token contract. However, as this is done inside the `auth` library, the token contract doesn't need to change much. + +The biggest difference is related to "cancelling" an authwit. Since it is no longer in the account contract, you cannot just emit a nullifier from it anymore. Instead it must rely on the token contract providing functionality for cancelling. + +There are also a few general changes to how authwits are generated, namely to more easily support the data required for a validity lookup now. Previously we could lookup the `message_hash` directly at the account contract, now we instead need to use the `inner_hash` and the contract of the consumer to figure out if it have already been emitted. + +A minor extension have been made to the authwit creations to make it easier to sign a specific a hash with a specific caller, e.g., the `inner_hash` can be provided as `{consumer, inner_hash}` to the `createAuthWit` where it previously needed to do a couple of manual steps to compute the outer hash. The `computeOuterAuthWitHash` have been made internal and the `computeAuthWitMessageHash` can instead be used to compute the values similarly to other authwit computations. + +```diff +const innerHash = computeInnerAuthWitHash([Fr.ZERO, functionSelector.toField(), entrypointPackedArgs.hash]); +-const outerHash = computeOuterAuthWitHash( +- this.dappEntrypointAddress, +- new Fr(this.chainId), +- new Fr(this.version), +- innerHash, +-); ++const messageHash = computeAuthWitMessageHash( ++ { consumer: this.dappEntrypointAddress, innerHash }, ++ { chainId: new Fr(this.chainId), version: new Fr(this.version) }, ++); +``` + +If the wallet is used to compute the authwit, it will populate the chain id and version instead of requiring it to be provided by tha actor. + +```diff +const innerHash = computeInnerAuthWitHash([Fr.fromString('0xdead')]); +-const outerHash = computeOuterAuthWitHash(wallets[1].getAddress(), chainId, version, innerHash); +-const witness = await wallets[0].createAuthWit(outerHash); ++ const witness = await wallets[0].createAuthWit({ comsumer: accounts[1].address, inner_hash }); +``` + +## 0.43.0 + +### [Aztec.nr] break `token.transfer()` into `transfer` and `transferFrom` + +Earlier we had just one function - `transfer()` which used authwits to handle the case where a contract/user wants to transfer funds on behalf of another user. +To reduce circuit sizes and proof times, we are breaking up `transfer` and introducing a dedicated `transferFrom()` function like in the ERC20 standard. + +### [Aztec.nr] `options.limit` has to be constant + +The `limit` parameter in `NoteGetterOptions` and `NoteViewerOptions` is now required to be a compile-time constant. This allows performing loops over this value, which leads to reduced circuit gate counts when setting a `limit` value. + +### [Aztec.nr] canonical public authwit registry + +The public authwits are moved into a shared registry (auth registry) to make it easier for sequencers to approve for their non-revertible (setup phase) whitelist. Previously, it was possible to DOS a sequencer by having a very expensive authwit validation that fails at the end, now the whitelist simply need the registry. + +Notable, this means that consuming a public authwit will no longer emit a nullifier in the account contract but instead update STORAGE in the public domain. This means that there is a larger difference between private and public again. However, it also means that if contracts need to approve, and use the approval in the same tx, it is transient and don't need to go to DA (saving 96 bytes). + +For the typescript wallets this is handled so the APIs don't change, but account contracts should get rid of their current setup with `approved_actions`. + +```diff +- let actions = AccountActions::init(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); ++ let actions = AccountActions::init(&mut context, is_valid_impl); +``` + +For contracts we have added a `set_authorized` function in the auth library that can be used to set values in the registry. + +```diff +- storage.approved_action.at(message_hash).write(true); ++ set_authorized(&mut context, message_hash, true); +``` + +### [Aztec.nr] emit encrypted logs + +Emitting or broadcasting encrypted notes are no longer done as part of the note creation, but must explicitly be either emitted or discarded instead. + +```diff ++ use dep::aztec::encrypted_logs::encrypted_note_emission::{encode_and_encrypt, encode_and_encrypt_with_keys}; + +- storage.balances.sub(from, amount); ++ storage.balances.sub(from, amount).emit(encode_and_encrypt_with_keys(&mut context, from, from)); ++ storage.balances.sub(from, amount).emit(encode_and_encrypt_with_keys(&mut context, from_ovpk, from_ivpk)); ++ storage.balances.sub(from, amount).discard(); +``` + +## 0.42.0 + +### [Aztec.nr] Unconstrained Context + +Top-level unconstrained execution is now marked by the new `UnconstrainedContext`, which provides access to the block number and contract address being used in the simulation. Any custom state variables that provided unconstrained functions should update their specialization parameter: + +```diff ++ use dep::aztec::context::UnconstrainedContext; + +- impl MyStateVariable<()> { ++ impl MyStateVariable<UnconstrainedContext> { +``` + +### [Aztec.nr] Filtering is now constrained + +The `filter` argument of `NoteGetterOptions` (typically passed via the `with_filter()` function) is now applied in a constraining environment, meaning any assertions made during the filtering are guaranteed to hold. This mirrors the behavior of the `select()` function. + +### [Aztec.nr] Emitting encrypted notes and logs + +The `emit_encrypted_log` context function is now `encrypt_and_emit_log` or `encrypt_and_emit_note`. + +```diff +- context.emit_encrypted_log(log1); ++ context.encrypt_and_emit_log(log1); ++ context.encrypt_and_emit_note(note1); +``` + +Broadcasting a note will call `encrypt_and_emit_note` in the background. To broadcast a generic event, use `encrypt_and_emit_log` +with the same encryption parameters as notes require. Currently, only fields and arrays of fields are supported as events. + +By default, logs emitted via `encrypt_and_emit_log` will be siloed with a _masked_ contract address. To force the contract address to be revealed, so everyone can check it rather than just the log recipient, provide `randomness = 0`. + +## Public execution migrated to the Aztec Virtual Machine + +**What does this mean for me?** + +It should be mostly transparent, with a few caveats: + +- Not all Noir blackbox functions are supported by the AVM. Only `Sha256`, `PedersenHash`, `Poseidon2Permutation`, `Keccak256`, and `ToRadix` are supported. +- For public functions, `context.nullifier_exists(...)` will now also consider pending nullifiers. +- The following methods of `PublicContext` are not supported anymore: `fee_recipient`, `fee_per_da_gas`, `fee_per_l2_gas`, `call_public_function_no_args`, `static_call_public_function_no_args`, `delegate_call_public_function_no_args`, `call_public_function_with_packed_args`, `set_return_hash`, `finish`. However, in terms of functionality, the new context's interface should be equivalent (unless otherwise specified in this list). +- Delegate calls are not yet supported in the AVM. +- If you have types with custom serialization that you use across external contracts calls, you might need to modify its serialization to match how Noir would serialize it. This is a known problem unrelated to the AVM, but triggered more often when using it. +- A few error messages might change format, so you might need to change your test assertions. + +**Internal details** + +Before this change, public bytecode was executed using the same simulator as in private: the ACIR simulator (and internally, the Brillig VM). On the Aztec.nr side, public functions accessed the context through `PublicContext`. + +After this change, public bytecode will be run using the AVM simulator (the simulator for our upcoming zkVM). This bytecode is generated from Noir contracts in two steps: First, `nargo compile` produces an artifact which has Brillig bytecode for public functions, just as it did before. Second: the `avm-transpiler` takes that artifact, and it transpiles Brillig bytecode to AVM bytecode. This final artifact can now be deployed and used with the new public runtime. + +On the Aztec.nr side, public functions keep accessing the context using `PublicContext` but the underlying implementation is switch with what formerly was the `AvmContext`. + +## 0.41.0 + +### [Aztec.nr] State variable rework + +Aztec.nr state variables have been reworked so that calling private functions in public and vice versa is detected as an error during compilation instead of at runtime. This affects users in a number of ways: + +#### New compile time errors + +It used to be that calling a state variable method only available in public from a private function resulted in obscure runtime errors in the form of a failed `_is_some` assertion. + +Incorrect usage of the state variable methods now results in compile time errors. For example, given the following function: + +```rust +#[aztec(public)] +fn get_decimals() -> pub u8 { + storage.decimals.read_private() +} +``` + +The compiler will now error out with + +``` +Expected type SharedImmutable<_, &mut PrivateContext>, found type SharedImmutable<u8, &mut PublicContext> +``` + +The key component is the second generic parameter: the compiler expects a `PrivateContext` (becuse `read_private` is only available during private execution), but a `PublicContext` is being used instead (because of the `#[aztec(public)]` attribute). + +#### Generic parameters in `Storage` + +The `Storage` struct (the one marked with `#[aztec(storage)]`) should now be generic over a `Context` type, which matches the new generic parameter of all Aztec.nr libraries. This parameter is always the last generic parameter. + +This means that, without any additional features, we'd end up with some extra boilerplate when declaring this struct: + +```diff +#[aztec(storage)] +- struct Storage { ++ struct Storage<Context> { +- nonce_for_burn_approval: PublicMutable<Field>, ++ nonce_for_burn_approval: PublicMutable<Field, Context>, +- portal_address: SharedImmutable<EthAddress>, ++ portal_address: SharedImmutable<EthAddress, Context>, +- approved_action: Map<Field, PublicMutable<bool>>, ++ approved_action: Map<Field, PublicMutable<bool, Context>, Context>, +} +``` + +Because of this, the `#[aztec(storage)]` macro has been updated to **automatically inject** this `Context` generic parameter. The storage declaration does not require any changes. + +#### Removal of `Context` + +The `Context` type no longer exists. End users typically didn't use it, but if imported it needs to be deleted. + +### [Aztec.nr] View functions and interface navigation + +It is now possible to explicitly state a function doesn't perform any state alterations (including storage, logs, nullifiers and/or messages from L2 to L1) with the `#[aztec(view)]` attribute, similarly to solidity's `view` function modifier. + +```diff + #[aztec(public)] ++ #[aztec(view)] + fn get_price(asset_id: Field) -> Asset { + storage.assets.at(asset_id).read() + } +``` + +View functions only generate a `StaticCallInterface` that doesn't include `.call` or `.enqueue` methods. Also, the denomination `static` has been completely removed from the interfaces, in favor of the more familiar `view` + +```diff +- let price = PriceFeed::at(asset.oracle).get_price(0).static_call(&mut context).price; ++ let price = PriceFeed::at(asset.oracle).get_price(0).view(&mut context).price; +``` + +```diff +#[aztec(private)] +fn enqueue_public_get_value_from_child(target_contract: AztecAddress, value: Field) { +- StaticChild::at(target_contract).pub_get_value(value).static_enqueue(&mut context); ++ StaticChild::at(target_contract).pub_get_value(value).enqueue_view(&mut context); +} +``` + +Additionally, the Noir LSP will now honor "go to definitions" requests for contract interfaces (Ctrl+click), taking the user to the original function implementation. + +### [Aztec.js] Simulate changes + +- `.simulate()` now tracks closer the process performed by `.send().wait()`, specifically going through the account contract entrypoint instead of directly calling the intended function. +- `wallet.viewTx(...)` has been renamed to `wallet.simulateUnconstrained(...)` to better clarify what it does. + +### [Aztec.nr] Keys: Token note now stores an owner master nullifying public key hash instead of an owner address + +i.e. + +```diff +struct TokenNote { + amount: U128, +- owner: AztecAddress, ++ npk_m_hash: Field, + randomness: Field, +} +``` + +Creating a token note and adding it to storage now looks like this: + +```diff +- let mut note = ValueNote::new(new_value, owner); +- storage.a_private_value.insert(&mut note, true); ++ let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); ++ let owner_ivpk_m = get_ivpk_m(&mut context, owner); ++ let mut note = ValueNote::new(new_value, owner_npk_m_hash); ++ storage.a_private_value.insert(&mut note, true, owner_ivpk_m); +``` + +Computing the nullifier similarly changes to use this master nullifying public key hash. + +## 0.40.0 + +### [Aztec.nr] Debug logging + +The function `debug_log_array_with_prefix` has been removed. Use `debug_log_format` with `{}` instead. The special sequence `{}` will be replaced with the whole array. You can also use `{0}`, `{1}`, ... as usual with `debug_log_format`. + +```diff +- debug_log_array_with_prefix("Prefix", my_array); ++ debug_log_format("Prefix {}", my_array); +``` + +## 0.39.0 + +### [Aztec.nr] Mutable delays in `SharedMutable` + +The type signature for `SharedMutable` changed from `SharedMutable<T, DELAY>` to `SharedMutable<T, INITIAL_DELAY>`. The behavior is the same as before, except the delay can now be changed after deployment by calling `schedule_delay_change`. + +### [Aztec.nr] get_public_key oracle replaced with get_ivpk_m + +When implementing changes according to a new key scheme we had to change oracles. +What used to be called encryption public key is now master incoming viewing public key. + +```diff +- use dep::aztec::oracles::get_public_key::get_public_key; ++ use dep::aztec::keys::getters::get_ivpk_m; + +- let encryption_pub_key = get_public_key(self.owner); ++ let ivpk_m = get_ivpk_m(context, self.owner); +``` + +## 0.38.0 + +### [Aztec.nr] Emitting encrypted logs + +The `emit_encrypted_log` function is now a context method. + +```diff +- use dep::aztec::log::emit_encrypted_log; +- use dep::aztec::logs::emit_encrypted_log; + +- emit_encrypted_log(context, log1); ++ context.emit_encrypted_log(log1); +``` + +## 0.36.0 + +### `FieldNote` removed + +`FieldNote` only existed for testing purposes, and was not a note type that should be used in any real application. Its name unfortunately led users to think that it was a note type suitable to store a `Field` value, which it wasn't. + +If using `FieldNote`, you most likely want to use `ValueNote` instead, which has both randomness for privacy and an owner for proper nullification. + +### `SlowUpdatesTree` replaced for `SharedMutable` + +The old `SlowUpdatesTree` contract and libraries have been removed from the codebase, use the new `SharedMutable` library instead. This will require that you add a global variable specifying a delay in blocks for updates, and replace the slow updates tree state variable with `SharedMutable` variables. + +```diff ++ global CHANGE_ROLES_DELAY_BLOCKS = 5; + +struct Storage { +- slow_update: SharedImmutable<AztecAddress>, ++ roles: Map<AztecAddress, SharedMutable<UserFlags, CHANGE_ROLES_DELAY_BLOCKS>>, +} +``` + +Reading from `SharedMutable` is much simpler, all that's required is to call `get_current_value_in_public` or `get_current_value_in_private`, depending on the domain. + +```diff +- let caller_roles = UserFlags::new(U128::from_integer(slow.read_at_pub(context.msg_sender().to_field()).call(&mut context))); ++ let caller_roles = storage.roles.at(context.msg_sender()).get_current_value_in_public(); +``` + +Finally, you can remove all capsule usage on the client code or tests, since those are no longer required when working with `SharedMutable`. + +### [Aztec.nr & js] Portal addresses + +Deployments have been modified. No longer are portal addresses treated as a special class, being immutably set on creation of a contract. They are no longer passed in differently compared to the other variables and instead should be implemented using usual storage by those who require it. One should use the storage that matches the usecase - likely shared storage to support private and public. + +This means that you will likely add the portal as a constructor argument + +```diff +- fn constructor(token: AztecAddress) { +- storage.token.write(token); +- } ++ struct Storage { + ... ++ portal_address: SharedImmutable<AztecAddress>, ++ } ++ fn constructor(token: AztecAddress, portal_address: EthAddress) { ++ storage.token.write(token); ++ storage.portal_address.initialize(portal_address); ++ } +``` + +And read it from storage whenever needed instead of from the context. + +```diff +- context.this_portal_address(), ++ storage.portal_address.read_public(), +``` + +### [Aztec.nr] Oracles + +Oracle `get_nullifier_secret_key` was renamed to `get_app_nullifier_secret_key` and `request_nullifier_secret_key` function on PrivateContext was renamed as `request_app_nullifier_secret_key`. + +```diff +- let secret = get_nullifier_secret_key(self.owner); ++ let secret = get_app_nullifier_secret_key(self.owner); +``` + +```diff +- let secret = context.request_nullifier_secret_key(self.owner); ++ let secret = context.request_app_nullifier_secret_key(self.owner); +``` + +### [Aztec.nr] Contract interfaces + +It is now possible to import contracts on another contracts and use their automatic interfaces to perform calls. The interfaces have the same name as the contract, and are automatically exported. Parameters are automatically serialized (using the `Serialize<N>` trait) and return values are automatically deserialized (using the `Deserialize<N>` trait). Serialize and Deserialize methods have to conform to the standard ACVM serialization schema for the interface to work! + +1. Only fixed length types are supported +2. All numeric types become Fields +3. Strings become arrays of Fields, one per char +4. Arrays become arrays of Fields following rules 2 and 3 +5. Structs become arrays of Fields, with every item defined in the same order as they are in Noir code, following rules 2, 3, 4 and 5 (recursive) + +```diff +- context.call_public_function( +- storage.gas_token_address.read_private(), +- FunctionSelector::from_signature("pay_fee(Field)"), +- [42] +- ); +- +- context.call_public_function( +- storage.gas_token_address.read_private(), +- FunctionSelector::from_signature("pay_fee(Field)"), +- [42] +- ); +- +- let _ = context.call_private_function( +- storage.subscription_token_address.read_private(), +- FunctionSelector::from_signature("transfer((Field),(Field),Field,Field)"), +- [ +- context.msg_sender().to_field(), +- storage.subscription_recipient_address.read_private().to_field(), +- storage.subscription_price.read_private(), +- nonce +- ] +- ); ++ use dep::gas_token::GasToken; ++ use dep::token::Token; ++ ++ ... ++ // Public call from public land ++ GasToken::at(storage.gas_token_address.read_private()).pay_fee(42).call(&mut context); ++ // Public call from private land ++ GasToken::at(storage.gas_token_address.read_private()).pay_fee(42).enqueue(&mut context); ++ // Private call from private land ++ Token::at(asset).transfer(context.msg_sender(), storage.subscription_recipient_address.read_private(), amount, nonce).call(&mut context); +``` + +It is also possible to use these automatic interfaces from the local contract, and thus enqueue public calls from private without having to rely on low level `context` calls. + +### [Aztec.nr] Rename max block number setter + +The `request_max_block_number` function has been renamed to `set_tx_max_block_number` to better reflect that it is not a getter, and that the setting is transaction-wide. + +```diff +- context.request_max_block_number(value); ++ context.set_tx_max_block_number(value); +``` + +### [Aztec.nr] Get portal address + +The `get_portal_address` oracle was removed. If you need to get the portal address of SomeContract, add the following methods to it + +``` +#[aztec(private)] +fn get_portal_address() -> EthAddress { + context.this_portal_address() +} + +#[aztec(public)] +fn get_portal_address_public() -> EthAddress { + context.this_portal_address() +} +``` + +and change the call to `get_portal_address` + +```diff +- let portal_address = get_portal_address(contract_address); ++ let portal_address = SomeContract::at(contract_address).get_portal_address().call(&mut context); +``` + +### [Aztec.nr] Required gas limits for public-to-public calls + +When calling a public function from another public function using the `call_public_function` method, you must now specify how much gas you're allocating to the nested call. This will later allow you to limit the amount of gas consumed by the nested call, and handle any out of gas errors. + +Note that gas limits are not yet enforced. For now, it is suggested you use `dep::aztec::context::gas::GasOpts::default()` which will forward all available gas. + +```diff ++ use dep::aztec::context::gas::GasOpts; + +- context.call_public_function(target_contract, target_selector, args); ++ context.call_public_function(target_contract, target_selector, args, GasOpts::default()); +``` + +Note that this is not required when enqueuing a public function from a private one, since top-level enqueued public functions will always consume all gas available for the transaction, as it is not possible to handle any out-of-gas errors. + +### [Aztec.nr] Emitting unencrypted logs + +The `emit_unencrypted_logs` function is now a context method. + +```diff +- use dep::aztec::log::emit_unencrypted_log; +- use dep::aztec::log::emit_unencrypted_log_from_private; + +- emit_unencrypted_log(context, log1); +- emit_unencrypted_log_from_private(context, log2); ++ context.emit_unencrypted_log(log1); ++ context.emit_unencrypted_log(log2); +``` + +## 0.33 + +### [Aztec.nr] Storage struct annotation + +The storage struct now identified by the annotation `#[aztec(storage)]`, instead of having to rely on it being called `Storage`. + +```diff +- struct Storage { +- ... +- } ++ #[aztec(storage)] ++ struct MyStorageStruct { ++ ... ++ } +``` + +### [Aztec.js] Storage layout and note info + +Storage layout and note information are now exposed in the TS contract artifact + +```diff +- const note = new Note([new Fr(mintAmount), secretHash]); +- const pendingShieldStorageSlot = new Fr(5n); // storage slot for pending_shields +- const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // note type id for TransparentNote +- const extendedNote = new ExtendedNote( +- note, +- admin.address, +- token.address, +- pendingShieldStorageSlot, +- noteTypeId, +- receipt.txHash, +- ); +- await pxe.addNote(extendedNote); ++ const note = new Note([new Fr(mintAmount), secretHash]); ++ const extendedNote = new ExtendedNote( ++ note, ++ admin.address, ++ token.address, ++ TokenContract.storage.pending_shields.slot, ++ TokenContract.notes.TransparentNote.id, ++ receipt.txHash, ++ ); ++ await pxe.addNote(extendedNote); +``` + +### [Aztec.nr] rand oracle is now called unsafe_rand + +`oracle::rand::rand` has been renamed to `oracle::unsafe_rand::unsafe_rand`. +This change was made to communicate that we do not constrain the value in circuit and instead we just trust our PXE. + +```diff +- let random_value = rand(); ++ let random_value = unsafe_rand(); +``` + +### [AztecJS] Simulate and get return values for ANY call and introducing `prove()` + +Historically it have been possible to "view" `unconstrained` functions to simulate them and get the return values, but not for `public` nor `private` functions. +This has lead to a lot of bad code where we have the same function implemented thrice, once in `private`, once in `public` and once in `unconstrained`. +It is not possible to call `simulate` on any call to get the return values! +However, beware that it currently always returns a Field array of size 4 for private and public. +This will change to become similar to the return values of the `unconstrained` functions with proper return types. + +```diff +- #[aztec(private)] +- fn get_shared_immutable_constrained_private() -> pub Leader { +- storage.shared_immutable.read_private() +- } +- +- unconstrained fn get_shared_immutable() -> pub Leader { +- storage.shared_immutable.read_public() +- } + ++ #[aztec(private)] ++ fn get_shared_immutable_private() -> pub Leader { ++ storage.shared_immutable.read_private() ++ } + +- const returnValues = await contract.methods.get_shared_immutable().view(); ++ const returnValues = await contract.methods.get_shared_immutable_private().simulate(); +``` + +```diff +await expect( +- asset.withWallet(wallets[1]).methods.update_admin(newAdminAddress).simulate()).rejects.toThrow( ++ asset.withWallet(wallets[1]).methods.update_admin(newAdminAddress).prove()).rejects.toThrow( + "Assertion failed: caller is not admin 'caller_roles.is_admin'", +); +``` + +## 0.31.0 + +### [Aztec.nr] Public storage historical read API improvement + +`history::public_value_inclusion::prove_public_value_inclusion` has been renamed to `history::storage::public_storage_historical_read`, and its API changed slightly. Instead of receiving a `value` parameter it now returns the historical value stored at that slot. + +If you were using an oracle to get the value to pass to `prove_public_value_inclusion`, drop the oracle and use the return value from `public_storage_historical_read` instead: + +```diff +- let value = read_storage(); +- prove_public_value_inclusion(value, storage_slot, contract_address, context); ++ let value = public_storage_historical_read(storage_slot, contract_address, context); +``` + +If you were proving historical existence of a value you got via some other constrained means, perform an assertion against the return value of `public_storage_historical_read` instead: + +```diff +- prove_public_value_inclusion(value, storage_slot, contract_address, context); ++ assert(public_storage_historical_read(storage_slot, contract_address, context) == value); +``` + +## 0.30.0 + +### [AztecJS] Simplify authwit syntax + +```diff +- const messageHash = computeAuthWitMessageHash(accounts[1].address, action.request()); +- await wallets[0].setPublicAuth(messageHash, true).send().wait(); ++ await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait(); +``` + +```diff +const action = asset + .withWallet(wallets[1]) + .methods.unshield(accounts[0].address, accounts[1].address, amount, nonce); +-const messageHash = computeAuthWitMessageHash(accounts[1].address, action.request()); +-const witness = await wallets[0].createAuthWitness(messageHash); ++const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action }); +await wallets[1].addAuthWitness(witness); +``` + +Also note some of the naming changes: +`setPublicAuth` -> `setPublicAuthWit` +`createAuthWitness` -> `createAuthWit` + +### [Aztec.nr] Automatic NoteInterface implementation and selector changes + +Implementing a note required a fair amount of boilerplate code, which has been substituted by the `#[aztec(note)]` attribute. + +```diff ++ #[aztec(note)] +struct AddressNote { + address: AztecAddress, + owner: AztecAddress, + randomness: Field, + header: NoteHeader +} + +impl NoteInterface<ADDRESS_NOTE_LEN> for AddressNote { +- fn serialize_content(self) -> [Field; ADDRESS_NOTE_LEN]{ +- [self.address.to_field(), self.owner.to_field(), self.randomness] +- } +- +- fn deserialize_content(serialized_note: [Field; ADDRESS_NOTE_LEN]) -> Self { +- AddressNote { +- address: AztecAddress::from_field(serialized_note[0]), +- owner: AztecAddress::from_field(serialized_note[1]), +- randomness: serialized_note[2], +- header: NoteHeader::empty(), +- } +- } +- +- fn compute_note_content_hash(self) -> Field { +- pedersen_hash(self.serialize_content(), 0) +- } +- + fn compute_nullifier(self, context: &mut PrivateContext) -> Field { + let note_hash_for_nullify = compute_note_hash_for_consumption(self); + let secret = context.request_nullifier_secret_key(self.owner); + pedersen_hash([ + note_hash_for_nullify, + secret.low, + secret.high, + ],0) + } + + fn compute_nullifier_without_context(self) -> Field { + let note_hash_for_nullify = compute_note_hash_for_consumption(self); + let secret = get_nullifier_secret_key(self.owner); + pedersen_hash([ + note_hash_for_nullify, + secret.low, + secret.high, + ],0) + } + +- fn set_header(&mut self, header: NoteHeader) { +- self.header = header; +- } +- +- fn get_header(note: Self) -> NoteHeader { +- note.header +- } + + fn broadcast(self, context: &mut PrivateContext, slot: Field) { + let encryption_pub_key = get_public_key(self.owner); + emit_encrypted_log( + context, + (*context).this_address(), + slot, + Self::get_note_type_id(), + encryption_pub_key, + self.serialize_content(), + ); + } + +- fn get_note_type_id() -> Field { +- 6510010011410111511578111116101 +- } +} +``` + +Automatic note (de)serialization implementation also means it is now easier to filter notes using `NoteGetterOptions.select` via the `::properties()` helper: + +Before: + +```rust +let options = NoteGetterOptions::new().select(0, amount, Option::none()).select(1, owner.to_field(), Option::none()).set_limit(1); +``` + +After: + +```rust +let options = NoteGetterOptions::new().select(ValueNote::properties().value, amount, Option::none()).select(ValueNote::properties().owner, owner.to_field(), Option::none()).set_limit(1); +``` + +The helper returns a metadata struct that looks like this (if autogenerated) + +```rust +ValueNoteProperties { + value: PropertySelector { index: 0, offset: 0, length: 32 }, + owner: PropertySelector { index: 1, offset: 0, length: 32 }, + randomness: PropertySelector { index: 2, offset: 0, length: 32 }, +} +``` + +It can also be used for the `.sort` method. + +## 0.27.0 + +### `initializer` macro replaces `constructor` + +Before this version, every contract was required to have exactly one `constructor` private function, that was used for deployment. We have now removed this requirement, and made `constructor` a function like any other. + +To signal that a function can be used to **initialize** a contract, you must now decorate it with the `#[aztec(initializer)]` attribute. Initializers are regular functions that set an "initialized" flag (a nullifier) for the contract. A contract can only be initialized once, and contract functions can only be called after the contract has been initialized, much like a constructor. However, if a contract defines no initializers, it can be called at any time. Additionally, you can define as many initializer functions in a contract as you want, both private and public. + +To migrate from current code, simply add an initializer attribute to your constructor functions. + +```diff ++ #[aztec(initializer)] +#[aztec(private)] +fn constructor() { ... } +``` + +If your private constructor was used to just call a public internal initializer, then remove the private constructor and flag the public function as initializer. And if your private constructor was an empty one, just remove it. + +## 0.25.0 + +### [Aztec.nr] Static calls + +It is now possible to perform static calls from both public and private functions. Static calls forbid any modification to the state, including L2->L1 messages or log generation. Once a static context is set through a static all, every subsequent call will also be treated as static via context propagation. + +```rust +context.static_call_private_function(targetContractAddress, targetSelector, args); + +context.static_call_public_function(targetContractAddress, targetSelector, args); +``` + +### [Aztec.nr] Introduction to `prelude` + +A new `prelude` module to include common Aztec modules and types. +This simplifies dependency syntax. For example: + +```rust +use dep::aztec::protocol::address::AztecAddress; +use dep::aztec::{ + context::{PrivateContext, Context}, note::{note_header::NoteHeader, utils as note_utils}, + state_vars::Map +}; +``` + +Becomes: + +```rust +use dep::aztec::prelude::{AztecAddress, NoteHeader, PrivateContext, Map}; +use dep::aztec::context::Context; +use dep::aztec::notes::utils as note_utils; +``` + +This will be further simplified in future versions (See [4496](https://github.com/AztecProtocol/aztec-packages/pull/4496) for further details). + +The prelude consists of + +\[Edit: removed because the prelude no-longer exists\] + +### `internal` is now a macro + +The `internal` keyword is now removed from Noir, and is replaced by an `aztec(internal)` attribute in the function. The resulting behavior is exactly the same: these functions will only be callable from within the same contract. + +Before: + +```rust +#[aztec(private)] +internal fn double(input: Field) -> Field { + input * 2 +} +``` + +After: + +```rust +#[aztec(private)] +#[aztec(internal)] +fn double(input: Field) -> Field { + input * 2 +} +``` + +### [Aztec.nr] No SafeU120 anymore! + +Noir now have overflow checks by default. So we don't need SafeU120 like libraries anymore. + +You can replace it with `U128` instead + +Before: + +``` +SafeU120::new(0) +``` + +Now: + +``` +U128::from_integer(0) +``` + +### [Aztec.nr] `compute_note_hash_and_nullifier` is now autogenerated + +Historically developers have been required to include a `compute_note_hash_and_nullifier` function in each of their contracts. This function is now automatically generated, and all instances of it in contract code can be safely removed. + +It is possible to provide a user-defined implementation, in which case auto-generation will be skipped (though there are no known use cases for this). + +### [Aztec.nr] Updated naming of state variable wrappers + +We have decided to change the naming of our state variable wrappers because the naming was not clear. +The changes are as follows: + +1. `Singleton` -> `PrivateMutable` +2. `ImmutableSingleton` -> `PrivateImmutable` +3. `StablePublicState` -> `SharedImmutable` +4. `PublicState` -> `PublicMutable` + +This is the meaning of "private", "public" and "shared": +Private: read (R) and write (W) from private, not accessible from public +Public: not accessible from private, R/W from public +Shared: R from private, R/W from public + +Note: `SlowUpdates` will be renamed to `SharedMutable` once the implementation is ready. + +### [Aztec.nr] Authwit updates + +Authentication Witnesses have been updates such that they are now cancellable and scoped to a specific consumer. +This means that the `authwit` nullifier must be emitted from the account contract, which require changes to the interface. +Namely, the `assert_current_call_valid_authwit_public` and `assert_current_call_valid_authwit` in `auth.nr` will **NO LONGER** emit a nullifier. +Instead it will call a `spend_*_authwit` function in the account contract - which will emit the nullifier and perform a few checks. +This means that the `is_valid` functions have been removed to not confuse it for a non-mutating function (static). +Furthermore, the `caller` parameter of the "authwits" have been moved "further out" such that the account contract can use it in validation, allowing scoped approvals from the account POV. +For most contracts, this won't be changing much, but for the account contract, it will require a few changes. + +Before: + +```rust +#[aztec(public)] +fn is_valid_public(message_hash: Field) -> Field { + let actions = AccountActions::public(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); + actions.is_valid_public(message_hash) +} + +#[aztec(private)] +fn is_valid(message_hash: Field) -> Field { + let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); + actions.is_valid(message_hash) +} +``` + +After: + +```rust +#[aztec(private)] +fn verify_private_authwit(inner_hash: Field) -> Field { + let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); + actions.verify_private_authwit(inner_hash) +} + +#[aztec(public)] +fn spend_public_authwit(inner_hash: Field) -> Field { + let actions = AccountActions::public(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); + actions.spend_public_authwit(inner_hash) +} +``` + +## 0.24.0 + +### Introduce Note Type IDs + +Note Type IDs are a new feature which enable contracts to have multiple `Map`s with different underlying note types, something that was not possible before. This is done almost without any user intervention, though some minor changes are required. + +The mandatory `compute_note_hash_and_nullifier` now has a fifth parameter `note_type_id`. Use this instead of `storage_slot` to determine which deserialization function to use. + +Before: + +```rust +unconstrained fn compute_note_hash_and_nullifier( + contract_address: AztecAddress, + nonce: Field, + storage_slot: Field, + preimage: [Field; TOKEN_NOTE_LEN] +) -> pub [Field; 4] { + let note_header = NoteHeader::new(contract_address, nonce, storage_slot); + + if (storage_slot == storage.pending_shields.get_storage_slot()) { + note_utils::compute_note_hash_and_nullifier(TransparentNote::deserialize_content, note_header, preimage) + } else if (note_type_id == storage.slow_update.get_storage_slot()) { + note_utils::compute_note_hash_and_nullifier(FieldNote::deserialize_content, note_header, preimage) + } else { + note_utils::compute_note_hash_and_nullifier(TokenNote::deserialize_content, note_header, preimage) + } +``` + +Now: + +```rust +unconstrained fn compute_note_hash_and_nullifier( + contract_address: AztecAddress, + nonce: Field, + storage_slot: Field, + note_type_id: Field, + preimage: [Field; TOKEN_NOTE_LEN] +) -> pub [Field; 4] { + let note_header = NoteHeader::new(contract_address, nonce, storage_slot); + + if (note_type_id == TransparentNote::get_note_type_id()) { + note_utils::compute_note_hash_and_nullifier(TransparentNote::deserialize_content, note_header, preimage) + } else if (note_type_id == FieldNote::get_note_type_id()) { + note_utils::compute_note_hash_and_nullifier(FieldNote::deserialize_content, note_header, preimage) + } else { + note_utils::compute_note_hash_and_nullifier(TokenNote::deserialize_content, note_header, preimage) + } +``` + +The `NoteInterface` trait now has an additional `get_note_type_id()` function. This implementation will be autogenerated in the future, but for now providing any unique ID will suffice. The suggested way to do it is by running the Python command shown in the comment below: + +```rust +impl NoteInterface<N> for MyCustomNote { + fn get_note_type_id() -> Field { + // python -c "print(int(''.join(str(ord(c)) for c in 'MyCustomNote')))" + 771216711711511611110978111116101 + } +} +``` + +### [js] Importing contracts in JS + +`@aztec/noir-contracts` is now `@aztec/noir-contracts.js`. You'll need to update your package.json & imports. + +Before: + +```js +import { TokenContract } from "@aztec/noir-contracts/Token"; +``` + +Now: + +```js +import { TokenContract } from "@aztec/noir-contracts.js/Token"; +``` + +### [Aztec.nr] Aztec.nr contracts location change in Nargo.toml + +Aztec contracts are now moved outside of the `yarn-project` folder and into `noir-projects`, so you need to update your imports. + +Before: + +```rust +easy_private_token_contract = {git = "https://github.com/AztecProtocol/aztec-packages/", tag ="v0.23.0", directory = "yarn-project/noir-contracts/contracts/easy_private_token_contract"} +``` + +Now, update the `yarn-project` folder for `noir-projects`: + +```rust +easy_private_token_contract = {git = "https://github.com/AztecProtocol/aztec-packages/", tag ="v0.24.0", directory = "noir-projects/noir-contracts/contracts/easy_private_token_contract"} +``` + +## 0.22.0 + +### `Note::compute_note_hash` renamed to `Note::compute_note_content_hash` + +The `compute_note_hash` function in of the `Note` trait has been renamed to `compute_note_content_hash` to avoid being confused with the actual note hash. + +Before: + +```rust +impl NoteInterface for CardNote { + fn compute_note_hash(self) -> Field { + pedersen_hash([ + self.owner.to_field(), + ], 0) + } +``` + +Now: + +```rust +impl NoteInterface for CardNote { + fn compute_note_content_hash(self) -> Field { + pedersen_hash([ + self.owner.to_field(), + ], 0) + } +``` + +### Introduce `compute_note_hash_for_consumption` and `compute_note_hash_for_insertion` + +Makes a split in logic for note hash computation for consumption and insertion. This is to avoid confusion between the two, and to make it clear that the note hash for consumption is different from the note hash for insertion (sometimes). + +`compute_note_hash_for_consumption` replaces `compute_note_hash_for_read_or_nullify`. +`compute_note_hash_for_insertion` is new, and mainly used in `lifecycle.nr`` + +### `Note::serialize_content` and `Note::deserialize_content` added to `NoteInterface + +The `NoteInterface` have been extended to include `serialize_content` and `deserialize_content` functions. This is to convey the difference between serializing the full note, and just the content. This change allows you to also add a `serialize` function to support passing in a complete note to a function. + +Before: + +```rust +impl Serialize<ADDRESS_NOTE_LEN> for AddressNote { + fn serialize(self) -> [Field; ADDRESS_NOTE_LEN]{ + [self.address.to_field(), self.owner.to_field(), self.randomness] + } +} +impl Deserialize<ADDRESS_NOTE_LEN> for AddressNote { + fn deserialize(serialized_note: [Field; ADDRESS_NOTE_LEN]) -> Self { + AddressNote { + address: AztecAddress::from_field(serialized_note[0]), + owner: AztecAddress::from_field(serialized_note[1]), + randomness: serialized_note[2], + header: NoteHeader::empty(), + } + } +``` + +Now + +```rust +impl NoteInterface<ADDRESS_NOTE_LEN> for AddressNote { + fn serialize_content(self) -> [Field; ADDRESS_NOTE_LEN]{ + [self.address.to_field(), self.owner.to_field(), self.randomness] + } + + fn deserialize_content(serialized_note: [Field; ADDRESS_NOTE_LEN]) -> Self { + AddressNote { + address: AztecAddress::from_field(serialized_note[0]), + owner: AztecAddress::from_field(serialized_note[1]), + randomness: serialized_note[2], + header: NoteHeader::empty(), + } + } + ... +} +``` + +### [Aztec.nr] No storage.init() and `Serialize`, `Deserialize`, `NoteInterface` as Traits, removal of SerializationMethods and SERIALIZED_LEN + +Storage definition and initialization has been simplified. Previously: + +```rust +struct Storage { + leader: PublicState<Leader, LEADER_SERIALIZED_LEN>, + legendary_card: Singleton<CardNote, CARD_NOTE_LEN>, + profiles: Map<AztecAddress, Singleton<CardNote, CARD_NOTE_LEN>>, + test: Set<CardNote, CARD_NOTE_LEN>, + imm_singleton: PrivateImmutable<CardNote, CARD_NOTE_LEN>, +} + +impl Storage { + fn init(context: Context) -> Self { + Storage { + leader: PublicMutable::new( + context, + 1, + LeaderSerializationMethods, + ), + legendary_card: PrivateMutable::new(context, 2, CardNoteMethods), + profiles: Map::new( + context, + 3, + |context, slot| { + PrivateMutable::new(context, slot, CardNoteMethods) + }, + ), + test: Set::new(context, 4, CardNoteMethods), + imm_singleton: PrivateImmutable::new(context, 4, CardNoteMethods), + } + } + } +``` + +Now: + +```rust +struct Storage { + leader: PublicMutable<Leader>, + legendary_card: Singleton<CardNote>, + profiles: Map<AztecAddress, Singleton<CardNote>>, + test: Set<CardNote>, + imm_singleton: PrivateImmutable<CardNote>, +} +``` + +For this to work, Notes must implement Serialize, Deserialize and NoteInterface Traits. Previously: + +```rust +use dep::aztec::protocol::address::AztecAddress; +use dep::aztec::{ + note::{ + note_header::NoteHeader, + note_interface::NoteInterface, + utils::compute_note_hash_for_read_or_nullify, + }, + oracle::{ + nullifier_key::get_nullifier_secret_key, + get_public_key::get_public_key, + }, + log::emit_encrypted_log, + hash::pedersen_hash, + context::PrivateContext, +}; + +// Shows how to create a custom note + +global CARD_NOTE_LEN: Field = 1; + +impl CardNote { + pub fn new(owner: AztecAddress) -> Self { + CardNote { + owner, + } + } + + pub fn serialize(self) -> [Field; CARD_NOTE_LEN] { + [self.owner.to_field()] + } + + pub fn deserialize(serialized_note: [Field; CARD_NOTE_LEN]) -> Self { + CardNote { + owner: AztecAddress::from_field(serialized_note[1]), + } + } + + pub fn compute_note_hash(self) -> Field { + pedersen_hash([ + self.owner.to_field(), + ],0) + } + + pub fn compute_nullifier(self, context: &mut PrivateContext) -> Field { + let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(CardNoteMethods, self); + let secret = context.request_nullifier_secret_key(self.owner); + pedersen_hash([ + note_hash_for_nullify, + secret.high, + secret.low, + ],0) + } + + pub fn compute_nullifier_without_context(self) -> Field { + let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(CardNoteMethods, self); + let secret = get_nullifier_secret_key(self.owner); + pedersen_hash([ + note_hash_for_nullify, + secret.high, + secret.low, + ],0) + } + + pub fn set_header(&mut self, header: NoteHeader) { + self.header = header; + } + + // Broadcasts the note as an encrypted log on L1. + pub fn broadcast(self, context: &mut PrivateContext, slot: Field) { + let encryption_pub_key = get_public_key(self.owner); + emit_encrypted_log( + context, + (*context).this_address(), + slot, + encryption_pub_key, + self.serialize(), + ); + } +} + +fn deserialize(serialized_note: [Field; CARD_NOTE_LEN]) -> CardNote { + CardNote::deserialize(serialized_note) +} + +fn serialize(note: CardNote) -> [Field; CARD_NOTE_LEN] { + note.serialize() +} + +fn compute_note_hash(note: CardNote) -> Field { + note.compute_note_hash() +} + +fn compute_nullifier(note: CardNote, context: &mut PrivateContext) -> Field { + note.compute_nullifier(context) +} + +fn compute_nullifier_without_context(note: CardNote) -> Field { + note.compute_nullifier_without_context() +} + +fn get_header(note: CardNote) -> NoteHeader { + note.header +} + +fn set_header(note: &mut CardNote, header: NoteHeader) { + note.set_header(header) +} + +// Broadcasts the note as an encrypted log on L1. +fn broadcast(context: &mut PrivateContext, slot: Field, note: CardNote) { + note.broadcast(context, slot); +} + +global CardNoteMethods = NoteInterface { + deserialize, + serialize, + compute_note_hash, + compute_nullifier, + compute_nullifier_without_context, + get_header, + set_header, + broadcast, +}; +``` + +Now: + +```rust +use dep::aztec::{ + note::{ + note_header::NoteHeader, + note_interface::NoteInterface, + utils::compute_note_hash_for_read_or_nullify, + }, + oracle::{ + nullifier_key::get_nullifier_secret_key, + get_public_key::get_public_key, + }, + log::emit_encrypted_log, + hash::pedersen_hash, + context::PrivateContext, + protocol::{ + address::AztecAddress, + traits::{Serialize, Deserialize, Empty} + } +}; + +// Shows how to create a custom note + +global CARD_NOTE_LEN: Field = 1; + +impl CardNote { + pub fn new(owner: AztecAddress) -> Self { + CardNote { + owner, + } + } +} + +impl NoteInterface for CardNote { + fn compute_note_content_hash(self) -> Field { + pedersen_hash([ + self.owner.to_field(), + ],0) + } + + fn compute_nullifier(self, context: &mut PrivateContext) -> Field { + let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(self); + let secret = context.request_nullifier_secret_key(self.owner); + pedersen_hash([ + note_hash_for_nullify, + secret.high, + secret.low, + ],0) + } + + fn compute_nullifier_without_context(self) -> Field { + let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(self); + let secret = get_nullifier_secret_key(self.owner); + pedersen_hash([ + note_hash_for_nullify, + secret.high, + secret.low, + ],0) + } + + fn set_header(&mut self, header: NoteHeader) { + self.header = header; + } + + fn get_header(note: CardNote) -> NoteHeader { + note.header + } + + fn serialize_content(self) -> [Field; CARD_NOTE_LEN]{ + [self.owner.to_field()] + } + + fn deserialize_content(serialized_note: [Field; CARD_NOTE_LEN]) -> Self { + AddressNote { + owner: AztecAddress::from_field(serialized_note[0]), + header: NoteHeader::empty(), + } + } + + // Broadcasts the note as an encrypted log on L1. + fn broadcast(self, context: &mut PrivateContext, slot: Field) { + let encryption_pub_key = get_public_key(self.owner); + emit_encrypted_log( + context, + (*context).this_address(), + slot, + encryption_pub_key, + self.serialize(), + ); + } +} +``` + +Public state must implement Serialize and Deserialize traits. + +It is still possible to manually implement the storage initialization (for custom storage wrappers or internal types that don't implement the required traits). For the above example, the `impl Storage` section would look like this: + +```rust +impl Storage { + fn init(context: Context) -> Self { + Storage { + leader: PublicMutable::new( + context, + 1 + ), + legendary_card: PrivateMutable::new(context, 2), + profiles: Map::new( + context, + 3, + |context, slot| { + PrivateMutable::new(context, slot) + }, + ), + test: Set::new(context, 4), + imm_singleton: PrivateImmutable::new(context, 4), + } + } +} +``` + +## 0.20.0 + +### [Aztec.nr] Changes to `NoteInterface` + +1. Changing `compute_nullifier()` to `compute_nullifier(private_context: PrivateContext)` + + This API is invoked for nullifier generation within private functions. When using a secret key for nullifier creation, retrieve it through: + + `private_context.request_nullifier_secret_key(account_address)` + + The private context will generate a request for the kernel circuit to validate that the secret key does belong to the account. + + Before: + + ```rust + pub fn compute_nullifier(self) -> Field { + let secret = oracle.get_secret_key(self.owner); + pedersen_hash([ + self.value, + secret.low, + secret.high, + ]) + } + ``` + + Now: + + ```rust + pub fn compute_nullifier(self, context: &mut PrivateContext) -> Field { + let secret = context.request_nullifier_secret_key(self.owner); + pedersen_hash([ + self.value, + secret.low, + secret.high, + ]) + } + ``` + +2. New API `compute_nullifier_without_context()`. + + This API is used within unconstrained functions where the private context is not available, and using an unverified nullifier key won't affect the network or other users. For example, it's used in `compute_note_hash_and_nullifier()` to compute values for the user's own notes. + + ```rust + pub fn compute_nullifier_without_context(self) -> Field { + let secret = oracle.get_nullifier_secret_key(self.owner); + pedersen_hash([ + self.value, + secret.low, + secret.high, + ]) + } + ``` + + > Note that the `get_secret_key` oracle API has been renamed to `get_nullifier_secret_key`. + +## 0.18.0 + +### [Aztec.nr] Remove `protocol` from Nargo.toml + +The `protocol` package is now being reexported from `aztec`. It can be accessed through `dep::aztec::protocol`. + +```toml +aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="yarn-project/aztec-nr/aztec" } +``` + +### [Aztec.nr] key type definition in Map + +The `Map` class now requires defining the key type in its declaration which _must_ implement the `ToField` trait. + +Before: + +```rust +struct Storage { + balances: Map<PublicMutable<Field, FIELD_SERIALIZED_LEN>> +} + +let user_balance = balances.at(owner.to_field()) +``` + +Now: + +```rust +struct Storage { + balances: Map<AztecAddress, PublicState<Field, FIELD_SERIALIZED_LEN>> +} + +let user_balance = balances.at(owner) +``` + +### [js] Updated function names + +- `waitForSandbox` renamed to `waitForPXE` in `@aztec/aztec.js` +- `getSandboxAccountsWallets` renamed to `getInitialTestAccountsWallets` in `@aztec/accounts/testing` + +## 0.17.0 + +### [js] New `@aztec/accounts` package + +Before: + +```js +import { getSchnorrAccount } from "@aztec/aztec.js"; // previously you would get the default accounts from the `aztec.js` package: +``` + +Now, import them from the new package `@aztec/accounts` + +```js +import { getSchnorrAccount } from "@aztec/accounts"; +``` + +### Typed Addresses + +Address fields in Aztec.nr now is of type `AztecAddress` as opposed to `Field` + +Before: + +```rust +unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, serialized_note: [Field; VALUE_NOTE_LEN]) -> [Field; 4] { + let note_header = NoteHeader::new(_address, nonce, storage_slot); + ... +``` + +Now: + +```rust +unconstrained fn compute_note_hash_and_nullifier( + contract_address: AztecAddress, + nonce: Field, + storage_slot: Field, + serialized_note: [Field; VALUE_NOTE_LEN] + ) -> pub [Field; 4] { + let note_header = NoteHeader::new(contract_address, nonce, storage_slot); +``` + +Similarly, there are changes when using aztec.js to call functions. + +To parse a `AztecAddress` to BigInt, use `.inner` +Before: + +```js +const tokenBigInt = await bridge.methods.token().simulate(); +``` + +Now: + +```js +const tokenBigInt = (await bridge.methods.token().simulate()).inner; +``` + +### [Aztec.nr] Add `protocol` to Nargo.toml + +```toml +aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="yarn-project/aztec-nr/aztec" } +protocol = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="yarn-project/noir-protocol-circuits/crates/types"} +``` + +### [Aztec.nr] moving compute_address func to AztecAddress + +Before: + +```rust +let calculated_address = compute_address(pub_key_x, pub_key_y, partial_address); +``` + +Now: + +```rust +let calculated_address = AztecAddress::compute(pub_key_x, pub_key_y, partial_address); +``` + +### [Aztec.nr] moving `compute_selector` to FunctionSelector + +Before: + +```rust +let selector = compute_selector("_initialize((Field))"); +``` + +Now: + +```rust +let selector = FunctionSelector::from_signature("_initialize((Field))"); +``` + +### [js] Importing contracts in JS + +Contracts are now imported from a file with the type's name. + +Before: + +```js +import { TokenContract } from "@aztec/noir-contracts/types"; +``` + +Now: + +```js +import { TokenContract } from "@aztec/noir-contracts/Token"; +``` + +### [Aztec.nr] Aztec example contracts location change in Nargo.toml + +Aztec contracts are now moved outside of the `src` folder, so you need to update your imports. + +Before: + +```rust +easy_private_token_contract = {git = "https://github.com/AztecProtocol/aztec-packages/", tag ="v0.16.9", directory = "noir-projects/noir-contracts/contracts/easy_private_token_contract"} +``` + +Now, just remove the `src` folder,: + +```rust +easy_private_token_contract = {git = "https://github.com/AztecProtocol/aztec-packages/", tag ="v0.17.0", directory = "noir-projects/noir-contracts/contracts/easy_private_token_contract"} +``` diff --git a/docs/lines/v6/docs-developers/docs/resources/video_lessons.mdx b/docs/lines/v6/docs-developers/docs/resources/video_lessons.mdx new file mode 100644 index 00000000000..3200aa8537a --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/resources/video_lessons.mdx @@ -0,0 +1,54 @@ +--- +title: Video lessons +description: "Learn Aztec through short video explainers covering what Aztec is, private and public state, private composability, and getting started." +tags: [videos, learning] +sidebar_position: 0 +--- + +import YouTubeEmbed from "@site/src/components/YouTubeEmbed"; + +Prefer watching to reading? These short explainers, presented by Ciara Nightingale from the Aztec team, each cover a core Aztec concept in just a few minutes. Written pages that go deeper are linked below each video. + +## What is Aztec? + +Aztec is a decentralized Layer 2 on Ethereum for building unstoppable private apps: a zero-knowledge rollup where smart contracts can have both public and private state, and private execution happens locally on your own device. This video explains the core idea in under 90 seconds. + +<YouTubeEmbed videoId="urcBvo2QJp0" title="What is Aztec: Explained in Under 90 Seconds" /> + +Related reading: [Aztec overview](../../overview.md), [foundational topics](../foundational-topics/index.md) + +## Private and public state in one transaction + +A single Aztec transaction can span private and public execution. Using a private voting contract as the example, this video shows how private execution runs first on your device, producing a proof and side effects (nullifiers, note commitments, and enqueued public calls) that the sequencer then applies in public, keeping your vote private while the tally stays public. + +<YouTubeEmbed videoId="MayopgQ1FjI" title="One Transaction, Two Worlds: Private and Public State on Aztec" /> + +Related reading: [transactions](../foundational-topics/transactions.md), [state management](../foundational-topics/state_management.md) + +## What is private composability? + +On Aztec, smart contracts can call each other privately. Because transactions execute and prove locally, not only the state but the call stack itself can stay private: nobody watching the chain learns which contract called which. This video explains how that lets you build on top of other apps permissionlessly, just like Ethereum, without leaking what you are doing. + +<YouTubeEmbed videoId="idxRuGQnQKs" title="What is Private Composability? An Aztec Explainer" /> + +Related reading: [call types](../foundational-topics/call_types.md), [calling other contracts](../aztec-nr/framework-description/calling_contracts.md) + +## How authorization works (authwits) + +Authentication witnesses (authwits) are Aztec's generalized alternative to Ethereum's approve and transferFrom pattern: they authorize a specific action for a specific caller, work in both private and public execution, and prevent replay. This lesson walks through the message hash structure, the private and public flows, and the `#[authorize_once]` macro. + +<YouTubeEmbed videoId="VRZVOCdjGZ4" title="How Authorization Works on Aztec" /> + +Related reading: [authentication witness concepts](../foundational-topics/advanced/authwit.md), [using authwits in aztec.nr](../aztec-nr/framework-description/authentication_witnesses.md) + +## Get started in under 60 seconds + +Ready to build? This video walks through installing the Aztec tooling, creating a new contract project, compiling it, and deploying it to a local network, all in under a minute. + +<YouTubeEmbed videoId="_jgHNdNgFOg" title="Get Started on Aztec in Under 60 Seconds" /> + +Related reading: [getting started on a local network](../../getting_started_on_local_network.md) + +## More videos + +For a full-length course and more explainers, visit the [Aztec Network YouTube channel](https://www.youtube.com/@aztecnetwork). diff --git a/docs/lines/v6/docs-developers/docs/tutorials/_category_.json b/docs/lines/v6/docs-developers/docs/tutorials/_category_.json new file mode 100644 index 00000000000..ef8209bd2fa --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Tutorials", + "position": 1, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/_category_.json b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/_category_.json new file mode 100644 index 00000000000..852419cf559 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Contract Tutorials", + "position": 1, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md new file mode 100644 index 00000000000..8835665d878 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md @@ -0,0 +1,167 @@ +--- +title: Counter contract +description: Code-along tutorial for creating a simple counter contract on Aztec. +sidebar_position: 0 +references: ["docs/examples/contracts/counter_contract/src/main.nr"] +--- + +import Image from "@theme/IdealImage"; + +In this guide, we will create our first Aztec.nr smart contract. We will build a simple private counter, where each account keeps its own counter as encrypted private state, so the count stays known only to you. This contract will get you started with the basic setup and syntax of Aztec.nr, but doesn't showcase all of the awesome stuff Aztec is capable of. + +This tutorial is compatible with the Aztec version `#include_aztec_version`. Install the correct version with `VERSION=#include_version_without_prefix bash -i <(curl -sL https://install.aztec.network/#include_version_without_prefix)`. Or if you'd like to use a different version, you can find the relevant tutorial by clicking the version dropdown at the top of the page. + +## Prerequisites + +- You have followed the [quickstart](../../../getting_started_on_local_network.md) +- Running Aztec local network +- Installed [Noir LSP](../../aztec-nr/installation.md) (optional) + +## Set up a project + +Run this to create a new contract project: + +```bash +aztec new counter +``` + +Your structure should look like this: + +```tree +. +|-counter +| |-Nargo.toml <-- workspace root +| |-counter_contract +| | |-src +| | | |-main.nr +| | |-Nargo.toml <-- contract package config +| |-counter_test +| | |-src +| | | |-lib.nr +| | |-Nargo.toml <-- test package config +``` + +The `aztec new` command creates a two-crate workspace: a `counter_contract` crate for your contract and a `counter_test` crate for tests. The file `counter_contract/src/main.nr` will soon turn into our smart contract! + +Add the following dependency to `counter_contract/Nargo.toml` under the existing `aztec` dependency: + +```toml +[dependencies] +aztec = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="aztec" } +balance_set = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec_version", directory="balance-set" } +``` + +## Define the functions + +Go to `counter_contract/src/main.nr`, and replace the boilerplate code with this contract initialization: + +```rust +#include_code setup /docs/examples/contracts/counter_contract/src/main.nr raw +} +``` + +This defines a contract called `Counter`. + +:::note Clear the scaffold's placeholder test +The scaffolded `counter_test/src/lib.nr` imports the default contract name (`Main`) we just renamed to `Counter`, so it now fails to compile. Tests aren't used in this tutorial, so replace its contents with a single-line stub to keep `aztec compile` clean: + +```rust +// Tests are out of scope for this tutorial. See https://docs.aztec.network/developers/docs/aztec-nr/testing_contracts for examples. +``` +::: + +## Imports + +We need to define some imports. + +Write this inside your contract, ie inside these brackets: + +```rust +pub contract Counter { + // imports go here! +} +``` + +#include_code imports /docs/examples/contracts/counter_contract/src/main.nr rust + +- `macros::{functions::{external, initializer}, storage::storage}` + Imports the macros needed to define function types (`external`, `initializer`) and the `storage` macro for declaring contract storage structures. + +- `messages::delivery::MessageDelivery` + Imports `MessageDelivery` for specifying how note delivery should be handled (e.g., constrained onchain delivery). + +- `oracle::logging::debug_log_format` + Imports a debug logging utility for printing formatted messages during contract execution. + +- `protocol::{address::AztecAddress, traits::ToField}` + Brings in `AztecAddress` (used to identify accounts/contracts) and traits for converting values to field elements, necessary for serialization and formatting inside Aztec. + +- `state_vars::Owned` + Brings in `Owned`, a wrapper for state variables that have a single owner. + +- `use balance_set::BalanceSet` + Imports `BalanceSet` from the `balance_set` dependency, which provides functionality for managing private balances (used for our counter). + +## Declare storage + +Add this below the imports. It declares the storage variables for our contract. We use an `Owned` state variable wrapping a `BalanceSet` to manage private balances for each owner. + +#include_code storage_struct /docs/examples/contracts/counter_contract/src/main.nr rust + +## Keep the counter private + +Now we’ve got a mechanism for storing our private state, we can start using it to ensure the privacy of balances. + +Let’s create a constructor method to run on deployment that assigns an initial count to a specified owner. We name it `constructor` here, but the name is arbitrary; it is the `#[initializer]` decorator that marks it to run once when the contract is deployed. Write this: + +#include_code constructor /docs/examples/contracts/counter_contract/src/main.nr rust + +This function accesses the counters from storage. It adds the `initial_value` to the `owner`'s counter using `at().add()`, then calls `.deliver(MessageDelivery::onchain_constrained())` to ensure the note is delivered onchain. + +We have annotated this and other functions with `#[external("private")]` which are ABI macros so the compiler understands it will handle private inputs. + +## Incrementing our counter + +Now let's implement an `increment` function to increase the counter. + +#include_code increment /docs/examples/contracts/counter_contract/src/main.nr rust + +The `increment` function works similarly to the `constructor`. It logs a debug message, then adds 1 to the `owner`'s counter and delivers the note onchain. + +## Getting a counter + +The last thing we need to implement is a function to retrieve a counter value. + +#include_code get_counter /docs/examples/contracts/counter_contract/src/main.nr rust + +This is a `utility` function used to obtain the counter value outside of a transaction. We access the `owner`'s balance from the `counters` storage variable using `at(owner)`, then call `balance_of()` to retrieve the current count. This yields a private counter that only the owner can decrypt. + +## Compile + +Now we've written a simple Aztec.nr smart contract, we can compile it. + +### Compile the smart contract + +In the `./counter/` directory, run: + +```bash +aztec compile +``` + +This command compiles your Noir contract and creates a `target` folder with a `.json` artifact inside. + +After compiling, you can generate a TypeScript class using the `aztec codegen` command. + +In the same directory, run this: + +```bash +aztec codegen -o src/artifacts target +``` + +You can now use the artifact and/or the TS class in your Aztec.js! + +## Next steps + +### Optional: learn more about concepts mentioned here + +- [Functions and annotations like `#[external("private")]`](../../aztec-nr/framework-description/functions/function_transforms.md#private-functions) diff --git a/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md new file mode 100644 index 00000000000..5ba204338e2 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md @@ -0,0 +1,706 @@ +--- +title: "Verify Noir Proofs in Aztec Contracts" +description: "Learn to generate offchain ZK proofs with Noir and verify them onchain in Aztec private smart contracts" +references: ["docs/examples/circuits/hello_circuit/*", "docs/examples/contracts/recursive_verification_contract/*", "docs/examples/ts/recursive_verification/*"] +--- + +## Overview + +In this tutorial, you will build a system that generates zero-knowledge proofs offchain using a Noir circuit and verifies them onchain within an Aztec Protocol smart contract. You will create a simple circuit that proves two values are not equal, generate an UltraHonk proof, deploy an Aztec contract that stores a verification key hash, and submit the proof for onchain verification. This pattern enables trustless computation where anyone can verify that a computation was performed correctly without revealing the private inputs. + +:::note Why "Recursive" Verification? +This is called "recursive" verification because the proof is verified inside an Aztec private function, which itself gets compiled into a ZK circuit. The result is a proof being verified inside another proof. The Noir circuit you write is not recursive; the recursion happens at the Aztec protocol level when the private function execution (including the `verify_honk_proof` call) is proven. +::: + +:::tip Full Working Example +The complete code for this tutorial is available in the [docs/examples](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples) directory. Clone it to follow along or use it as a reference. +::: + +## Prerequisites + +Before starting, ensure you have the following installed and configured: + +- Node.js (v22 or later) +- yarn package manager +- Aztec CLI (version #include_aztec_version) +- Nargo +- Familiarity with [Noir syntax](https://noir-lang.org/docs) and [Aztec contract basics](../../aztec-nr/index.md) + +Install the required tools: + +```bash +# Install Aztec CLI +VERSION=#include_version_without_prefix bash -i <(curl -sL https://install.aztec.network/#include_version_without_prefix) +``` + +## Part 1: Understanding the Architecture + +### The Core Problem + +Aztec contracts have inherent [limits on function inputs and transaction complexity](../../resources/considerations/limitations.md#circuit-limitations). These constraints stem from the circuit-based nature of private execution. When your computation requires more inputs than these limits allow, or when the computation itself is too complex to fit in a single function, **recursive proof verification** provides an escape hatch. + +For example, consider a machine learning inference that needs 10,000 input features, or a Merkle tree verification with 1,000 leaves. These cannot fit within a single Aztec function's input constraints. Instead, you can: + +1. Perform the computation offchain in a vanilla Noir circuit with no input limits +2. Generate a proof of correct execution +3. Verify only the proof onchain (115 fields for VK + 458 fields for proof + N public inputs) + +This pattern transforms arbitrarily large computations into fixed-size proof verification. + +### Data Flow + +The recursive verification pattern follows this data flow: + +1. **Circuit Definition**: Write a Noir circuit that defines the computation you want to prove +2. **Compilation**: Compile the circuit with `aztec-nargo compile` (or your own `nargo compile` install) to produce bytecode +3. **Proof Generation**: Execute the circuit offchain and generate an UltraHonk proof using [Barretenberg](https://github.com/AztecProtocol/barretenberg) +4. **Onchain Verification**: Submit the proof to an Aztec contract that verifies it using the stored [verification key](../../resources/glossary.md#verification-key) hash + +```mermaid +flowchart LR + A["Noir Circuit<br/>(main.nr)"] --> B["Compiled<br/>Bytecode"] + B --> C["Proof + VK +<br/>Public Inputs"] + C --> D["Aztec Contract<br/>verify_honk_proof"] +``` + +**Why this separation matters**: The circuit defines _what_ you're proving. The proof is _evidence_ that you executed the circuit correctly with valid inputs. The onchain verifier checks the evidence without re-running the computation. This is what makes ZK proofs powerful: verification is orders of magnitude cheaper than computation. + +### Why Verify Proofs in Aztec Contracts? + +Proof verification enables several patterns: + +- **Bypassing Input Limits**: Aztec private functions have strict input constraints. A proof verification call uses 574 fields (115 VK + 458 proof + 1 public input), but can attest to computations with arbitrarily many inputs. For example, proving membership in a set of 10,000 elements becomes a fixed-size verification. + +- **Cross-System Verification**: Verify proofs generated by external Noir circuits within your Aztec application. This enables composability: your contract can trust computations performed by other systems without those systems needing to be Aztec-native. + +- **Batching Operations**: Aggregate multiple operations into a single proof. Instead of making N separate contract calls, prove all N operations were done correctly and verify once. + +### Why Use Aztec for Proof Verification? + +Aztec provides a unique advantage: **private function execution**. When you verify a proof in an Aztec private function: + +1. The proof verification happens inside a zero-knowledge circuit +2. The inputs to verification (the proof itself) can remain private +3. You can compose proof verification with other private operations + +This enables patterns impossible on transparent blockchains, like proving you have a valid credential without revealing which credential or when you obtained it. + +### UX Considerations: Multiple Proof Generation + +When using [recursive verification](https://noir-lang.org/docs/libraries/standard_library/recursion) in Aztec, users experience **two distinct proof generation phases**: + +1. **Noir Proof Generation** (application-specific): + - Happens before interacting with the Aztec contract + - Proves the computation (e.g., "I know values x and y where x ≠ y") + - Time depends on circuit complexity (seconds to minutes) + - Produces the proof and verification key that will be verified + +2. **Aztec Transaction Proof** (protocol-level): + - Generated by the [PXE](../../foundational-topics/pxe/index.md) when calling the private function + - Proves correct execution of the Aztec contract (including the `verify_honk_proof` call) + +With this foundation in mind, let's build a complete example. You'll create a Noir circuit, generate a proof, and verify it inside an Aztec contract. + +## Part 2: Writing the Noir Circuit + +Start by writing a simple circuit that proves two field values are not equal. This minimal example demonstrates the core pattern—you can extend it for more complex computations like Merkle proofs, credential verification, or something else entirely. + +### Create the Circuit Project + +Use `aztec-nargo new` to generate the project structure (the Aztec installer ships `nargo` as `aztec-nargo`; substitute your own `nargo` if its version matches `aztec-nargo --version`): + +```bash +aztec-nargo new circuit +``` + +This creates the following structure: + +```tree +circuit/ +├── src/ +│ └── main.nr # Circuit code +└── Nargo.toml # Circuit configuration +``` + +### Circuit Code + +Replace the contents of `circuit/src/main.nr` with: + +#include_code circuit /docs/examples/circuits/hello_circuit/src/main.nr rust + +This is intentionally minimal to focus on the verification pattern. In production, you would replace `assert(x != y)` with meaningful computations like: + +- Merkle tree membership proofs +- Hash preimage verification +- Range proofs (proving a value is within bounds) +- Credential verification +- Email verification (proving you received an email from a domain without revealing its contents, like [zkEmail](https://www.prove.email/)) + +### Understanding Private vs Public Inputs + +The circuit has two inputs with different visibility: + +- `x: u64` - A **private input** known only to the prover. This value is never revealed onchain or included in the proof data. The verifier cannot determine what value was used—only that _some_ valid value exists. + +- `y: pub u64` - A **public input** that is visible to the verifier. This value is included in the proof data, but since proof verification happens within a private function, it isn't exposed onchain unless you explicitly reveal it. + +**Why this distinction matters**: The circuit asserts that `x != y`. The prover demonstrates they know a secret value `x` that differs from the public value `y`. + +Public inputs don't have to come from the caller. During verification, the Aztec contract can read values from its own storage and use them as public inputs. This pattern ties the proof to contract state—the prover must generate a proof against the _current_ stored value and cannot substitute a different public input. + +To make the "public input" truly public, the contract developer can enqueue a public function call from the private function that verifies the proof, passing the public input to a public function to be logged or verified against public state. + +For example, you could create a zkpassport proof demonstrating that you are over a certain age. The proof is verified in a private function, then the age (the public input) is passed to a public function where it's compared against a mutable threshold in public storage. + +### Circuit Configuration + +Update `circuit/Nargo.toml` (see [Noir crates and packages](https://noir-lang.org/docs/project_structure/crates_and_packages) for more details): + +#include_code circuit_nargo_toml /docs/examples/circuits/hello_circuit/Nargo.toml toml + +**Note**: This is a vanilla Noir circuit, not an Aztec contract. It has `type = "bin"` (binary) and no Aztec dependencies. The circuit is compiled with `nargo`, not `aztec compile`. This distinction is important—you can verify proofs from _any_ Noir circuit inside Aztec contracts. + +### Compile the Circuit + +```bash +cd circuit +aztec-nargo compile +``` + +This generates `target/hello_circuit.json` containing: + +- **Bytecode**: The compiled circuit representation +- **ABI (Application Binary Interface)**: Describes the circuit's inputs and outputs, including which are public + +The TypeScript code uses the ABI to correctly format inputs during witness generation. + +### Test the Circuit + +```bash +aztec-nargo test +``` + +Expected output: + +```text +[hello_circuit] Running 1 test function +[hello_circuit] Testing test_main ... ok +[hello_circuit] 1 test passed +``` + +**Tip**: Circuit tests run without generating proofs, making them fast for development. Use them to verify your circuit logic before the more expensive proof generation step. + +## Part 3: Writing the Aztec Contract + +The Aztec contract stores the verification key hash and verifies proofs submitted by users. When a valid proof is submitted, it increments a counter for the caller. + +### Why This Contract Design? + +The contract demonstrates several important patterns: + +1. **VK Hash Storage**: Instead of storing the full 115-field verification key onchain (expensive), we store only its hash (1 field). The prover submits the full VK with each proof, and the contract verifies it matches the stored hash. + +2. **Private-to-Public Flow**: Proof verification happens in a [private function](../../aztec-nr/framework-description/functions/visibility.md) (generating a ZK proof of the verification), but the counter update happens in a public function (visible state change). This separation is fundamental to Aztec's architecture. + +3. **Self-Only Public Functions**: The `_increment_public` function can only be called by the contract itself, not external accounts (similar to `internal` functions in Solidity). This ensures the counter can only be modified after successful proof verification. + +### Create the Contract Project + +Use `aztec new` to generate the workspace: + +```bash +aztec new ValueNotEqual +``` + +This creates a two-crate workspace under `ValueNotEqual/`: the contract crate is named `ValueNotEqual_contract` and the test crate is named `ValueNotEqual_test`, both derived from the positional argument. The Noir `contract` identifier declared inside `main.nr` is independent of the crate name and determines the compiled artifact filename. + +```tree +ValueNotEqual/ +├── Nargo.toml # [workspace] members +├── ValueNotEqual_contract/ +│ ├── src/ +│ │ └── main.nr # Contract code +│ └── Nargo.toml # Contract package (type = "contract") +└── ValueNotEqual_test/ + ├── src/ + │ └── lib.nr # Noir tests + └── Nargo.toml # Test package (type = "lib") +``` + +### Contract Configuration + +Update `ValueNotEqual/ValueNotEqual_contract/Nargo.toml` with the required dependencies: + +```toml +[package] +name = "ValueNotEqual_contract" +type = "contract" +authors = ["[YOUR_NAME]"] + +[dependencies] +aztec = { git = "https://github.com/aztec-labs-eng/aztec-nr/", tag = "#include_aztec_version", directory = "aztec" } +bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "#include_foundation_version", directory = "barretenberg/noir/bb_proof_verification" } +``` + +**Key differences from the circuit's Nargo.toml** (in `ValueNotEqual/ValueNotEqual_contract/Nargo.toml`): + +- `type = "contract"` (not `"bin"`) +- Depends on `aztec` for Aztec-specific features +- Depends on `bb_proof_verification` for `verify_honk_proof` + +### Contract Structure + +Replace the contents of `ValueNotEqual/ValueNotEqual_contract/src/main.nr` with: + +#include_code full_contract /docs/examples/contracts/recursive_verification_contract/src/main.nr rust + +:::note Clear the scaffold's placeholder test +The scaffolded `ValueNotEqual/ValueNotEqual_test/src/lib.nr` imports the default contract name (`Main`) we just replaced above, so it now fails to compile. Tests aren't used in this tutorial — replace its contents with a single-line stub so `aztec compile` stays clean: + +```rust +// Tests are out of scope for this tutorial. See https://docs.aztec.network/developers/docs/aztec-nr/testing_contracts for examples. +``` +::: + +### Storage Variables Explained + +The contract uses two [storage types](../../aztec-nr/framework-description/state_variables.md) with different characteristics: + +**`vk_hash: PublicImmutable<Field>`** + +`PublicImmutable` is perfect for values that: + +- Are set once during contract initialization +- Never change after deployment +- Need to be readable from both public and private contexts + +The VK hash fits all these criteria. Once you deploy a contract to verify proofs from a specific circuit, the circuit (and thus its VK) shouldn't change. + +**Why store the hash instead of the full VK?** + +- Storage costs: 1 field vs 115 fields +- The prover already has the full VK (needed to generate the proof) +- Hash verification is cheap compared to storing/loading 115 fields + +**`counters: Map<AztecAddress, PublicMutable<Field>>`** + +`PublicMutable` is used for values that: + +- Change over time +- Are updated by public functions +- Need to be visible onchain + +The counter must be `PublicMutable` because it's modified by `_increment_public`, a public function. Private functions cannot directly write to public state; they can only enqueue public function calls. + +### Function Breakdown + +**1. `constructor` (public initializer)** + +```rust +#[initializer] +#[external("public")] +fn constructor(headstart: Field, owner: AztecAddress, vk_hash: Field) { + self.storage.counters.at(owner).write(headstart); + self.storage.vk_hash.initialize(vk_hash); +} +``` + +- `#[initializer]`: Marks this as the constructor, called once during deployment +- `#[external("public")]`: Executes publicly (visible onchain) +- Sets the initial counter value for the owner +- Stores the VK hash using `initialize()` (required for `PublicImmutable`) + +**2. `increment` (private function)** + +```rust +#[external("private")] +fn increment( + owner: AztecAddress, + verification_key: UltraHonkVerificationKey, + proof: UltraHonkZKProof, + public_inputs: [Field; 1], +) { + let vk_hash = self.storage.vk_hash.read(); + verify_honk_proof(verification_key, proof, public_inputs, vk_hash); + self.enqueue_self._increment_public(owner); +} +``` + +- `#[external("private")]`: Executes privately (generates a ZK proof of execution in the PXE) +- Reads VK hash from storage (allowed because `PublicImmutable` is readable in private context) +- Calls `verify_honk_proof()` which: + - Computes the hash of the provided verification key + - Checks it matches the stored `vk_hash` + - Verifies the proof against the VK and public inputs + - Fails (reverts) if any check fails +- Uses `enqueue_self._increment_public(owner)` to schedule a public function call + +**Why `enqueue_self` instead of a direct call?** + +In Aztec, private functions cannot directly modify public state. Instead, they enqueue public function calls that execute after the private phase completes. This ensures: + +- Private execution remains private (no public state reads during private execution) +- State updates are atomic (all enqueued calls execute or none do) +- The execution order is deterministic + +**3. `_increment_public` (public, self-only)** + +```rust +#[only_self] +#[external("public")] +fn _increment_public(owner: AztecAddress) { + let current = self.storage.counters.at(owner).read(); + self.storage.counters.at(owner).write(current + 1); +} +``` + +- `#[only_self]`: Only callable by the contract itself (via `enqueue_self`) +- `#[external("public")]`: Executes publicly +- Reads the current counter and increments it + +**Why `#[only_self]`?** + +Without this modifier, anyone could call `_increment_public` directly, bypassing proof verification. The `#[only_self]` modifier ensures the function is only reachable through the private `increment` function, which requires a valid proof. + +**4. `get_counter` (public view)** + +```rust +#[view] +#[external("public")] +fn get_counter(owner: AztecAddress) -> Field { + self.storage.counters.at(owner).read() +} +``` + +- `#[view]`: Read-only function, doesn't modify state +- Returns the counter value for any address + +## Part 4: TypeScript Setup and Proof Generation + +Before compiling the contract or running any TypeScript scripts, set up the project with the necessary configuration files and dependencies. + +### Project Setup + +Create the following files in your project root directory. + +**Create `package.json`:** + +```json +{ + "name": "recursive-verification-tutorial", + "type": "module", + "scripts": { + "ccc": "cd ValueNotEqual && aztec compile && aztec codegen target -o ../artifacts", + "data": "tsx scripts/generate_data.ts", + "recursion": "tsx index.ts" + }, + "dependencies": { + "@aztec-labs/accounts": "#include_version_without_prefix", + "@aztec-labs/aztec.js": "#include_version_without_prefix", + "@aztec-foundation/bb.js": "#include_foundation_version_without_prefix", + "@aztec-labs/kv-store": "#include_version_without_prefix", + "@aztec-labs/noir-contracts.js": "#include_version_without_prefix", + "@aztec-foundation/noir-noir_js": "#include_foundation_version_without_prefix", + "@aztec-labs/pxe": "#include_version_without_prefix", + "@aztec-labs/wallets": "#include_version_without_prefix", + "tsx": "^4.20.6" + }, + "devDependencies": { + "@types/node": "^22.0.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} +``` + +**Create `tsconfig.json`:** + +```json +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + } +} +``` + +**Install dependencies:** + +```bash +yarn install +``` + +This installs all the Aztec packages needed for proof generation and contract interaction. The installation may take a few minutes due to the size of the cryptographic libraries. + +### Compile the Contract + +Now compile the Aztec contract and generate TypeScript bindings: + +```bash +yarn ccc +``` + +**What this command does** (see [How to Compile a Contract](../../aztec-nr/compiling_contracts.md) for details): + +1. `aztec compile`: Compiles the Noir contract and post-processes it for Aztec (different from `nargo compile`) +2. `aztec codegen`: Generates TypeScript bindings from the contract artifact, enabling type-safe contract interaction + +This generates: + +- `ValueNotEqual/target/ValueNotEqual_contract-ValueNotEqual.json` - Contract artifact (bytecode, ABI, etc.) +- `artifacts/ValueNotEqual.ts` - TypeScript class for deploying and interacting with the contract + +### Proof Generation Script + +The proof generation script executes the circuit offchain and produces the proof data needed for onchain verification. + +Create `scripts/generate_data.ts`: + +```js +import circuitJson from "../circuit/target/hello_circuit.json" with { type: "json" }; +#include_code generate_data /docs/examples/ts/recursive_verification/scripts/generate_data.ts raw +``` + +### Understanding the Proof Generation Pipeline + +#### Setup + +- Initialize Barretenberg (the cryptographic backend) +- Load the compiled circuit + +#### Witness Generation + +```typescript +const { witness: mainWitness } = await helloWorld.execute({ x: 1, y: 2 }); +``` + +The witness contains all values computed during circuit execution, not just inputs and outputs, but every intermediate value. The prover needs the witness to construct the proof. The verifier never sees the witness (that's the point of ZK proofs). + +#### Proof Generation + +```typescript +const mainProofData = await mainBackend.generateProof(mainWitness, { + verifierTarget: "noir-recursive", +}); +``` + +**Why `verifierTarget: 'noir-recursive'`?** There are different proof formats optimized for different verifiers: + +- Native verifiers (standalone programs) +- Smart contract verifiers (Solidity) +- Recursive verifiers (inside other ZK circuits) + +Aztec contracts are compiled to ZK circuits, so `verify_honk_proof` runs inside a circuit. We need the recursive-friendly proof format. + +#### Local Verification + +```typescript +const isValid = await mainBackend.verifyProof(mainProofData, { + verifierTarget: "noir-recursive", +}); +``` + +Always verify locally before submitting onchain. Onchain verification costs gas/fees and takes time. Local verification is free and instant. + +#### Field Element Conversion + +ZK proofs are arrays of bytes, but Aztec contracts work with field elements. We convert the proof and VK to arrays of 115 and 458 field elements respectively. + +```typescript +let proofAsFields = recursiveArtifacts.proofAsFields; +if (proofAsFields.length === 0) { + console.log("Using deflattenFields to convert proof..."); + proofAsFields = deflattenFields(mainProofData.proof).map((f) => f.toString()); +} + +const vkAsFields = recursiveArtifacts.vkAsFields; +``` + +Some versions of the library return an empty `proofAsFields` array, requiring manual conversion via `deflattenFields`. + +#### Saving Data for Contract Interaction + +```typescript +const data = { + vkAsFields: vkAsFields, + vkHash: recursiveArtifacts.vkHash, + proofAsFields: proofAsFields, + publicInputs: mainProofData.publicInputs.map((p: string) => p.toString()), +}; + +fs.writeFileSync("data.json", JSON.stringify(data, null, 2)); +await barretenbergAPI.destroy(); +``` + +The data is saved as JSON so the deployment script can load it. We call `barretenbergAPI.destroy()` to clean up the WebAssembly resources used by Barretenberg. This is important because Barretenberg allocates significant memory for cryptographic operations, and not destroying it can cause memory leaks in long-running processes. + +### Run Proof Generation + +```bash +yarn data +``` + +Expected output: + +```text +Proof verification: SUCCESS +Using deflattenFields to convert proof... +VK size: 115 +Proof size: 458 +Public inputs: 1 +Done +``` + +### Output Format + +The generated `data.json` contains: + +```json +{ + "vkAsFields": ["0x...", "0x...", ...], // 115 field elements + "vkHash": "0x...", // Single field element + "proofAsFields": ["0x...", "0x...", ...], // 458 field elements + "publicInputs": ["2"] // The public input y=2 +} +``` + +**What each field is used for**: + +- `vkHash`: Passed to the contract constructor, stored permanently +- `vkAsFields`: Passed to `increment()`, verified against stored hash +- `proofAsFields`: Passed to `increment()`, verified by `verify_honk_proof` +- `publicInputs`: Passed to `increment()`, must match what was used during proof generation + +## Part 5: Deploying and Verifying + +The deployment script connects to the Aztec network, creates an account, deploys the contract, and submits a proof for verification. + +### Deployment Script + +Create `index.ts`: + +#include_code run_recursion /docs/examples/ts/recursive_verification/index.ts typescript + +### Understanding the Deployment Script + +#### Sponsored Fee Payment + +Aztec transactions require fees. For testing, we use a Sponsored Fee Payment Contract (FPC) that pays fees on behalf of users: + +```typescript +const sponsoredFPC = await getSponsoredFPCInstance(); +const sponsoredPaymentMethod = new SponsoredFeePaymentMethod( + sponsoredFPC.address, +); +``` + +In production, you would use real [fee payment methods](../../aztec-js/how_to_pay_fees.md) (native tokens, ERC20, etc.). + +#### What happens during `increment().send()` + +This single line triggers a complex flow: + +1. **Private Execution** (client-side, in PXE): + - Execute `increment()` with provided arguments + - Read `vk_hash` from contract storage + - Execute `verify_honk_proof()` inside the private function + - Generate the `enqueue_self._increment_public(owner)` call + +2. **Proof Generation** (client-side, in PXE): + - Generate a ZK proof that the private execution was correct + - This proof doesn't reveal inputs (including the 458-field proof!) + +3. **Transaction Submission**: + - Send the proof + encrypted logs + public function calls to the network + +4. **Verification & Public Execution** (onchain): + - Network verifies the private execution proof + - Execute `_increment_public(owner)` publicly + - Update the counter in storage + +### Supporting Utility + +Create `scripts/sponsored_fpc.ts`: + +#include_code sponsored_fpc /docs/examples/ts/recursive_verification/scripts/sponsored_fpc.ts typescript + +This utility computes the address of the pre-deployed sponsored FPC contract. The salt ensures we get the same address every time. For more information about fee payment options, see [Paying Fees](../../aztec-js/how_to_pay_fees.md). + +### Start the Local Network + +In a separate terminal, start the [Aztec local network](../../../getting_started_on_local_network.md): + +```bash +aztec start --local-network +``` + +**What this starts**: + +- **Anvil**: A local Ethereum node (L1) +- **Aztec Node**: The L2 rollup node +- **PXE**: Private eXecution Environment (embedded in node for local development) + +Wait for the network to fully initialize. You should see logs indicating readiness. The PXE will be available at `http://localhost:8080`. + +### Deploy and Verify + +Run the deployment script: + +```bash +yarn recursion +``` + +Expected output: + +```text +Contract deployed at: 0x... +Counter value: 10 +Counter value: 11 +``` + +The counter starts at 10 (set during deployment), and after successful proof verification, it increments to 11. This confirms that the Noir proof was verified inside the Aztec contract. + +## Quick Reference + +If you want to run all commands at once, or if you're starting fresh, here's the complete workflow. You can also reference the [full working example](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples) in the main repository. + +```bash +# Install dependencies (after creating package.json and tsconfig.json) +yarn install + +# Compile the Noir circuit +cd circuit && aztec-nargo compile && cd .. + +# Compile the Aztec contract and generate TypeScript bindings +yarn ccc + +# Generate proof data +yarn data + +# Start the local network (in a separate terminal) +aztec start --local-network + +# Deploy and verify +yarn recursion +``` + +## Next Steps + +Now that you understand the basics of proof verification in Aztec contracts, explore these topics: + +- **Simpler Contract Examples**: If you're new to Aztec contracts, the [Counter Tutorial](./counter_contract.md) provides a gentler introduction to contract development patterns. +- **Multiple Public Inputs**: Extend the circuit to have multiple public inputs. Update `public_inputs: [Field; 1]` in the contract to match. +- **Noir Language Reference**: Explore advanced Noir features like loops, arrays, and standard library functions at [noir-lang.org](https://noir-lang.org/docs). diff --git a/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/token_contract.md b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/token_contract.md new file mode 100644 index 00000000000..10083fa64ca --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/token_contract.md @@ -0,0 +1,475 @@ +--- +title: Private Token Contract +sidebar_position: 1 +tags: [privacy, tokens, intermediate] +description: Build a privacy-preserving token for employee mental health benefits that keeps spending habits confidential. +references: ["docs/examples/contracts/bob_token_contract/src/main.nr", "docs/examples/ts/bob_token_contract/index.ts"] +--- + +## The Privacy Challenge: Mental Health Benefits at Giggle + +Giggle (a fictional tech company) wants to support their employees' mental health by providing BOB tokens that can be spent at Bob's Psychology Clinic. However, employees have a crucial requirement: **complete privacy**. They don't want Giggle to know: + +- How many BOB tokens they've actually used +- When they're using mental health services +- Their therapy patterns or frequency + +In this tutorial, we'll build a token contract that allows Giggle to mint BOB tokens for employees while ensuring complete privacy in how those tokens are spent. + +## Prerequisites + +This is an intermediate tutorial that assumes you have: + +- Completed the [Counter Contract tutorial](./counter_contract.md) +- A Running Aztec local network (see the Counter tutorial for setup) +- Basic understanding of Aztec.nr syntax and structure +- Aztec toolchain installed (`VERSION=#include_version_without_prefix bash -i <(curl -sL https://install.aztec.network/#include_version_without_prefix)`) + +If you haven't completed the Counter Contract tutorial, please do so first as we'll skip the basic setup steps covered there. + +## What We're Building + +We'll create BOB tokens with: + +- **Public and Private minting**: Giggle can mint tokens in private or public +- **Public and Private transfers**: Employees can spend tokens at Bob's clinic with full privacy + +### Project Setup + +Let's create a simple yarn + aztec.nr project: + +```bash +aztec new bob_token +cd bob_token +yarn init -y +# This is to ensure yarn uses node_modules instead of pnp for dependency installation +yarn config set nodeLinker node-modules +yarn add @aztec-labs/aztec.js@#include_aztec_version @aztec-labs/accounts@#include_aztec_version @aztec-labs/kv-store@#include_aztec_version @aztec-labs/wallets@#include_aztec_version +``` + +## Contract structure + +The `aztec new` command created a workspace with two crates: a `bob_token_contract` crate for your smart contract code and a `bob_token_test` crate for Noir tests. In `bob_token_contract/src/main.nr` we have a proto-contract. Let's replace it with a simple starting point: + +```rust +#include_code start /docs/examples/contracts/bob_token_contract/src/main.nr raw + // We'll build the mental health token here +} +``` + +:::note Clear the scaffold's placeholder test +The scaffolded `bob_token_test/src/lib.nr` imports the default contract name (`Main`) we just replaced above, so it now fails to compile. Tests aren't used in this tutorial — replace its contents with a single-line stub so `aztec compile` stays clean: + +```rust +// Tests are out of scope for this tutorial. See https://docs.aztec.network/developers/docs/aztec-nr/testing_contracts for examples. +``` +::: + +The `#[aztec]` macro transforms our contract code to work with Aztec's privacy protocol. + +Make sure the Aztec.nr library is listed as a dependency in `bob_token_contract/Nargo.toml`: + +```toml +[package] +name = "bob_token_contract" +type = "contract" + +[dependencies] +aztec = { git = "https://github.com/aztec-labs-eng/aztec-nr/", tag = "#include_aztec_version", directory = "aztec" } +``` + +Since we're here, let's import more specific stuff from this library: + +```rust +#include_code imports /docs/examples/contracts/bob_token_contract/src/main.nr raw +} +``` + +These are the different macros we need to define the visibility of functions, and some handy types and functions. + +:::note +You may see "unused import" warnings from your IDE or compiler for `only_self`, `MessageDelivery`, and `Owned`. That's expected at this stage — we'll start using them in Part 2 when we add the private half of the contract. +::: + +## Building the Mental Health Token System + +### The Privacy Architecture + +Before we start coding, let's understand how privacy works in our mental health token system: + +1. **Public Layer**: Giggle mints tokens publicly - transparent and auditable +2. **Private Layer**: Employees transfer and spend tokens privately - completely confidential +3. **Cross-layer Transfer**: Employees can move tokens between public and private domains as needed + +This architecture ensures that while the initial allocation is transparent (important for corporate governance), the actual usage remains completely private. + +:::info Privacy Note +In Aztec, private state uses a UTXO model with "notes" - think of them as encrypted receipts that only the owner can decrypt and spend. When an employee receives BOB tokens privately, they get encrypted notes that only they can see and use. +::: + +Let's start building! Remember to import types as needed - your IDE's Noir extension can help with auto-imports. + +## Part 1: Public Minting for Transparency + +Let's start with the public components that Giggle will use to mint and track initial token allocations. + +### Setting Up Storage + +First, define the storage for our BOB tokens: + +```rust +#include_code public_storage /docs/examples/contracts/bob_token_contract/src/main.nr raw +} +``` + +This storage structure allows: + +- `owner`: Stores Giggle's admin address (who can mint tokens) +- `public_balances`: Tracks public token balances (employees can verify their allocations) + +:::tip Why Public Balances? +While employees want privacy when spending, having public balances during minting allows: + +1. Employees to verify they received their mental health benefits +2. Auditors to confirm fair distribution +3. Transparency in the allocation process + +::: + +### Initializing Giggle as Owner + +When deploying the contract, we need to set Giggle as the owner: + +#include_code setup /docs/examples/contracts/bob_token_contract/src/main.nr rust + +The `#[initializer]` decorator ensures this runs once during deployment. Only Giggle's address will have the power to mint new BOB tokens for employees. + +### Minting BOB Tokens for Employees + +Giggle needs a way to allocate mental health tokens to employees: + +#include_code mint_public /docs/examples/contracts/bob_token_contract/src/main.nr rust + +This public minting function: + +1. Verifies that only Giggle (the owner) is calling +2. Transparently adds tokens to the employee's public balance +3. Creates an auditable record of the allocation + +:::info Real-World Scenario +Imagine Giggle allocating 100 BOB tokens to each employee at the start of the year. This public minting ensures employees can verify they received their benefits, while their actual usage remains private. +::: + +### Public Transfers (Optional Transparency) + +While most transfers will be private, we'll add public transfers for cases where transparency is desired: + +#include_code transfer_public /docs/examples/contracts/bob_token_contract/src/main.nr rust + +This might be used when: + +- An employee transfers tokens to a colleague who's comfortable with transparency +- Bob's clinic makes a public refund +- Any scenario where privacy isn't required + +### Admin Transfer (Future-Proofing) + +In case Giggle's mental health program administration changes: + +#include_code transfer_ownership /docs/examples/contracts/bob_token_contract/src/main.nr rust + +## Your First Deployment - Let's See It Work + +### Compile Your Contract + +You've written enough code to have a working token! Let's compile and test it: + +```bash +aztec compile +``` + +### Generate TypeScript Interface + +```bash +aztec codegen target --outdir artifacts +``` + +You should now have a nice typescript interface in a new `artifacts` folder. Pretty useful! + +### Deploy and Test + +Create `index.ts`. We will connect to our running local network and its wallet, then deploy the test accounts and get three wallets out of it. Ensure that your local network is running: + +```bash +aztec start --local-network +``` + +Then we will use the `giggleWallet` to deploy our contract, mint 100 BOB to Alice, then transfer 10 of those to Bob's Clinic publicly... for now. Let's go: + +```typescript +#include_code imports /docs/examples/ts/bob_token_contract/index.ts raw + +#include_code checkpoint_1 /docs/examples/ts/bob_token_contract/index.ts raw +} + +main().catch(console.error); +``` + +Run your test: + +```bash +npx tsx index.ts +``` + +:::tip + +What's this `tsx` dark magic? `tsx` is a tool that compiles and runs TypeScript using reasonable defaults. `npx` will auto-install it if you don't have it. If you'd prefer to install it explicitly, run `yarn add -D tsx` first. + +::: + +:::tip Ephemeral PXE state + +We pass `{ ephemeral: true }` to `EmbeddedWallet.create`. This tells the PXE to keep its state in memory instead of writing it to `pxe_data_*` / `wallet_data_*` folders on disk. If you ever stop and restart your local network (or wipe its state), the next run starts clean instead of failing with errors like `No local block hash for block number …` because on-disk PXE state no longer matches the chain. + +For real applications you typically want persistent state, but for tutorials that spin up a fresh network each run, ephemeral is the safer default. + +::: + +### 🎉 Celebrate + +Congratulations! You've just deployed a working token contract on Aztec! You can: + +- ✅ Mint BOB tokens as Giggle +- ✅ Transfer tokens between employees +- ✅ Track balances publicly + +But there's a problem... **Giggle can see everything!** They know: + +- Who's transferring tokens +- How much is being spent +- When mental health services are being used + +This defeats the whole purpose of our mental health privacy initiative. Let's fix this by adding private functionality! + +## Part 2: Adding Privacy - The Real Magic Begins + +Now let's add the privacy features that make our mental health benefits truly confidential. + +### Understanding Private Notes + +Here's where Aztec's privacy magic happens. Unlike public balances (a single number), private balances are collections of encrypted "notes". Think of it this way: + +- **Public balance**: "Alice has 100 BOB tokens" (visible to everyone) +- **Private balance**: Alice has encrypted notes [Note1: 30 BOB, Note2: 50 BOB, Note3: 20 BOB] that only she can decrypt + +When Alice spends 40 BOB tokens at Bob's clinic: + +1. She consumes Note1 (30 BOB) and Note2 (50 BOB) = 80 BOB total +2. She creates a new note for Bob's clinic (40 BOB) +3. She creates a "change" note for herself (40 BOB) +4. The consumed notes are nullified (marked as spent) + +:::info What is a nullifier? +A **nullifier** is a unique, one-way tag emitted when a private note is spent. The network adds it to a nullifier tree so the same note can't be spent twice, but because the nullifier is derived from secrets only the note's owner knows, nobody can link a nullifier back to the note it invalidated. See [State Management](../../foundational-topics/state_management.md#private-state) for more. +::: + +In this case, all that the network sees (including Giggle) is just "something happening to some state in some contract". How cool is that? + +### Updating Storage for Privacy + +For something like balances, you can use a simple library called `balance_set` which abstracts away a custom private Note. A Note is at the core of how private state works in Aztec and you can read about it [here](../../foundational-topics/state_management.md). For now, let's add it by replacing the `[dependencies]` section in `Nargo.toml`: + +```toml +[dependencies] +aztec = { git="https://github.com/aztec-labs-eng/aztec-nr", tag="#include_aztec_version", directory="aztec" } +balance_set = { git = "https://github.com/aztec-labs-eng/aztec-nr/", tag = "#include_aztec_version", directory = "balance-set" } +``` + +Then import `BalanceSet` in our contract: + +```rust +use aztec::macros::aztec; + +#[aztec] +pub contract BobToken { + // ... other imports + use balance_set::BalanceSet; + // ... +} +``` + +We need to update the contract storage to have private balances as well: + +#include_code storage /docs/examples/contracts/bob_token_contract/src/main.nr rust + +The `private_balances` use `BalanceSet` which manages encrypted notes automatically. + +### Moving Tokens to Privateland + +Great, now our contract knows about private balances. Let's implement a method to allow users to move their publicly minted tokens there: + +#include_code public_to_private /docs/examples/contracts/bob_token_contract/src/main.nr rust + +And the helper function: + +#include_code _deduct_public_balance /docs/examples/contracts/bob_token_contract/src/main.nr rust + +By calling `public_to_private` we're telling the network "deduct this amount from my balance" while simultaneously creating a Note with that balance in privateland. + +### Private Transfers + +Now for the crucial privacy feature - transferring BOB tokens in privacy. This is actually pretty simple: + +#include_code transfer_private /docs/examples/contracts/bob_token_contract/src/main.nr rust + +This function simply nullifies the sender's notes, while adding them to the recipient. + +:::info Real-World Impact + +When an employee uses 50 BOB tokens at Bob's clinic, this private transfer ensures Giggle has no visibility into: + +- The fact that the employee is seeking mental health services +- The frequency of visits +- The amount spent on treatment + +::: + +### Checking Balances + +Employees can check their BOB token balances without hitting the network by using utility unconstrained functions: + +#include_code check_balances /docs/examples/contracts/bob_token_contract/src/main.nr rust + +## Part 3: Securing Private Minting + +Let's make this a little bit harder, and more interesting. Let's say Giggle doesn't want to mint the tokens in public. Can we have private minting on Aztec? + +Sure we can. Let's see. + +### Understanding Execution Domains + +Our BOB token system operates in two domains: + +1. **Public Domain**: Where Giggle mints tokens transparently +2. **Private Domain**: Where employees spend tokens confidentially + +The key challenge: How do we ensure only Giggle can mint tokens when the minting happens in a private function? + +:::warning Privacy Trade-off + +Private functions can't directly read current public state (like who the owner is). They can only read historical public state or enqueue public function calls for validation. + +::: + +### The Access Control Challenge + +We want Giggle to mint BOB tokens directly to employees' private balances (for maximum privacy), but we need to ensure only Giggle can do this. The challenge: ownership is stored publicly, but private functions can't read current public state. + +Let's use a clever pattern where private functions enqueue public validation checks. First we make a little helper function in public. Remember, public functions always run _after_ private functions, since private functions run client-side. + +#include_code _assert_is_owner /docs/examples/contracts/bob_token_contract/src/main.nr rust + +Now we can add a secure private minting function. It looks pretty easy, and it is, since the whole thing will revert if the public function fails: + +#include_code mint_private /docs/examples/contracts/bob_token_contract/src/main.nr rust + +This pattern ensures: + +1. The private minting executes first (creating the proof) +2. The public ownership check executes after +3. If the check fails, the entire transaction (including the private part) reverts +4. Only Giggle can successfully mint BOB tokens + +## Part 4: Converting Back to Public + +For the sake of completeness, let's also have a function that brings the tokens back to publicland: + +#include_code private_to_public /docs/examples/contracts/bob_token_contract/src/main.nr rust + +Now you've made changes to your contract, you need to recompile your contract. + +Here are the steps from above, for reference: + +```bash +aztec compile +aztec codegen target --outdir artifacts +``` + +## Testing the Complete Privacy System + +Before running the updated script, double-check your local network is still running: + +```bash +aztec start --local-network +``` + +If you stopped it between parts of the tutorial, start it again here. Because we set `ephemeral: true` when creating the wallet, restarting the network is safe — the script won't try to reuse stale PXE state from a previous run. + +Now that you've implemented all the privacy features, let's update our test script to showcase the full privacy flow: + +### Update Your Test Script + +Let's stop being lazy and add a nice little "log" function that just spits out everyone's balances to the console, for example: + +```typescript +// at the top of your file +#include_code get_balances /docs/examples/ts/bob_token_contract/index.ts raw +``` + +Looks ugly but it does what it says: prints Alice's and Bob's balances. This will make it easier to see our contract working. + +Now let's add some more stuff to our `index.ts`: + +```typescript +async function main() { + // ...etc +#include_code checkpoint_2 /docs/examples/ts/bob_token_contract/index.ts raw +``` + +The flow is something like: + +- Giggle mints Alice 100 BOB in public +- Alice transfers 10 BOB to Bob in public +- Alice makes the remaining 90 BOB private +- Alice transfers 50 of those to Bob, in private +- Of the remaining 40 BOB, she makes 10 public again +- Giggle mints 100 BOB tokens for Alice, in private + +Let's give it a try: + +```bash +npx tsx index.ts +``` + +You should see the complete privacy journey from transparent allocation to confidential usage. The final pair of log lines should look like: + +```text +📊 Alice has 10 public BOB tokens and 130 private BOB tokens +📊 Bob's Clinic has 10 public BOB tokens and 50 private BOB tokens +``` + +If your output doesn't match, double-check that the local network is running and that you started this run with a fresh `aztec start --local-network`. + +## Summary + +You've built a privacy-preserving token system that solves a real-world problem: enabling corporate mental health benefits while protecting employee privacy. This demonstrates Aztec's unique ability to provide both transparency and privacy where each is most needed. + +The BOB token shows how blockchain can enable new models of corporate benefits that weren't possible before - where verification and privacy coexist, empowering employees to seek help without fear of judgment or career impact. + +### What You Learned + +- How to create tokens with both public and private states +- How to bridge between public and private domains +- How to implement access control across execution contexts +- How to build real-world privacy solutions on Aztec + +## Going Further: The AIP-20 Token Standard + +The BOB token you built in this tutorial implements a simplified version of the patterns formalized in **AIP-20**, Aztec's fungible token standard. AIP-20 extends these patterns with commitment-based transfers for DeFi composability, recursive note consumption for large balances, and tokenized vault support (AIP-4626). + +Read the full [AIP-20 standard reference](../../aztec-nr/standards/aip-20.md) for details, or explore all [Aztec Contract Standards](../../aztec-nr/standards/index.md). + +### Continue Your Journey + +- Explore [cross-chain communication](../../foundational-topics/ethereum-aztec-messaging/index.md) to integrate with existing health systems +- Learn about [account abstraction](../../foundational-topics/accounts/index.md) for recovery mechanisms diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/_category_.json b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/_category_.json new file mode 100644 index 00000000000..d497430c8a5 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Full-Stack Tutorials", + "position": 2, + "collapsible": true, + "collapsed": true +} diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md new file mode 100644 index 00000000000..ed673785e36 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md @@ -0,0 +1,532 @@ +--- +title: "Deposit to Aave from Aztec" +sidebar_position: 2 +description: "Build a cross-chain DeFi integration that deposits tokens into Aave from Aztec L2 and claims yield back." +tags: [defi, cross-chain, messaging, portals, advanced] +references: ["docs/examples/contracts/aave_bridge/src/main.nr", "docs/examples/solidity/aave_bridge/AavePortal.sol", "docs/examples/ts/aave_bridge/index.ts"] +--- + +## Why DeFi from Aztec? + +Imagine you hold DAI on Aztec L2. Gas is cheap, transactions are private, but your tokens are just sitting there. What if you could deposit them into Aave on Ethereum, earn yield, and then bring those yield-bearing tokens back to Aztec? + +In this tutorial, you'll build exactly that: a **cross-chain DeFi bridge** that moves tokens between Aztec and Aave's lending pool on Ethereum. By the end, you'll understand how to compose L1 DeFi protocols with Aztec's cross-chain messaging system. + +## What You'll Build + +The diagram below shows the full round-trip, starting from tokens the user already holds on L2: + +```mermaid +graph LR + subgraph Ethereum["Ethereum (L1)"] + Portal["🌉 AavePortal"] + Aave["🏦 Aave Pool"] + end + + subgraph Aztec["Aztec (L2)"] + Bridge["🔗 AaveBridge"] + Token["🪙 Token"] + end + + Bridge -->|"1. Burn tokens"| Token + Bridge -->|"2. L2→L1 Message"| Portal + Portal -->|"3. Deposit"| Aave + + Aave -.->|"4. Withdraw + Yield"| Portal + Portal -.->|"5. L1→L2 Message"| Bridge + Bridge -.->|"6. Mint (with yield)"| Token + + style Bridge fill:#4ade80,stroke:#22c55e,stroke-width:3px + style Portal fill:#4ade80,stroke:#22c55e,stroke-width:3px + style Token fill:#f0f0f0,stroke:#999,stroke-width:2px + style Aave fill:#f0f0f0,stroke:#999,stroke-width:2px +``` + +You'll create: + +- **AaveBridge (L2)** — A Noir contract that burns/mints tokens and sends/consumes cross-chain messages +- **AavePortal (L1)** — A Solidity contract that interacts with Aave and handles L1↔L2 messaging +- **Mock Aave contracts** — Simplified mocks of Aave's lending pool for local testing +- **Integration script** — A TypeScript script that deploys everything and runs the full flow + +## Prerequisites + +- [Aztec local network running at version #include_aztec_version](../../../getting_started_on_local_network.md) (includes Aztec CLI and Node.js v24+) +- [Hardhat](https://hardhat.org/getting-started) installed for Solidity compilation and deployment +- Familiarity with the [Token Bridge tutorial](./token_bridge.md) (recommended) +- Basic understanding of [cross-chain messaging](../../foundational-topics/ethereum-aztec-messaging/index.md) + +## Understanding the Flow + +The bridge has two directions: **depositing** tokens from L2 into Aave on L1, and **claiming** them back (with yield) on L2. + +### Deposit Flow (L2 → Aave) + +```mermaid +sequenceDiagram + participant User + participant Bridge as AaveBridge (L2) + participant Token as Token (L2) + participant Outbox as Outbox + participant Portal as AavePortal (L1) + participant Aave as Aave Pool (L1) + + User->>Bridge: exit_to_l1_public(amount) + Bridge->>Outbox: message_portal(content) + Bridge->>Token: burn_public(user, amount) + Note over Outbox: Wait for epoch to be submitted to L1 + User->>Portal: depositToAave(amount, proof) + Portal->>Outbox: consume(message) + Portal->>Aave: supply(underlying, amount) + Aave-->>Portal: aTokens +``` + +### Claim Flow (Aave → L2) + +```mermaid +sequenceDiagram + participant User + participant Portal as AavePortal (L1) + participant Aave as Aave Pool (L1) + participant Inbox as Inbox + participant Bridge as AaveBridge (L2) + participant Token as Token (L2) + + User->>Portal: claimFromAavePublic(aTokenAmount) + Portal->>Aave: withdraw(aTokenAmount) + Aave-->>Portal: underlying + yield + Portal->>Inbox: sendL2Message(content) + Note over Inbox: Wait for an L2 block to include the message (12-30s) + User->>Bridge: claim_public(amount_with_yield) + Bridge->>Inbox: consume_l1_to_l2_message + Bridge->>Token: mint_to_public(user, amount_with_yield) +``` + +## Project Setup + +Start with the Hardhat + Aztec template. This provides a pre-configured Hardhat project with Aztec dependencies and Solidity compilation settings: + +:::note + +This template is a community-maintained starter. If the repository is unavailable, you can set up a Hardhat project manually and add the `@aztec/*` Solidity remappings from the [cross-chain messaging docs](../../foundational-topics/ethereum-aztec-messaging/index.md). + +::: + +```bash +git clone https://github.com/critesjosh/hardhat-aztec-example +cd hardhat-aztec-example +``` + +Before adding dependencies, make the starter install with yarn 4: create the `.yarnrc.yml`, swap `@aztec/l1-contracts` for `@aztec-foundation/l1-artifacts`, point the `postinstall` remappings at the new package, and add the root `remappings.txt`. These are the first three steps of the [Token Bridge tutorial's project setup](./token_bridge.md#project-setup); without them `yarn add` fails on the starter's git dependencies and `npx hardhat compile` cannot resolve the `@aztec/*` imports. + +When complete, your project will have this structure: + +``` +hardhat-aztec-example/ + contracts/ # Solidity contracts (Hardhat default) + MockERC20.sol + MockAToken.sol + MockAavePool.sol + AavePortal.sol + contracts/aztec/ # Noir contracts + aave_bridge/ + Nargo.toml # workspace + aave_bridge_contract/ + Nargo.toml + src/main.nr + src/config.nr + aave_bridge_test/ + Nargo.toml + src/lib.nr + scripts/ + index.ts # Integration script + artifacts/ # Generated by aztec codegen +``` + +Add the Aztec dependencies: + +```bash +yarn add @aztec-labs/aztec.js@#include_version_without_prefix @aztec-labs/accounts@#include_version_without_prefix @aztec-labs/wallets@#include_version_without_prefix @aztec-labs/stdlib@#include_version_without_prefix @aztec-labs/foundation@#include_version_without_prefix @aztec-labs/ethereum@#include_version_without_prefix @aztec-labs/noir-contracts.js@#include_version_without_prefix @aztec/viem@2.38.2 tsx +``` + +Start the local network in another terminal: + +```bash +aztec start --local-network +``` + +## Part 1: The L2 Bridge Contract + +The L2 bridge is the simpler side. It doesn't know anything about Aave — it just burns/mints tokens and passes messages. All the Aave-specific logic lives on L1. + +:::note +The L2 bridge is intentionally protocol-agnostic — it just burns/mints tokens and relays messages. All Aave-specific logic lives on L1. This means you can compose with any L1 protocol without changing your L2 contract. If you've completed the [Token Bridge tutorial](./token_bridge.md), you'll recognize the pattern and can skim to [Part 2](#part-2-the-ethereum-side). +::: + +Create the bridge contract: + +```bash +aztec new contracts/aztec/aave_bridge +cd contracts/aztec/aave_bridge +``` + +The `aztec new` command creates a workspace with an `aave_bridge_contract` crate and an `aave_bridge_test` crate. Replace the generated test file at `aave_bridge_test/src/lib.nr` with a basic constructor test: + +```rust +use aztec::protocol::address::{AztecAddress, EthAddress}; +use aztec::protocol::traits::FromField; +use aztec::test::helpers::test_environment::TestEnvironment; +use aave_bridge_contract::AaveBridge; + +#[test] +unconstrained fn test_constructor() { + let mut env = TestEnvironment::new(); + let deployer = env.create_light_account(); + + let token = AztecAddress::from_field(1); + let portal = EthAddress::from_field(2); + + let initializer = AaveBridge::interface().constructor(token, portal); + let _contract_address = + env.deploy("@aave_bridge_contract/AaveBridge").with_public_initializer(deployer, initializer); +} +``` + +The test crate must depend on the same `aztec` source as the contract, or `aztec compile` fails with "`PublicCall` and `PublicCall` are distinct types". Set `contracts/aztec/aave_bridge/aave_bridge_test/Nargo.toml` to: + +```toml +[package] +name = "aave_bridge_test" +type = "lib" + +[dependencies] +aztec = { git="https://github.com/aztec-labs-eng/aztec-node", tag = "#include_aztec_version", directory = "noir-projects/aztec-nr/aztec" } +aave_bridge_contract = { path = "../aave_bridge_contract" } +``` + +The bridge reuses the existing `Token` contract and the `token_portal_content_hash_lib` for content hash functions. Add these dependencies to `contracts/aztec/aave_bridge/aave_bridge_contract/Nargo.toml`: + +```toml +[dependencies] +aztec = { git="https://github.com/aztec-labs-eng/aztec-node", tag = "#include_aztec_version", directory = "noir-projects/aztec-nr/aztec" } +token_portal_content_hash_lib = { git="https://github.com/aztec-labs-eng/aztec-node", tag = "#include_aztec_version", directory = "noir-projects/noir-contracts/contracts/libs/token_portal_content_hash_lib" } +token = { git="https://github.com/aztec-labs-eng/aztec-node", tag = "#include_aztec_version", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } +``` + +### Bridge Storage + +The bridge stores two things: the L2 token address and the L1 portal address. First, create the config module at `contracts/aztec/aave_bridge/aave_bridge_contract/src/config.nr`: + +#include_code config /docs/examples/contracts/aave_bridge/src/config.nr rust + +Then replace `contracts/aztec/aave_bridge/aave_bridge_contract/src/main.nr`: + +<!-- wrapped in a code block to add a "}" at the end --> + +```rust +#include_code bridge_setup /docs/examples/contracts/aave_bridge/src/main.nr raw +} +``` + +:::warning Assembling the Contract +The code above shows the contract opening — imports, storage, constructor, and a getter — followed by a closing `}`. In the sections below, you'll add more functions **inside** this contract body. Place them before the final `}` so they are part of `pub contract AaveBridge { ... }`. +::: + +### Public Claim and Exit + +Add the following functions inside the `AaveBridge` contract body (before the closing `}`). `claim_public` consumes an L1→L2 message and mints tokens. `exit_to_l1_public` burns tokens and sends an L2→L1 message: + +#include_code claim_public /docs/examples/contracts/aave_bridge/src/main.nr rust + +#include_code exit_to_l1_public /docs/examples/contracts/aave_bridge/src/main.nr rust + +The `authwit_nonce` parameter supports [authentication witnesses](../../aztec-js/how_to_use_authwit.md). When the caller is the token owner (`msg.sender`), pass `0` — no authorization witness is needed. If a third party calls this function on behalf of the owner, they must provide a valid nonce from an authwit the owner previously created. + +### Private Claim and Exit + +Still inside the contract body, add the private variants. They work the same way but use private token operations. The recipient's address is hidden when claiming privately: + +#include_code claim_private /docs/examples/contracts/aave_bridge/src/main.nr rust + +#include_code exit_to_l1_private /docs/examples/contracts/aave_bridge/src/main.nr rust + +:::info Content Hash Matching + +The content hash is the critical link between L1 and L2. Both sides must produce the exact same hash for a message to be consumed. The `token_portal_content_hash_lib` handles this by encoding parameters identically to the Solidity side's `abi.encodeWithSignature`. For example, `get_mint_to_public_content_hash(to, amount)` on L2 matches `Hash.sha256ToField(abi.encodeWithSignature("mint_to_public(bytes32,uint256)", to, amount))` on L1. + +::: + +### Compile + +```bash +aztec compile +``` + +Generate TypeScript bindings: + +```bash +aztec codegen target --outdir ../artifacts +``` + +:::note Token Contract +The integration script imports `TokenContract` from `@aztec-labs/noir-contracts.js`, which provides pre-built bindings for the standard Token contract. Only the custom `AaveBridge` contract needs codegen. +::: + +## Part 2: The Ethereum Side + +### Mock Aave Contracts + +For local testing, you'll use simplified mocks of Aave's lending pool. The mock pool accepts deposits and returns them with a configurable yield — 10% in this tutorial (1000 basis points, where 10000 bps = 100%). + +:::tip Mock vs Real Aave + +In production, replace `MockAavePool` with Aave V3's `IPool` interface at `0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2` (Ethereum mainnet). The portal contract's `IAavePool` interface already matches Aave V3's function signatures. For realistic testing, fork mainnet with `aztec-anvil --fork-url <your-rpc-url>` (the Aztec installer ships Foundry's `anvil` as `aztec-anvil`; substitute your own `anvil` if its version matches `aztec-anvil --version`). + +::: + +Create the following mock contracts in `contracts/`. + +`contracts/MockERC20.sol` — a minimal ERC20 with public minting: + +```solidity +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +#include_code mock_erc20 /docs/examples/solidity/aave_bridge/MockERC20.sol raw +``` + +`contracts/MockAToken.sol` — Aave's yield-bearing token mock: + +```solidity +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +#include_code mock_atoken /docs/examples/solidity/aave_bridge/MockAToken.sol raw +``` + +`contracts/MockAavePool.sol` — simplified Aave lending pool that returns a configurable yield: + +```solidity +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {MockERC20} from "./MockERC20.sol"; +import {MockAToken} from "./MockAToken.sol"; + +#include_code mock_aave_pool /docs/examples/solidity/aave_bridge/MockAavePool.sol raw +``` + +### AavePortal Contract + +The portal is where the magic happens. It bridges Aztec's cross-chain messages with Aave's lending pool. Create `contracts/AavePortal.sol`: + +<!-- wrapped in a code block to add a "}" at the end --> + +```solidity +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IRegistry} from "@aztec-foundation/l1-artifacts/l1-contracts/src/governance/interfaces/IRegistry.sol"; +import {IInbox} from "@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol"; +import {IOutbox} from "@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol"; +import {IRollup} from "@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/IRollup.sol"; +import {DataStructures} from "@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol"; +import {Hash} from "@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/crypto/Hash.sol"; +import {Epoch} from "@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/TimeLib.sol"; + +#include_code portal_setup /docs/examples/solidity/aave_bridge/AavePortal.sol raw +} +``` + +:::warning Assembling the Contract +Like the L2 contract, the code above shows the contract opening — imports, state variables, and `initialize()`. The subsequent function snippets go **inside** this contract body, before the closing `}`. +::: + +The portal has three key functions. First, `depositToAave` consumes an L2→L1 message (proving the user burned tokens on L2) and deposits the underlying tokens into Aave: + +#include_code portal_deposit_to_aave /docs/examples/solidity/aave_bridge/AavePortal.sol solidity + +Then, `claimFromAavePublic` withdraws from Aave (including any yield earned) and sends an L1→L2 message so the user can mint tokens on L2: + +#include_code portal_claim_public /docs/examples/solidity/aave_bridge/AavePortal.sol solidity + +There's also a private variant that lets the user claim without revealing their L2 address: + +#include_code portal_claim_private /docs/examples/solidity/aave_bridge/AavePortal.sol solidity + +### Compile + +```bash +npx hardhat compile +``` + +:::note Solidity Artifact Paths +Hardhat compiles Solidity contracts to `artifacts/contracts/` by default. The integration script imports ABIs from this location (e.g., `../artifacts/contracts/AavePortal.sol/AavePortal.json`). +::: + +## Part 3: Deploying and Testing + +Create `scripts/index.ts` to run the full flow. This script deploys all contracts, initializes them, deposits tokens into Aave from L2, and claims them back with yield. + +### Setup + +```typescript +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; +import { AztecAddress, EthAddress } from "@aztec-labs/aztec.js/addresses"; +import { SetPublicAuthwitContractInteraction } from "@aztec-labs/aztec.js/authorization"; +import { Fr } from "@aztec-labs/aztec.js/fields"; +import { createAztecNodeClient, waitForNode } from "@aztec-labs/aztec.js/node"; +import { createExtendedL1Client } from "@aztec-labs/ethereum/client"; +import { deployL1Contract } from "@aztec-labs/ethereum/deploy-l1-contract"; +import { sha256ToField } from "@aztec-labs/foundation/crypto/sha256"; +import { + computeL2ToL1MessageHash, + computeSecretHash, +} from "@aztec-labs/stdlib/hash"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { decodeEventLog, pad, toFunctionSelector } from "@aztec/viem"; +import { foundry } from "@aztec/viem/chains"; +import AavePortal from "../artifacts/contracts/AavePortal.sol/AavePortal.json" with { type: "json" }; +import MockERC20 from "../artifacts/contracts/MockERC20.sol/MockERC20.json" with { type: "json" }; +import MockAToken from "../artifacts/contracts/MockAToken.sol/MockAToken.json" with { type: "json" }; +import MockAavePool from "../artifacts/contracts/MockAavePool.sol/MockAavePool.json" with { type: "json" }; +import { TokenContract } from "@aztec-labs/noir-contracts.js/Token"; +import { AaveBridgeContract } from "../contracts/aztec/artifacts/AaveBridge.js"; + +#include_code setup /docs/examples/ts/aave_bridge/index.ts raw +``` + +:::note About EmbeddedWallet +`EmbeddedWallet` is a simplified wallet for local development. It handles key management, transaction signing, and proof generation in-process. Code written against `EmbeddedWallet` works with any `Wallet` implementation, so your application logic transfers directly to production. +::: + +### Deploy L1 Contracts + +:::note Hardhat artifacts +The deployment snippet below comes from the example project, which builds its Solidity with Foundry. Hardhat artifacts store the bytecode as a plain string, so with `npx hardhat compile` use `MockERC20.bytecode` (and likewise for `MockAToken`, `MockAavePool` and `AavePortal`) instead of `.bytecode.object`. +::: + +#include_code deploy_l1 /docs/examples/ts/aave_bridge/index.ts typescript + +### Deploy L2 Contracts + +#include_code deploy_l2 /docs/examples/ts/aave_bridge/index.ts typescript + +### Initialize + +#include_code initialize /docs/examples/ts/aave_bridge/index.ts typescript + +### Fund the User + +For this tutorial, you need tokens in two places: + +- **L2 tokens for the user** — The user needs tokens on L2 to burn and bridge to L1. In production, these would come from a prior bridge operation. +- **L1 underlying tokens at the portal** — When the portal calls `depositToAave`, it transfers underlying tokens to Aave. The portal must already hold these tokens. In production, the tokens would arrive via a separate bridging mechanism. + +For simplicity, mint directly to both: + +#include_code fund_user /docs/examples/ts/aave_bridge/index.ts typescript + +### Deposit to Aave (L2 → L1) + +Now for the main flow. Burn tokens on L2 and send a message to L1. + +:::info Why is the portal the recipient? +The `recipient` in `exit_to_l1_public` is the L1 address that receives the withdrawal message. Since the AavePortal contract needs to deposit the tokens into Aave, the portal itself is the recipient. Setting `caller_on_l1` to `EthAddress.ZERO` means anyone can relay the message on L1 — there's no access restriction on who calls `depositToAave`. +::: + +#include_code deposit_to_aave /docs/examples/ts/aave_bridge/index.ts typescript + +Compute the membership witness to prove the message on L1: + +#include_code get_deposit_witness /docs/examples/ts/aave_bridge/index.ts typescript + +Execute the deposit on L1: + +#include_code execute_deposit_l1 /docs/examples/ts/aave_bridge/index.ts typescript + +### Claim from Aave with Yield (L1 → L2) + +Before withdrawing from Aave, generate a random secret and compute its hash. The secret hash is included in the L1-to-L2 message — only someone who knows the pre-image (the secret) can consume the message on L2. This prevents front-running: without the secret, no one else can claim your tokens. + +Withdraw from Aave on L1 and send the message to L2. The mock pool returns 10% yield: + +#include_code claim_from_aave_l1 /docs/examples/ts/aave_bridge/index.ts typescript + +Extract the message leaf index: + +#include_code get_claim_leaf_index /docs/examples/ts/aave_bridge/index.ts typescript + +On the local network, L2 blocks are only produced when transactions are submitted. An L1-to-L2 message can only be consumed once an L2 block includes it, and the network waits until the message is at least 12 seconds old before including it. This utility deploys two dummy contracts (with random salts for unique addresses) to force block production. On devnet or testnet, blocks are produced continuously and this step is unnecessary: + +#include_code mine_blocks /docs/examples/ts/aave_bridge/index.ts typescript + +Claim the tokens (with yield) on L2: + +#include_code claim_on_l2 /docs/examples/ts/aave_bridge/index.ts typescript + +### Verify + +#include_code verify /docs/examples/ts/aave_bridge/index.ts typescript + +Run the full flow: + +```bash +npx hardhat run scripts/index.ts --network localhost +``` + +You should see the user start with 1000 tokens, deposit 500 to Aave, and end up with 1050 tokens (500 remaining + 550 from Aave with 10% yield). + +## What You Built + +A complete cross-chain DeFi integration with: + +1. **L2 Bridge** (Noir) — Burns/mints tokens and handles cross-chain messages. Supports both public and private operations. +2. **L1 Portal** (Solidity) — Deposits into Aave and withdraws with yield. Handles message consumption and creation. +3. **Mock Aave** (Solidity) — Simulates yield generation for local testing. +4. **Full Flow** — Deposit tokens from L2 into Aave, earn yield, and claim back on L2. + +:::warning Production Considerations + +This tutorial uses mock contracts for simplicity. In production: + +- Replace `MockAavePool` with a real Aave V3 pool address +- Handle Aave's variable interest rates (the withdrawn amount may differ from expectations) +- Add slippage protection and error handling for failed messages +- Consider that funds are "in flight" between chains — implement recovery mechanisms +- Add proper access controls to the portal contract + +::: + +## Troubleshooting + +### Script hangs waiting for block to be published + +The deposit flow waits for the L2 block containing your exit transaction to be included in an epoch that is submitted to L1. On the local network, this typically takes 30–60 seconds. If it takes longer, check that your local network is running and producing blocks. + +### Content hash mismatch — L1 message consumption reverts + +This is the most common cross-chain debugging issue. The content hash computed on L2 (via `get_withdraw_content_hash`) must exactly match what the L1 portal reconstructs via `abi.encodeWithSignature`. Double-check that: +- The function signature string matches on both sides (e.g., `"withdraw(address,uint256,address)"`) +- Parameters are in the same order and encoded as the same types +- The `caller_on_l1` value matches: `EthAddress.ZERO` on L2 corresponds to `address(0)` on L1 + +### "Minter not set" — L2 claim fails + +If `claim_public` reverts, ensure you called `set_minter(l2Bridge.address, true)` on the Token contract **after** deploying the bridge. The bridge must be authorized as a minter before it can mint tokens on claim. + +### L1→L2 message not found — claim reverts after mining blocks + +An L1-to-L2 message becomes consumable once an L2 block includes it, which takes 12 to 30 seconds after the L1 transaction. Make sure `mine2Blocks` runs before the claim. If the issue persists, verify the `messageLeafIndex` extracted from the `MessageSent` event is correct. + +## Next Steps + +- **Test with a mainnet fork**: Use `aztec-anvil --fork-url` (or your own `anvil` install) to test against real Aave +- **Add private deposits**: Use the `claim_private` and `exit_to_l1_private` functions for privacy-preserving DeFi +- **Build a frontend**: Add a web UI for easy depositing and claiming +- **Compose with other protocols**: The same pattern works for Uniswap, Compound, or any L1 DeFi protocol + +:::tip Learn More + +- [Cross-chain messaging](../../foundational-topics/ethereum-aztec-messaging/index.md) +- [Token Bridge Tutorial](./token_bridge.md) +- [State management](../../foundational-topics/state_management.md) + +::: diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aztecjs-getting-started.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aztecjs-getting-started.md new file mode 100644 index 00000000000..4ca94044426 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aztecjs-getting-started.md @@ -0,0 +1,135 @@ +--- +title: Deploying a Token Contract +sidebar_position: 0 +description: A tutorial going through how to deploy a token contract to the local network using typescript. +references: ["docs/examples/ts/aztecjs_getting_started/index.ts"] +--- + +import Image from "@theme/IdealImage"; + +In this guide, we will retrieve the local network and deploy a pre-written token contract to it using Aztec.js. [Check out the source code](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr). We will then use Aztec.js to interact with this contract and transfer tokens. + +Before starting, make sure to be running Aztec local network at version #include_version_without_prefix. Check out [the guide](../../../getting_started_on_local_network.md) for info about that. + +## Set up the project + +First, create a new directory for your project and initialize it with yarn: + +```sh +mkdir token-tutorial +cd token-tutorial +yarn init -y +``` + +Next, add the TypeScript dependencies: + +```sh +yarn add "typescript@^5.3.3" @types/node tsx +``` + +:::tip + +Never heard of `tsx`? Well, it will just run `typescript` with reasonable defaults. Pretty cool for a small example like this one. You may want to tune in your own project's `tsconfig.json` later! + +::: + +Let's also import the Aztec dependencies for this tutorial: + +```sh +yarn add @aztec-labs/aztec.js@#include_version_without_prefix @aztec-labs/accounts@#include_version_without_prefix @aztec-labs/noir-contracts.js@#include_version_without_prefix @aztec-labs/wallets@#include_version_without_prefix +``` + +Aztec.js assumes your project is using ESM, so make sure you add `"type": "module"` to `package.json`. You probably also want at least a `start` script. For example: + +```json +{ + "type": "module", + "scripts": { + "start": "tsx index.ts" + } +} +``` + +### Connecting to the local network + +Now let's connect to the Aztec local network and set up test accounts. + +**Step 1: Start the Aztec Local Network** + +In a separate terminal, run: + +```sh +aztec start --local-network +``` + +Keep this terminal running throughout the tutorial. + +**Step 2: Create the index.ts file** + +Create an `index.ts` file in the root of your project with the following code. This connects to the local network and imports test accounts (Alice and Bob): + +#include_code setup /docs/examples/ts/aztecjs_getting_started/index.ts typescript + +**Step 3: Verify the script runs** + +Run the script to make sure everything is set up correctly: + +```sh +yarn start +``` + +If there are no errors, you're ready to continue. For more details on connecting to the local network, see [this guide](../../aztec-js/how_to_connect_to_local_network.md). + +## Deploy the token contract + +Now that we have our accounts loaded, let's deploy a pre-compiled token contract from the Aztec library. You can find the full code for the contract [here (GitHub link)](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/noir-projects/noir-contracts/contracts/app/token_contract/src). + +Add the following to `index.ts` to import the contract and deploy it with Alice as the admin: + +#include_code deploy /docs/examples/ts/aztecjs_getting_started/index.ts typescript + +## Mint and transfer + +Let's go ahead and have Alice mint herself some tokens, in private: + +#include_code mint /docs/examples/ts/aztecjs_getting_started/index.ts typescript + +Let's check both Alice's and Bob's balances now: + +#include_code check_balances /docs/examples/ts/aztecjs_getting_started/index.ts typescript + +Alice should have 100 tokens, while Bob has none yet. + +Great! Let's have Alice transfer some tokens to Bob, also in private: + +#include_code transfer /docs/examples/ts/aztecjs_getting_started/index.ts typescript + +Bob should now see 10 tokens in his balance. + +## Other cool things + +Say that Alice is nice and wants to set Bob as a minter. Even though it's a public function, it can be called in a similar way: + +#include_code set_minter /docs/examples/ts/aztecjs_getting_started/index.ts typescript + +Bob is now the minter, so he can mint some tokens to himself: + +#include_code bob_mints /docs/examples/ts/aztecjs_getting_started/index.ts typescript + +:::info + +Have a look at the [contract source](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr). Notice is that the `mint_to_private` function we used above actually starts a partial note. This allows the total balance to increase while keeping the recipient private! How cool is that? + +::: + +## Going Further + +The pre-compiled token contract used in this tutorial is Aztec's reference implementation. It covers the core operations you need to get started: minting, private transfers, and public balance management. + +For production applications, consider the **AIP-20 Token Standard** maintained by [DeFi Wonderland](https://github.com/defi-wonderland/aztec-standards/tree/dev/src/token_contract). AIP-20 formalizes the same patterns used in the reference contract and adds: + +- **Commitment-based transfers** for DeFi protocols where the recipient is determined asynchronously +- **Recursive note consumption** for handling large balances that span many notes +- **Tokenized vault support (AIP-4626)** for yield-bearing tokens that issue shares against an underlying asset + +To learn how to write a token contract from scratch rather than deploying a pre-compiled one, see the [Private Token Contract tutorial](../contract_tutorials/token_contract.md). For the full specifications of all Aztec contract standards, see the [Aztec Contract Standards](../../aztec-nr/standards/index.md) reference. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md new file mode 100644 index 00000000000..543158c3830 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md @@ -0,0 +1,560 @@ +--- +title: "Bridge Your NFT to Aztec" +sidebar_position: 1 +description: "Build a private NFT bridge between Ethereum and Aztec using custom notes, PrivateSet, and cross-chain messaging portals." +references: ["docs/examples/contracts/nft/*", "docs/examples/contracts/nft_bridge/*", "docs/examples/solidity/nft_bridge/*", "docs/examples/ts/token_bridge/*"] +--- + +## Why Bridge an NFT? + +Imagine you own a CryptoPunk NFT on Ethereum. You want to use it in games, social apps, or DeFi protocols, but gas fees on Ethereum make every interaction expensive. What if you could move your Punk to Aztec (L2), use it **privately** in dozens of applications, and then bring it back to Ethereum when you're ready to sell? + +In this tutorial, you'll build a **private NFT bridge**. By the end, you'll understand how **portals** work and how **cross-chain messages** flow between L1 and L2. + +Before starting, make sure you have the Aztec local network running at version #include_aztec_version. Check out [the local network guide](../../../getting_started_on_local_network.md) for setup instructions. + +## What You'll Build + +You'll create two contracts with **privacy at the core**: + +- **NFTPunk (L2)** - An NFT contract with encrypted ownership using `PrivateSet` +- **NFTBridge (L2)** - A bridge that mints NFTs privately when claiming L1 messages + +This tutorial focuses on the L2 side to keep things manageable. You'll learn the essential privacy patterns that apply to any asset bridge on Aztec. + +## Project Setup + +Let's start simple. Since this is an Ethereum project, it's easier to just start with Hardhat: + +```bash +git clone https://github.com/critesjosh/hardhat-aztec-example +``` + +You're cloning a repo here to make it easier for Aztec's `l1-contracts` to be mapped correctly. You should now have a `hardhat-aztec-example` folder with Hardhat's default starter, with a few changes in `package.json`. + +We want to add a few more dependencies now before we start. The starter needs four changes before it installs with yarn 4. + +First, from the `hardhat-aztec-example` directory (`cd hardhat-aztec-example`), tell yarn to install into `node_modules` (the starter's scripts and Hardhat expect a real `node_modules` tree) and allow the starter's one git dependency, `forge-std`. Create `.yarnrc.yml`: + +```yaml +nodeLinker: node-modules + +approvedGitRepositories: + - "https://github.com/foundry-rs/forge-std.git" +``` + +Second, replace the starter's `@aztec/l1-contracts` git dependency, whose repository is no longer updated, with the `@aztec-foundation/l1-artifacts` npm package. Its `postinstall` writes Solidity remappings for that package, so point it at the new one too: + +```bash +npm pkg delete dependencies.@aztec/l1-contracts devDependencies.@aztec/l1-contracts +npm pkg set "dependencies.@aztec-foundation/l1-artifacts=#include_foundation_version_without_prefix" +npm pkg set scripts.postinstall="node -e \"require('fs').writeFileSync('node_modules/@aztec-foundation/l1-artifacts/remappings.txt', '@oz/=l1-contracts/lib/openzeppelin-contracts/contracts/\\n@aztec/=l1-contracts/src/\\n@aztec-blob-lib/=l1-contracts/src/mock/libraries/\\n@zkpassport/=l1-contracts/lib/circuits/src/solidity/src/\\n')\"" +``` + +Hardhat resolves the `@aztec/...` imports inside the package's own Solidity sources through that `remappings.txt`, so the `postinstall` has to run after every install. + +Third, map your own contracts' imports in a `remappings.txt` at the project root: + +```text +@oz/=node_modules/@openzeppelin/contracts/ +@aztec/=node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/ +``` + +Finally, add the Aztec packages: + +```bash +yarn add @aztec-labs/aztec.js@#include_version_without_prefix @aztec-labs/accounts@#include_version_without_prefix @aztec-labs/stdlib@#include_version_without_prefix @aztec-labs/wallets@#include_version_without_prefix @aztec-labs/ethereum@#include_version_without_prefix @aztec-labs/foundation@#include_version_without_prefix @aztec/viem@2.38.2 tsx +``` + +:::tip Packages published in the last 24 hours +Yarn 4 refuses packages younger than a day by default, reporting them as "quarantined". If you install a release on its publication day, add this to `.yarnrc.yml`: + +```yaml +npmPreapprovedPackages: + - "@aztec-labs/*" + - "@aztec-foundation/*" +``` +::: + +The interfaces the portal imports later in this tutorial come from `@aztec-foundation/l1-artifacts`, so they match the contracts deployed by your running network. + +Now start the local network in another terminal: + +```bash +aztec start --local-network +``` + +This should start two important services on ports 8080 and 8545, respectively: Aztec and Anvil (an Ethereum development node). + +## Part 1: Building the NFT Contract + +Let's start with a basic NFT contract on Aztec. That's the representation of the NFT locked on the L2 side: + +```mermaid +graph LR + subgraph Ethereum["Ethereum (L1)"] + L1NFT["🎨 L1 NFT<br/>(CryptoPunk)"] + L1Portal["🌉 L1 Portal<br/>(TokenPortal)"] + end + + subgraph Aztec["Aztec (L2)"] + L2Bridge["🔗 L2 Bridge<br/>(NFTBridge)"] + L2NFT["🎭 L2 NFT<br/>(NFTPunk)"] + end + + L1NFT -->|"Lock NFT"| L1Portal + L1Portal -->|"L1→L2 Message"| L2Bridge + L2Bridge -->|"Mint Private"| L2NFT + + L2NFT -.->|"Burn"| L2Bridge + L2Bridge -.->|"L2→L1 Message"| L1Portal + L1Portal -.->|"Unlock NFT"| L1NFT + + style L2NFT fill:#4ade80,stroke:#22c55e,stroke-width:3px + style L2Bridge fill:#f0f0f0,stroke:#999,stroke-width:2px + style L1Portal fill:#f0f0f0,stroke:#999,stroke-width:2px + style L1NFT fill:#f0f0f0,stroke:#999,stroke-width:2px + + classDef highlight fill:#4ade80,stroke:#22c55e,stroke-width:3px +``` + +Let's create that crate in the `contracts` folder so it looks tidy: + +```bash +aztec new contracts/aztec/nft +cd contracts/aztec/nft +``` + +This creates a workspace with two crates: an `nft_contract` crate for the smart contract code and an `nft_test` crate for Noir tests. The `aztec` dependency is already configured in `nft_contract/Nargo.toml`. + +:::tip Noir Language Server + +If you're using VS Code, install the [Noir Language Support extension](https://marketplace.visualstudio.com/items?itemName=noir-lang.vscode-noir) for syntax highlighting, error checking, and code completion while writing Noir contracts. + +::: + +### Create the NFT Note + +First, let's create a custom note type for private NFT ownership. In the `nft_contract/src/` directory, create a new file called `nft.nr`: + +```bash +touch nft_contract/src/nft.nr +``` + +In this file, you're going to create a **private note** that represents NFT ownership. This is a struct with macros that indicate it is a note that can be compared and packed: + +#include_code nft_note_struct /docs/examples/contracts/nft/src/nft.nr rust + +You now have a note that represents the owner of a particular NFT. Next, move on to the contract itself. + +:::tip Custom Notes + +Notes are powerful concepts. Learn more about how to use them in the [state management guide](../../foundational-topics/state_management.md). + +::: + +### Define Storage + +Back in `nft_contract/src/main.nr`, you can now build the contract storage. You need: + +- **admin**: Who controls the contract (set once, never changes) +- **minter**: The bridge address (set once by admin) +- **nfts**: Track which NFTs exist (public, needed for bridging) +- **owners**: Private ownership using the NFTNote + +One interesting aspect of this storage configuration is the use of `DelayedPublicMutable`, which allows private functions to read and use public state. You're using it to publicly track which NFTs are already minted while keeping their owners private. Read more about `DelayedPublicMutable` in [the storage guide](../../aztec-nr/framework-description/state_variables.md). + +Write the storage struct and a simple [initializer](../../foundational-topics/contract_creation.md#initialization) to set the admin in the `nft_contract/src/main.nr` file: + +<!-- wrapped in a code block to add a "}" at the end --> + +```rust +#include_code contract_setup /docs/examples/contracts/nft/src/main.nr raw +} +``` + +### Utility Functions + +Add an internal function to handle the `DelayedPublicMutable` value change. Mark the function as public and `#[only_self]` so only the contract can call it: + +#include_code mark_nft_exists /docs/examples/contracts/nft/src/main.nr rust + +This function is marked with `#[only_self]`, meaning only the contract itself can call it. It uses `schedule_value_change` to update the `nfts` storage, preventing the same NFT from being minted twice or burned when it doesn't exist. You'll call this public function from a private function later using `enqueue_self`. + +Another useful function checks how many notes a caller has. You can use this later to verify the claim and exit from L2: + +#include_code notes_of /docs/examples/contracts/nft/src/main.nr rust + +### Add Minting and Burning + +Before anything else, you need to set the minter. This will be the bridge contract, so only the bridge contract can mint NFTs. This value doesn't need to change after initialization. Here's how to initialize the `PublicImmutable`: + +#include_code set_minter /docs/examples/contracts/nft/src/main.nr rust + +Now for the magic - minting NFTs **privately**. The bridge will call this to mint to a user, deliver the note using [constrained message delivery](../../aztec-nr/framework-description/events_and_logs.md) (best practice when "sending someone a +note") and then [enqueue a public call](../../aztec-nr/framework-description/calling_contracts.md) to the `_mark_nft_exists` function: + +#include_code mint /docs/examples/contracts/nft/src/main.nr rust + +The bridge will also need to burn NFTs when users withdraw back to L1: + +#include_code burn /docs/examples/contracts/nft/src/main.nr rust + +:::note Clear the scaffold's placeholder test +The scaffolded `nft_test/src/lib.nr` imports the default contract name (`Main`), which no longer exists, so it fails to compile with "Could not resolve 'Main'". Tests aren't used in this tutorial, so replace its contents with a single-line stub to keep `aztec compile` clean: + +```rust +// Tests are out of scope for this tutorial. See https://docs.aztec.network/developers/docs/aztec-nr/testing_contracts for examples. +``` +::: + +### Compiling! + +Let's verify it compiles: + +```bash +aztec compile +``` + +🎉 You should see "Compilation complete!" This means our private NFT contract is ready. Now let's build the bridge. + +## Part 2: Building the Bridge + +We have built the L2 NFT contract. This is the L2 representation of an NFT that is locked on the L1 bridge. + +The L2 bridge is the contract that talks to the L1 bridge through cross-chain messaging. You can read more about this protocol [here](../../foundational-topics/ethereum-aztec-messaging/index.md). + +```mermaid +graph LR + subgraph Ethereum["Ethereum (L1)"] + L1NFT["🎨 L1 NFT<br/>(CryptoPunk)"] + L1Portal["🌉 L1 Portal<br/>(TokenPortal)"] + end + + subgraph Aztec["Aztec (L2)"] + L2Bridge["🔗 L2 Bridge<br/>(NFTBridge)"] + L2NFT["🎭 L2 NFT<br/>(NFTPunk)"] + end + + L1NFT -->|"Lock NFT"| L1Portal + L1Portal -->|"L1→L2 Message"| L2Bridge + L2Bridge -->|"Mint Private"| L2NFT + + L2NFT -.->|"Burn"| L2Bridge + L2Bridge -.->|"L2→L1 Message"| L1Portal + L1Portal -.->|"Unlock NFT"| L1NFT + + style L2Bridge fill:#4ade80,stroke:#22c55e,stroke-width:3px + style L2NFT fill:#f0f0f0,stroke:#999,stroke-width:2px + style L1Portal fill:#f0f0f0,stroke:#999,stroke-width:2px + style L1NFT fill:#f0f0f0,stroke:#999,stroke-width:2px + + classDef highlight fill:#4ade80,stroke:#22c55e,stroke-width:3px +``` + +Let's create a new contract in the same tidy `contracts/aztec` folder: + +```bash +cd .. +aztec new nft_bridge +cd nft_bridge +``` + +Now add the `NFTPunk` contract dependency to `nft_bridge_contract/Nargo.toml`. The `aztec` dependency is already there: + +```toml +[dependencies] +aztec = { git="https://github.com/aztec-labs-eng/aztec-nr", tag = "#include_aztec_version", directory = "aztec" } +NFTPunk = { path = "../../nft/nft_contract" } +``` + +### Understanding Bridges + +A bridge has two jobs: + +1. **Claim**: When someone deposits an NFT on L1, mint it on L2 +2. **Exit**: When someone wants to withdraw, burn on L2 and unlock on L1 + +This means having knowledge about the L2 NFT contract, and the bridge on the L1 side. That's what goes into our bridge's storage. + +### Bridge Storage + +Clean up `nft_bridge_contract/src/main.nr` which is just a placeholder, and let's write the storage struct and the constructor. We'll use `PublicImmutable` since these values never change: + +<!-- wrapped in a code block to add a "}" at the end --> + +```rust +#include_code bridge_setup /docs/examples/contracts/nft_bridge/src/main.nr raw +} +``` + +You can't initialize the `portal` value in the constructor because the L1 portal hasn't been deployed yet. You'll need another function to set it up after the L1 portal is deployed. + +### Adding the Bridge Functions + +The Aztec network provides a way to consume messages from L1 to L2 called `consume_l1_to_l2_message`. + +You need to define how to encode messages. Here's a simple approach: when an NFT is being bridged, the L1 portal sends a hash of its `token_id` through the bridge, signaling which `token_id` was locked and can be minted on L2. This approach is simple but sufficient for this tutorial. + +Build the `claim` function, which consumes the message and mints the NFT on the L2 side: + +#include_code claim /docs/examples/contracts/nft_bridge/src/main.nr rust + +:::tip Secret + +The secret prevents front-running. Certainly you don't want anyone to claim your NFT on the L2 side by just being faster. Adding a secret acts like a "password": you can only claim it if you know it. + +::: + +Similarly, exiting to L1 means burning the NFT on the L2 side and pushing a message through the protocol. To ensure only the L1 recipient can claim it, hash the `token_id` together with the `recipient`: + +#include_code exit /docs/examples/contracts/nft_bridge/src/main.nr rust + +Cross-chain messaging on Aztec is powerful because it doesn't conform to any specific format—you can structure messages however you want. + +:::tip Private Functions + +Both `claim` and `exit` are `#[external("private")]`, which means the bridging process is private—nobody can see who's bridging which NFT by watching the chain. + +::: + +:::note Clear the scaffold's placeholder test +The scaffolded `nft_bridge_test/src/lib.nr` imports the default contract name (`Main`), which no longer exists, so it fails to compile with "Could not resolve 'Main'". Tests aren't used in this tutorial, so replace its contents with a single-line stub to keep `aztec compile` clean: + +```rust +// Tests are out of scope for this tutorial. See https://docs.aztec.network/developers/docs/aztec-nr/testing_contracts for examples. +``` +::: + +### Compile the Bridge + +```bash +aztec compile +``` + +Bridge compiled successfully! Now process both contracts and generate TypeScript bindings: + +```bash +cd ../nft +aztec codegen target --outdir ../artifacts + +cd ../nft_bridge +aztec codegen target --outdir ../artifacts +``` + +An `artifacts` folder should appear with TypeScript bindings for each contract. You'll use these when deploying the contracts. + +## Part 3: The Ethereum Side + +Now build the L1 contracts. You need: + +- A simple ERC721 NFT contract (the "CryptoPunk") +- A portal contract that locks/unlocks NFTs and communicates with Aztec + +### Install Dependencies + +Aztec's contracts are already in your `package.json`. You just need to add the OpenZeppelin contracts that provide the default ERC721 implementation: + +```bash +cd ../../.. +yarn add @openzeppelin/contracts +``` + +### Create a Simple NFT + +Delete the "Counter" contracts that show up by default in `contracts` and create `contracts/SimpleNFT.sol`: + +```bash +touch contracts/SimpleNFT.sol +``` + +Create a minimal NFT contract sufficient for demonstrating bridging: + +#include_code simple_nft /docs/examples/solidity/nft_bridge/SimpleNFT.sol solidity + +### Create the NFT Portal + +The NFT Portal has more code, so build it step-by-step. Create `contracts/NFTPortal.sol`: + +```bash +touch contracts/NFTPortal.sol +``` + +Initialize it with Aztec's registry, which holds the canonical contracts for Aztec-related contracts, including the Inbox and Outbox. These are the message-passing contracts—Aztec sequencers read any messages on these contracts. + +```solidity +#include_code portal_setup /docs/examples/solidity/nft_bridge/NFTPortal.sol raw +} +``` + +The core logic is similar to the L2 logic. `depositToAztec` calls the `Inbox` canonical contract to send a message to Aztec, and `withdraw` calls the `Outbox` contract. + +Add these two functions with explanatory comments: + +#include_code portal_deposit_and_withdraw /docs/examples/solidity/nft_bridge/NFTPortal.sol solidity + +The portal handles two flows: + +- **depositToAztec**: Locks NFT on L1, sends message to L2 +- **withdraw**: Verifies L2 message, unlocks NFT on L1 + +### Compile + +Let's make sure everything compiles: + +```bash +npx hardhat compile +``` + +You should see successful compilation of both contracts! + +## Part 4: Compiling, Deploying, and Testing + +Now deploy everything and test the full flow. This will help you understand how everything fits together. + +Delete the placeholders in `scripts` and create `index.ts`: + +```bash +touch scripts/index.ts +``` + +This script will implement the user flow. + +:::warning Testnet + +This section assumes you're working locally using the local network. For the testnet, you need to account for some things: + +- Your clients need to point to some Sepolia Node and to the public Aztec Full Node +- You need to [deploy your own Aztec accounts](../../aztec-js/how_to_create_account.md) +- You need to pay fees in some other way. Learn how in the [fees guide](../../aztec-js/how_to_pay_fees.md) + +::: + +### Deploying and Initializing + +First, initialize the clients: `aztec.js` for Aztec and `viem` for Ethereum: + +#include_code setup /docs/examples/ts/token_bridge/index.ts typescript + +:::warning Adjust the artifact imports for this project's layout +The snippet above comes from the monorepo's runnable example, and its artifact imports point at that repo's layout. In the Hardhat project used in this tutorial, replace the four artifact imports with: + +```typescript +import NFTPortal from "../artifacts/contracts/NFTPortal.sol/NFTPortal.json" with { type: "json" }; +import SimpleNFT from "../artifacts/contracts/SimpleNFT.sol/SimpleNFT.json" with { type: "json" }; +import { NFTBridgeContract } from "../contracts/aztec/artifacts/NFTBridge.js"; +import { NFTPunkContract } from "../contracts/aztec/artifacts/NFTPunk.js"; +``` + +`npx hardhat compile` writes the Solidity artifacts to `artifacts/contracts/`, and the `aztec codegen` commands from earlier wrote the TypeScript bindings to `contracts/aztec/artifacts/`. Hardhat artifacts also store the bytecode as a plain string, so in the deployment snippet below use `SimpleNFT.bytecode` and `NFTPortal.bytecode` instead of `.bytecode.object`. +::: + +You now have wallets for both chains, correctly connected to their respective chains. Next, deploy the L1 contracts: + +#include_code deploy_l1_contracts /docs/examples/ts/token_bridge/index.ts typescript + +Now deploy the L2 contracts. Thanks to the TypeScript bindings generated with `aztec codegen`, deployment is straightforward: + +#include_code deploy_l2_contracts /docs/examples/ts/token_bridge/index.ts typescript + +Now that you have the L2 bridge's contract address, initialize the L1 bridge: + +#include_code initialize_portal /docs/examples/ts/token_bridge/index.ts typescript + +The L2 contracts were already initialized when you deployed them, but you still need to: + +- Tell the L2 bridge about Ethereum's portal address (by calling `set_portal` on the bridge) +- Tell the L2 NFT contract who the minter is (by calling `set_minter` on the L2 NFT contract) + +Complete these initialization steps: + +#include_code initialize_l2_bridge /docs/examples/ts/token_bridge/index.ts typescript + +This completes the setup. It's a lot of configuration, but you're dealing with four contracts across two chains. + +### L1 → L2 Flow + +Now for the main flow. Mint a CryptoPunk on L1, deposit it to Aztec, and claim it on Aztec. Put everything in the same script. To mint, call the L1 contract with `mint`, which will mint `tokenId = 0`: + +#include_code mint_nft_l1 /docs/examples/ts/token_bridge/index.ts typescript + +To bridge, first approve the portal address to transfer the NFT, then transfer it by calling `depositToAztec`: + +#include_code deposit_to_aztec /docs/examples/ts/token_bridge/index.ts typescript + +The `Inbox` contract will emit an important log: `MessageSent(hash, inboxRollingHash, bucketSeq, message);`. The `message.index` field is the **leaf index** of the message in the [L1-L2 Message Tree](../../foundational-topics/ethereum-aztec-messaging/index.md)—the location of the message in the tree that will appear on L2. You need this index, plus the secret, to correctly claim and decrypt the message. + +Use viem to extract this information: + +#include_code get_message_leaf_index /docs/examples/ts/token_bridge/index.ts typescript + +This extracts the logs from the deposit and retrieves the leaf index. You can now claim it on L2. However, a message can only be claimed once an L2 block includes it, and the network waits until the message is at least 12 seconds old before including it, so expect 12 to 30 seconds of latency. If you called `claim` on the L2 contract immediately, it would return "no message available". + +On a local network blocks are only produced when transactions are submitted, so add a utility function that forces a couple of blocks (it deploys a contract with a random salt): + +#include_code mine_blocks /docs/examples/ts/token_bridge/index.ts typescript + +Now claim the message on L2: + +#include_code claim_on_l2 /docs/examples/ts/token_bridge/index.ts typescript + +### L2 → L1 Flow + +Great! You can expand the L2 contract to add features like NFT transfers. For now, exit the NFT on L2 and redeem it on L1. Mine two blocks because of `DelayedMutable`: + +#include_code exit_from_l2 /docs/examples/ts/token_bridge/index.ts typescript + +Just like in the L1 → L2 flow, you need to know what to claim on L1. Where in the message tree is the message you want to claim? Use `node.getL2ToL1MembershipWitness`, which provides the leaf and the sibling path of the message: + +#include_code get_withdrawal_witness /docs/examples/ts/token_bridge/index.ts typescript + +With this information, call the L1 contract and use the index and the sibling path to claim the L1 NFT: + +#include_code withdraw_on_l1 /docs/examples/ts/token_bridge/index.ts typescript + +You can now try the whole flow with: + +```bash +npx tsx scripts/index.ts +``` + +## What You Built + +A complete private NFT bridge with: + +1. **L1 Contracts** (Solidity) + - `SimpleNFT`: Basic ERC721 for testing + - `NFTPortal`: Locks/unlocks NFTs and handles L1↔L2 messaging + +2. **L2 Contracts** (Noir) + - `NFTPunk`: Private NFT with encrypted ownership using `PrivateSet` + - `NFTBridge`: Claims L1 messages and mints NFTs privately + +3. **Full Flow** + - Mint NFT on L1 + - Deploy portal and bridge + - Lock NFT on L1 → message sent to L2 + - Claim on L2 → private NFT minted + - Later: Burn on L2 → message to L1 → unlock + +## Going Further: The AIP-721 NFT Standard + +The NFTPunk contract you built in this tutorial implements a simplified NFT with private ownership. The **AIP-721** standard formalizes these patterns and adds partial-note transfers, commitment-based handoffs, all 7 cross-domain transfer patterns, and authwit-based authorization. + +Read the full [AIP-721 standard reference](../../aztec-nr/standards/aip-721.md) for details, or explore all [Aztec Contract Standards](../../aztec-nr/standards/index.md). + +## Next Steps + +- Add a web frontend for easy bridging +- Implement batch bridging for multiple NFTs +- Add metadata bridging +- Write comprehensive tests +- Add proper access controls +- Explore the [AIP-721 standard](../../aztec-nr/standards/aip-721.md) for production-grade NFT patterns + +:::tip Learn More + +- [State management page](../../foundational-topics/state_management.md) +- [Cross-chain messaging](../../foundational-topics/ethereum-aztec-messaging/index.md) +- [Aztec Contract Standards](../../aztec-nr/standards/index.md) + ::: diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/uniswap_swap.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/uniswap_swap.md new file mode 100644 index 00000000000..8d7dacfa9fe --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/uniswap_swap.md @@ -0,0 +1,411 @@ +--- +title: "Cross-Chain Token Swap (L1 <> L2)" +sidebar_position: 2 +description: "Build a cross-chain token swap that exits L2, swaps on L1, and deposits the output back to L2 using Aztec's messaging protocol." +references: ["docs/examples/contracts/example_uniswap/*", "docs/examples/solidity/example_swap/*", "docs/examples/ts/example_swap/*"] +--- + +## Why Build a Cross-Chain Swap? + +DeFi liquidity lives on Ethereum L1. Users with tokens on Aztec L2 need a way to access L1 DEXs like Uniswap without manually bridging tokens back and forth. A cross-chain swap automates this: the user initiates the swap on L2, and the protocol handles exiting to L1, performing the swap, and depositing the output back to L2. + +This tutorial walks you through building a version of this flow. You will learn how [**L2-to-L1 messages**](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) work and how multiple contracts across two chains coordinate to execute a single user action. + +## Prerequisites + +Before starting this tutorial, you need: + +1. **Aztec local network** running at version #include_aztec_version -- see [the local network guide](../../../getting_started_on_local_network.md) for setup instructions +2. **Node.js** (v24+) and a package manager (yarn or npm) +3. **Familiarity with the token bridge tutorial** -- this tutorial builds on concepts from [Bridge Your NFT to Aztec](./token_bridge.md), especially portal contracts and cross-chain messaging + +:::tip Background Knowledge +If you are new to Aztec's cross-chain architecture, review [Ethereum-Aztec Messaging](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) first. Key concepts used throughout this tutorial: + +- **Portals** -- L1 contracts that communicate with L2 contracts via the Aztec messaging protocol +- **L2-to-L1 messages** -- messages sent from Aztec to Ethereum, stored in a Merkle tree and consumed on L1 +- **Authorization witnesses (authwit)** -- Aztec's alternative to ERC20 approve/transferFrom ([learn more](../../aztec-nr/framework-description/authentication_witnesses.md)) +- **Content hashes** -- cryptographic digests that uniquely identify cross-chain messages, ensuring L1 and L2 agree on message parameters +::: + +## Project Setup + +This tutorial walks you through three types of contracts (Solidity, Noir, TypeScript) that work together. You will clone the example project and build it as you follow along. + +### Clone the Example Code + +The example code lives in the Aztec packages repository: + +```bash +git clone --depth 1 https://github.com/AztecProtocol/aztec-packages.git +cd aztec-packages +labs-patches/bootstrap.sh apply +(cd docs && yarn install) +cd docs/examples +``` + +`labs-patches/bootstrap.sh apply` checks out the `labs/` submodule, whose `aztec-nr` and token contracts the Noir contract builds against. `yarn install` in `docs/` installs `@aztec-foundation/l1-artifacts`, the L1 contract sources that `foundry.toml` maps. + +### Project Structure + +The relevant files are spread across three directories: + +``` +examples/ +├── contracts/ +│ └── example_uniswap/ +│ ├── Nargo.toml # Noir package config +│ └── src/ +│ ├── main.nr # L2 uniswap contract +│ └── util.nr # Content hash helpers +├── solidity/ +│ ├── foundry.toml # Solidity compiler config +│ └── example_swap/ +│ ├── ExampleERC20.sol # Minimal ERC20 tokens (WETH, DAI) +│ ├── ExampleTokenPortal.sol # L1 token bridge portal +│ └── ExampleUniswapPortal.sol # L1 swap orchestrator +└── ts/ + └── example_swap/ + ├── index.ts # TypeScript orchestration script + └── config.yaml # Build configuration +``` + +### Dependencies + +**Solidity** (via Foundry import mappings in `foundry.toml`): +- OpenZeppelin ERC20 (`@oz/token/ERC20/`) +- Aztec L1 contracts (`@aztec/core`, `@aztec/governance`) + +**Noir** (in `Nargo.toml`): +- `aztec` -- the Aztec Noir framework +- `token` -- the standard Token contract +- `token_bridge` -- the standard TokenBridge contract +- `keccak256` -- for computing Solidity-compatible function selectors + +**TypeScript**: +- `@aztec-labs/aztec.js`, `@aztec-labs/accounts`, `@aztec-labs/wallets`, `@aztec-labs/stdlib` +- `@aztec-labs/ethereum`, `@aztec-labs/noir-contracts.js`, `@aztec-labs/foundation` + +:::note +The TypeScript script imports compiled Solidity artifacts and a generated Noir artifact (`ExampleUniswapContract`). You will compile both before running the script. +::: + +## What You'll Build + +```mermaid +graph TB + subgraph L2["Aztec (L2)"] + User["User"] + UniL2["ExampleUniswap<br/>(Noir)"] + BridgeA["TokenBridge A"] + BridgeB["TokenBridge B"] + TokenA["Token A (WETH)"] + TokenB["Token B (DAI)"] + end + + subgraph L1["Ethereum (L1)"] + UniL1["ExampleUniswapPortal<br/>(Solidity)"] + PortalA["ExampleTokenPortal A"] + PortalB["ExampleTokenPortal B"] + ERC20A["WETH (ERC20)"] + ERC20B["DAI (ERC20)"] + Outbox["Outbox"] + end + + User -->|"1. swap_public()"| UniL2 + UniL2 -->|"2. Transfer WETH"| TokenA + TokenA -->|"3. Return WETH"| UniL2 + UniL2 -->|"4. Exit to L1"| BridgeA + BridgeA -->|"5. Bridge exit msg"| PortalA + UniL2 -->|"6. Swap intent msg"| UniL1 + + UniL1 -->|"7. Consume bridge exit"| PortalA + PortalA -->|"8. Release WETH"| ERC20A + ERC20A -->|"9. WETH to swap"| UniL1 + UniL1 -->|"10. Consume swap intent"| Outbox + UniL1 -->|"11. Mock swap DAI"| ERC20B + UniL1 -->|"12. Deposit DAI"| PortalB + + PortalB -->|"13. L1→L2 msg"| BridgeB + BridgeB -->|"14. Mint DAI"| TokenB + TokenB -->|"15. DAI to user"| User +``` + +Each swap generates **two L2-to-L1 messages**, both of which must be consumed on L1 before the swap executes: + +1. **Token bridge exit** - Authorizes releasing input tokens from the token portal to the uniswap portal +2. **Swap intent** - Proves the user authorized *this specific swap* with *these exact parameters* + +Neither message alone is sufficient. If only the token exit existed, anyone observing it could potentially redirect the swap. If only the swap intent existed, there would be no proof that tokens were actually withdrawn. Together, they create a cryptographic chain of authorization. This two-message pattern is common in Aztec cross-chain applications where multiple independent systems must coordinate. + +## Part 1: Token Portal (Solidity) + +The token portal handles depositing tokens from L1 to L2 and withdrawing from L2 to L1. This is a simplified version for tutorial purposes -- for a deeper look at how portals work, see the [token bridge tutorial](./token_bridge.md). + +#include_code example_token_portal /docs/examples/solidity/example_swap/ExampleTokenPortal.sol solidity + +Key functions: + +- `depositToAztecPublic` - Locks ERC20 tokens and sends an L1→L2 message for public minting +- `depositToAztecPrivate` - Same but for private minting +- `withdraw` - Consumes an L2→L1 message and releases tokens + +The [registry](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) provides governance-updateable addresses for core Aztec contracts. Rather than hardcoding rollup addresses, portals query the registry, allowing the protocol to upgrade without redeploying all portals. + +Each cross-chain message includes a **content hash** -- a `sha256` digest of the function selector and its parameters that uniquely identifies the message. The content hash is computed with `Hash.sha256ToField(abi.encodeWithSignature(...))`, where `abi.encodeWithSignature` prepends a 4-byte function selector (keccak256 of the function signature, per Solidity convention). This makes each message type unique, preventing a deposit message from being confused with a withdrawal message. + +#include_code deposit_to_aztec_public /docs/examples/solidity/example_swap/ExampleTokenPortal.sol solidity + +#include_code withdraw /docs/examples/solidity/example_swap/ExampleTokenPortal.sol solidity + +## Part 2: Uniswap Portal (Solidity) + +The uniswap portal orchestrates the swap on L1. It consumes two L2→L1 messages, performs the swap, and deposits the output back to L2. + +:::note Mock Swap +This tutorial uses a mock 1:1 swap instead of a real Uniswap V3 router. The portal must be pre-funded with output tokens. The important part is the **message-passing pattern**, not the swap itself. +::: + +#include_code example_uniswap_portal /docs/examples/solidity/example_swap/ExampleUniswapPortal.sol solidity + +The public swap function consumes two messages and deposits the output: + +#include_code swap_public /docs/examples/solidity/example_swap/ExampleUniswapPortal.sol solidity + +The private swap follows the same pattern but deposits output tokens privately: + +#include_code swap_private /docs/examples/solidity/example_swap/ExampleUniswapPortal.sol solidity + +### Compile Solidity Contracts + +With all Solidity contracts from Part 1 and Part 2 ready, compile them using Foundry (`forge`, or the `aztec-forge` the Aztec installer ships). From the `examples` directory: + +```bash +cd solidity +forge build --contracts example_swap --out ../../target/solidity/example_swap +``` + +This writes JSON artifacts containing the ABI and bytecode to `docs/target/solidity/example_swap/`, where the TypeScript script imports them from to deploy contracts on L1. The imports resolve from the `@aztec-foundation/l1-artifacts` package that `yarn install` put in `docs/node_modules`. + +## Part 3: Uniswap Contract (Noir) + +The L2 contract handles the user-facing logic: transferring input tokens, calling the bridge to exit to L1, and creating the swap intent message. + +### Setup + +The contract stores the portal address and imports the `Token` and `TokenBridge` contracts: + +#include_code example_uniswap_setup /docs/examples/contracts/example_uniswap/src/main.nr rust + +### Public Swap + +The public swap transfers tokens from the sender to the contract, exits them to L1 via the bridge, and sends a swap intent message: + +:::note Authorization Witnesses +Aztec uses [**authorization witnesses** (authwit)](../../aztec-nr/framework-description/authentication_witnesses.md) instead of the ERC20 approve/transferFrom pattern. The contract computes the hash of the exact action it wants to perform, sets that hash as authorized, then immediately performs the action. This gives fine-grained control - the authorization is for a specific action, not a blanket approval. Since we authorize and spend in the same transaction, replay attacks are impossible. +::: + +#include_code swap_public /docs/examples/contracts/example_uniswap/src/main.nr rust + +### Private Swap + +The private swap is similar but uses `transfer_to_public` (private to public transfer) and [`enqueue_self`](../../aztec-nr/framework-description/calling_contracts.md) instead of `call_self`. Because `swap_private` is a private function and `_approve_bridge_and_exit_input_asset_to_L1` is public, it cannot be called synchronously -- private functions execute before public functions in a transaction. `enqueue_self` schedules the public call to run in the public phase of the same transaction: + +#include_code swap_private /docs/examples/contracts/example_uniswap/src/main.nr rust + +:::note Why no recipient parameter? +In `swap_private`, the recipient is the person that provides the secret used to generate the hash for the L1 to L2 message. This preserves privacy: revealing a recipient address to L1 would compromise the caller's identity. The output tokens are deposited privately to L2, where only the secret holder can claim them. +::: + +### Bridge Helper + +Both flows share this internal function that approves the bridge to burn tokens and exits them to L1: + +#include_code approve_bridge_and_exit /docs/examples/contracts/example_uniswap/src/main.nr rust + +:::note Portal Address Validation +The portal address checks are a **safety mechanism**. If either portal is zero (not configured), the funds would be permanently lost. Always validate external addresses before sending irreversible messages. +::: + +:::note Fixed nonce safety +The fixed nonce `0xdeadbeef` used throughout this contract is safe because authorization and token spending occur in the same transaction. There's no opportunity for replay attacks since the authorization is set and consumed atomically. +::: + +### Content Hash Helpers + +These content hashes form the **cross-chain contract interface**. The L2 contract computes a hash of all swap parameters, and the L1 portal reconstructs the same hash from the parameters it receives. If they don't match exactly, the message consumption fails. + +This is how L1 verifies that L2 actually authorized the swap - not by trusting a signature, but by independently computing what the message should contain. The hashes must match exactly between L2 (Noir) and L1 (Solidity): + +#include_code swap_public_content_hash /docs/examples/contracts/example_uniswap/src/util.nr rust + +#include_code swap_private_content_hash /docs/examples/contracts/example_uniswap/src/util.nr rust + +### Compile and Generate Bindings + +From the `docs` directory (the Noir workspace root), compile the Noir contract and generate TypeScript bindings: + +```bash +cd ../.. +aztec compile --package example_uniswap +aztec codegen target/example_uniswap-ExampleUniswap.json -o examples/ts/example_swap/artifacts +``` + +Compile from the workspace root rather than inside the contract's directory: the artifact lands in `docs/target/`, and compiling from the root is what transpiles its public functions for the AVM. The `aztec codegen` command generates a TypeScript class (`ExampleUniswapContract`) from the compiled artifact, which you will use in the deployment script. + +:::note +Before proceeding, make sure you have compiled both the Solidity contracts (Part 1-2) and the Noir contract (Part 3). The TypeScript script below imports compiled artifacts from both. +::: + +## Install TypeScript Dependencies + +From the `examples/ts/example_swap` directory, initialize a project as an ES module (the script uses top-level `await`) and install the required packages: + +```bash +cd examples/ts/example_swap +npm init -y +npm pkg set type=module +npm install \ + @aztec-labs/aztec.js@#include_aztec_version \ + @aztec-labs/accounts@#include_aztec_version \ + @aztec-labs/wallets@#include_aztec_version \ + @aztec-labs/stdlib@#include_aztec_version \ + @aztec-labs/ethereum@#include_aztec_version \ + @aztec-labs/noir-contracts.js@#include_aztec_version \ + @aztec-labs/foundation@#include_aztec_version \ + npm:@aztec/viem@2.38.2 \ + tsx +``` + +## Part 4: Public Swap Flow (TypeScript) + +Now you can tie everything together in a TypeScript script. Start by setting up clients and deploying all contracts: + +#include_code setup /docs/examples/ts/example_swap/index.ts typescript + +### Deploy L1 Contracts + +Deploy two ERC20 tokens, two token portals, and the uniswap portal: + +#include_code deploy_l1 /docs/examples/ts/example_swap/index.ts typescript + +### Deploy L2 Contracts + +Deploy L2 tokens (using `TokenContract` from `@aztec-labs/noir-contracts.js`), bridges, and the uniswap contract: + +#include_code deploy_l2 /docs/examples/ts/example_swap/index.ts typescript + +### Initialize and Fund + +Initialize all portals and mint tokens: + +#include_code initialize /docs/examples/ts/example_swap/index.ts typescript + +Fund the user with input tokens and pre-fund the uniswap portal with output tokens: + +#include_code fund /docs/examples/ts/example_swap/index.ts typescript + +### Deposit to L2 + +Bridge WETH from L1 to L2: + +#include_code deposit_to_l2 /docs/examples/ts/example_swap/index.ts typescript + +:::tip Why use a secret hash? +When depositing from L1 to L2, we use a secret/secret-hash pattern: generate a random secret on the client, send only the hash to L1 (in the deposit transaction), then later reveal the secret on L2 to claim the tokens. This prevents **front-running attacks**: a malicious sequencer (the node that orders and processes L2 transactions) cannot observe the L1 deposit and claim the tokens themselves because they don't know the secret. Only someone who knows the preimage can claim. +::: + +Before claiming, we need to mine a couple of L2 blocks. An L1-to-L2 message is not available the moment it is sent -- the rollup only includes it in an L2 block once it is at least 12 seconds old, and it becomes consumable as soon as that block lands. We use a helper that deploys throwaway contracts to force these blocks: + +#include_code mine_blocks /docs/examples/ts/example_swap/index.ts typescript + +Claim the deposited tokens on L2: + +#include_code claim_on_l2 /docs/examples/ts/example_swap/index.ts typescript + +### Execute the Swap + +Initiate the swap on L2: + +#include_code public_swap /docs/examples/ts/example_swap/index.ts typescript + +### Waiting for Block Proofs + +L2→L1 messages can only be consumed on L1 after the L2 block containing them has been **proven**. Aztec batches blocks into [**epochs**](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) and generates ZK proofs for each epoch. The proof confirms that the L2 state transition (including our swap messages) actually happened according to the protocol rules. Until the proof is submitted to L1, the messages exist but cannot be trusted. + +#include_code wait_for_proof /docs/examples/ts/example_swap/index.ts typescript + +The [outbox](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) stores L2→L1 messages in a Merkle tree. To consume a message, you must provide the **epoch** (which proof batch contains the message), the **leaf index** (position in the message tree), and the **sibling path** (Merkle proof showing the message is in the tree). These parameters are computed offchain by observing L2 blocks. + +First, read the rollup version from the portal and compute the content hash and message leaf for the token bridge exit message: + +#include_code consume_l1_messages_setup /docs/examples/ts/example_swap/index.ts typescript + +Next, compute Merkle membership witnesses for both L2→L1 messages -- the sibling path (proof of inclusion) for each. We also compute the swap intent message leaf using the same encoding as the Solidity portal: + +#include_code consume_l1_messages_witnesses /docs/examples/ts/example_swap/index.ts typescript + +Next, call `swapPublic` on the L1 uniswap portal, passing both message proofs. The portal verifies both messages against the outbox, performs the mock swap, and deposits the output tokens back to L2: + +#include_code consume_l1_messages_execute /docs/examples/ts/example_swap/index.ts typescript + +Finally, claim the output DAI on L2: + +#include_code claim_output /docs/examples/ts/example_swap/index.ts typescript + +### Run It + +Start a local Aztec network in one terminal, then execute the script in another. Make sure you are in the `examples/ts/example_swap` directory: + +```bash +# Terminal 1: Start a local network +aztec start --local-network + +# Terminal 2: From examples/ts/example_swap, run the swap script +npx tsx index.ts +``` + +You should see console output tracing each step: deploying contracts, depositing to L2, initiating the swap, waiting for the proof, consuming messages on L1, and claiming the output DAI on L2. + +## Public vs Private Comparison + +| Aspect | Public Swap | Private Swap | +|--------|------------|-------------| +| **L2 function** | `swap_public()` | `swap_private()` | +| **Token transfer** | `transfer_in_public` (public→public) | `transfer_to_public` (private→public) | +| **Bridge call** | `call_self` (immediate) | `enqueue_self` (deferred) | +| **L1 deposit** | `depositToAztecPublic` | `depositToAztecPrivate` | +| **L2 claim** | `claim_public` | `claim_private` | +| **Visibility** | Swap amount and recipient visible | Swap amount visible, recipient hidden | + +The private flow hides *who* is swapping, but the amounts are visible on L1 because the token bridge exit is a public operation. The output deposit can be claimed privately, so the final recipient is hidden. + +## What You Built + +A complete cross-chain token swap system with: + +1. **L1 Contracts** (Solidity) + - `ExampleERC20`: Minimal ERC20 tokens for testing + - `ExampleTokenPortal`: Handles L1↔L2 token deposits and withdrawals + - `ExampleUniswapPortal`: Orchestrates the swap by consuming two L2→L1 messages + +2. **L2 Contract** (Noir) + - `ExampleUniswap`: User-facing contract that initiates the swap, exits tokens to L1, and sends the swap intent message + +3. **Message Flow** + - User calls `swap_public` on L2 + - Two L2→L1 messages are created (bridge exit + swap intent) + - L1 portal consumes both messages, swaps, and deposits output back to L2 + - User claims output tokens on L2 + +## Next Steps + +- Extend with a real Uniswap V3 integration instead of the mock swap +- Add slippage protection with meaningful `minimum_output_amount` values +- Implement the private swap flow end-to-end in the TypeScript script +- Explore [cross-chain messaging](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) in depth + +:::tip Learn More +- [Token bridge tutorial](./token_bridge.md) - NFT bridge example +- [Cross-chain messaging reference](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) +::: diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/01-architecture.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/01-architecture.md new file mode 100644 index 00000000000..bb5bf874ac2 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/01-architecture.md @@ -0,0 +1,184 @@ +--- +title: "1. Extension Architecture" +description: Understanding Chrome extension architecture for Aztec wallets - service workers, offscreen documents, and message passing +sidebar_position: 1 +references: ["docs/examples/webapp-tutorial/test-extension/src/content-script.ts", "docs/examples/webapp-tutorial/test-extension/src/background.ts", "docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts", "docs/examples/webapp-tutorial/test-extension/src/config.ts"] +--- + +# Extension Architecture + +Browser extension wallets face unique challenges that don't exist in embedded wallets. This section explains why you need a multi-component architecture and how the pieces fit together. + +## The Service Worker Problem + +Chrome's Manifest V3 requires extensions to use **service workers** instead of persistent background pages. Service workers have limitations that affect wallet development: + +1. **5-minute timeout** - Service workers terminate after 5 minutes of inactivity +2. **No DOM access** - Can't use DOM APIs or run WASM directly +3. **Limited storage** - Must use async storage APIs like `chrome.storage` +4. **Cold starts** - Must reinitialize state when waking up + +For a wallet, these limitations are problematic because: + +- **PXE needs persistence** - The Private eXecution Environment maintains Merkle tree state +- **Proof generation takes time** - Can exceed the 5-minute timeout +- **WASM is essential** - Aztec's cryptographic operations use WASM + +## The Solution: Offscreen Documents + +Manifest V3 introduced **offscreen documents** as a way to handle operations that service workers can't: + +```javascript +// In background.ts (service worker) +await chrome.offscreen.createDocument({ + url: 'offscreen.html', + reasons: [chrome.offscreen.Reason.WORKERS], + justification: 'Aztec PXE requires long-running WASM operations', +}); +``` + +Offscreen documents: +- Run longer than service workers +- Support WASM and IndexedDB +- Can maintain state across requests +- Are invisible to users (no UI) + +## Component Responsibilities + +### Content Script (`content-script.ts`) + +The content script runs in the context of every web page. Its only job is to relay messages: + +#include_code content-script docs/examples/webapp-tutorial/test-extension/src/content-script.ts typescript + +It uses the `ContentScriptConnectionHandler` from the wallet SDK, which: +- Listens for messages from the page (dApp) +- Forwards them to the background service worker +- Relays responses back to the page + +### Service Worker (`background.ts`) + +The service worker handles the wallet SDK protocol and coordinates between components: + +#include_code offscreen-management docs/examples/webapp-tutorial/test-extension/src/background.ts typescript + +Key responsibilities: +- **Protocol handling** - Discovery, key exchange, session management +- **Offscreen lifecycle** - Creating/checking the offscreen document +- **Message routing** - Forwarding wallet calls to offscreen +- **User approvals** - Triggering popups for connection/transaction approval + +### Offscreen Document (`offscreen.ts`) + +The offscreen document is where the heavy lifting happens: + +#include_code pxe-instance docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript + +It hosts: +- **PXE instance** - The Private eXecution Environment +- **Wallet implementation** - The `OffscreenWallet` that extends `BaseWallet` +- **Method handlers** - All wallet operations (send, simulate, sign) + +### Popup (`popup.tsx`) + +The popup provides the user interface for: +- Viewing accounts +- Creating new accounts +- Deploying account contracts +- Approving connections +- Approving transactions + +## Message Flow + +Here's how a transaction flows through the system: + +```text +1. dApp calls wallet.sendTx(...) + ↓ +2. Page postMessage to content script + ↓ +3. Content script → chrome.runtime.sendMessage → Background + ↓ +4. Background checks: requires approval? + ├─ No: Forward directly to offscreen + └─ Yes: Store pending, update badge, wait for popup approval + ↓ +5. User clicks extension icon, sees pending tx + ↓ +6. User clicks "Approve" + ↓ +7. Popup → chrome.runtime.sendMessage → Background + ↓ +8. Background → persistent port → Offscreen + ↓ +9. Offscreen executes: wallet.sendTx(...) + - Creates execution request + - Generates proof (WASM, can take time) + - Submits to node + ↓ +10. Response flows back: Offscreen → port → Background → Content → Page +``` + +The extension uses two messaging strategies: +- **Persistent ports** (`chrome.runtime.connect`) for background ↔ offscreen and background ↔ popup. Ports provide point-to-point channels with automatic disconnect detection. +- **One-shot messages** (`chrome.runtime.sendMessage`) for content script → background and popup → background requests. These are broadcast messages filtered by a `target` field. + +## The Manifest + +The `manifest.json` declares all components: + +```json +{ + "manifest_version": 3, + "name": "Aztec Tutorial Wallet", + "version": "1.0.0", + "permissions": ["storage", "offscreen"], + "background": { + "service_worker": "dist/background.js", + "type": "module" + }, + "content_scripts": [{ + "matches": ["<all_urls>"], + "js": ["dist/content-script.js"], + "run_at": "document_start" + }], + "action": { + "default_popup": "popup/popup.html" + } +} +``` + +:::note +This is a simplified manifest. The full version in the example project includes additional permissions (`alarms`, `notifications`, `windows`), `host_permissions`, `content_security_policy` for WASM, and `web_accessible_resources` for WASM files. +::: + +Key points: +- `"offscreen"` permission enables offscreen document creation +- `"storage"` permission for encrypted key storage +- `"type": "module"` enables ES modules in the service worker +- `"run_at": "document_start"` ensures content script loads early + +## Configuration + +Constants are centralized in `config.ts`: + +#include_code wallet-config docs/examples/webapp-tutorial/test-extension/src/config.ts typescript + +This keeps configuration in one place and provides typed message constants. + +## Building the Extension + +The build uses a two-step process orchestrated by `esbuild.extension.mjs`: + +1. **Vite** bundles the background script, offscreen document, and popup — with React support, node polyfills, and a custom plugin that patches Barretenberg worker files for `crossOriginIsolated` (see `vite.extension.config.ts`) +2. **esbuild** bundles the content script separately as IIFE (Chrome content scripts don't support ES modules) + +```bash +node esbuild.extension.mjs +``` + +The script also copies static files (offscreen HTML, WASM binaries) to the correct locations. + +## Next Steps + +Now that you understand the architecture, let's implement the [Wallet Protocol](./02-wallet-protocol.md) - the discovery and key exchange that establishes secure connections between dApps and the wallet. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/02-wallet-protocol.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/02-wallet-protocol.md new file mode 100644 index 00000000000..5de276c4e27 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/02-wallet-protocol.md @@ -0,0 +1,212 @@ +--- +title: "2. Wallet Protocol" +description: Implementing the Aztec wallet SDK protocol - discovery, ECDH key exchange, and secure messaging +sidebar_position: 2 +references: ["docs/examples/webapp-tutorial/test-extension/src/background.ts", "labs/yarn-project/wallet-sdk/src/*"] +--- + +# Wallet Protocol + +The Aztec wallet SDK defines a protocol for dApps to discover and communicate with wallets securely. This section covers how the extension implements this protocol. + +## Protocol Overview + +The wallet SDK protocol has three phases: + +1. **Discovery** - dApp broadcasts a request, wallets respond with their info +2. **Key Exchange** - ECDH establishes a shared secret for encrypted messaging +3. **Secure Messaging** - All subsequent messages are encrypted + +This design: +- Works with any number of wallets +- Prevents eavesdropping on wallet calls +- Allows user verification (emoji codes) + +## Discovery Phase + +When a dApp calls `aztec.connect()`, it broadcasts a discovery request: + +```typescript +// From dApp (simplified) +const wallet = await aztec.connect({ + appId: 'my-dapp', + chainInfo: { chainId: 31337, version: 1 }, +}); +``` + +The content script receives this and forwards it to the background: + +```typescript +// Content script forwards to background +chrome.runtime.sendMessage({ + type: 'DISCOVERY_REQUEST', + content: { appId, chainInfo, requestId }, +}); +``` + +The background service worker handles it: + +#include_code callbacks docs/examples/webapp-tutorial/test-extension/src/background.ts typescript + +Key points: +- Each discovery becomes a "pending discovery" awaiting user approval +- The `BackgroundConnectionHandler` from wallet SDK manages the protocol state +- `onPendingDiscovery` callback lets us show the connection request to users + +## Connection Approval + +When the user clicks "Connect" in the popup: + +#include_code popup-messages docs/examples/webapp-tutorial/test-extension/src/background.ts typescript + +The `handler.approveDiscovery()` call: +1. Marks the discovery as approved +2. Sends a discovery response to the dApp with wallet info +3. Triggers the key exchange phase + +## Key Exchange Phase + +After approval, the dApp initiates key exchange by sending its ECDH public key. The `BackgroundConnectionHandler` handles this automatically — the pseudocode below shows the conceptual flow, not code you write: + +```typescript +// Conceptual flow inside BackgroundConnectionHandler (from wallet SDK) +async handleKeyExchangeRequest(sessionId, request) { + // Generate our ECDH key pair + const keyPair = await generateKeyPair(); + const publicKey = await exportPublicKey(keyPair.publicKey); + + // Derive shared secret from their public key + const appPublicKey = await importPublicKey(request.publicKey); + const sessionKeys = await deriveSessionKeys(keyPair, appPublicKey, false); + + // Store session with shared encryption key + const session = { + sessionId, + sharedKey: sessionKeys.encryptionKey, + verificationHash: sessionKeys.verificationHash, // For emoji display + // ... + }; + + // Send our public key back + this.transport.sendToTab(tabId, { + type: 'KEY_EXCHANGE_RESPONSE', + publicKey, + }); +} +``` + +The key exchange uses: +- **ECDH** (Elliptic Curve Diffie-Hellman) for shared secret derivation +- **AES-GCM** for subsequent message encryption +- **Verification hash** that can be displayed as emojis for visual confirmation + +## Emoji Verification + +The verification hash can be converted to emojis for users to confirm they're talking to the right wallet: + +```typescript +// Convert hash to emoji sequence +function hashToEmojis(hash: string): string { + const emojis = ['🔐', '🎮', '🚀', '⭐', '🎯', '💎', '🔥', '🌟']; + return hash + .slice(0, 8) + .split('') + .map((c) => emojis[parseInt(c, 16) % emojis.length]) + .join(''); +} +``` + +Both the dApp and wallet should display the same emoji sequence, confirming the connection is secure. + +## Secure Messaging + +Once key exchange completes, all messages are encrypted: + +```typescript +// In BackgroundConnectionHandler +async handleEncryptedMessage(sessionId, encrypted) { + const session = this.activeSessions.get(sessionId); + if (!session) return; + + // Decrypt using shared key + const message = await decrypt(session.sharedKey, encrypted); + + // Call our handler + this.callbacks.onWalletMessage?.(session, message); +} + +async sendResponse(sessionId, response) { + const session = this.activeSessions.get(sessionId); + if (!session) return; + + // Encrypt response + const encrypted = await encrypt(session.sharedKey, JSON.stringify(response)); + + // Send to content script + this.transport.sendToTab(session.tabId, { + type: 'SECURE_RESPONSE', + sessionId, + content: encrypted, + }); +} +``` + +## Message Routing + +The extension checks whether a wallet method needs user approval before forwarding: + +```typescript +// sendTx always requires approval — it's a state-changing operation. +// batch requires approval only if it contains a sendTx. +// Read-only calls (getAccounts, simulateTx, executeUtility, etc.) auto-execute. +const needsApproval = + message.type === 'sendTx' || + (message.type === 'batch' && + Array.isArray(message.args?.[0]) && + message.args[0].some((m: any) => m.name === 'sendTx')); +``` + +`requestCapabilities` has its own approval flow — the wallet stores it as pending and shows a capability grant prompt. + +For methods that don't need approval, the extension forwards directly to offscreen via a persistent port. The port uses `messageId`-based request/response correlation with a 5-minute timeout and automatic retry if the offscreen document is torn down by Chrome: + +#include_code send-to-offscreen docs/examples/webapp-tutorial/test-extension/src/background.ts typescript + +For methods that need approval, the extension stores them as pending and waits for user action: + +```typescript +// Store pending transaction +const pending = { + sessionId: session.sessionId, + messageId: message.messageId, + method: message.type, + args: message.args, + from: message.args?.options?.from, + origin: session.origin, + timestamp: Date.now(), +}; +pendingTransactions.push(pending); +updateBadge(); +``` + +## Transport Implementation + +The transport bridges Chrome's messaging APIs: + +#include_code transport docs/examples/webapp-tutorial/test-extension/src/background.ts typescript + +Key points: +- `sendToTab` uses `chrome.tabs.sendMessage` to reach content scripts +- `addContentListener` uses `chrome.runtime.onMessage` to receive content script messages +- The listener skips messages with a `target` field (those are popup → background) and storage proxy messages (those are offscreen → background via broadcast) + +## Session Lifecycle + +Sessions are cleaned up automatically when: +- **Page refresh** — `onPendingDiscovery` terminates stale sessions from the same tab before processing the new discovery +- **Tab closed** — `chrome.tabs.onRemoved` calls `handler.terminateForTab()` to remove all sessions and discoveries for the tab +- **User disconnects** — The popup sends `DISCONNECT_SESSION`, which also removes the origin from trusted origins + +## Next Steps + +With the protocol in place, let's set up [PXE Integration](./03-pxe-integration.md) - running a full Private eXecution Environment inside the extension. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/03-pxe-integration.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/03-pxe-integration.md new file mode 100644 index 00000000000..5a53dfd734b --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/03-pxe-integration.md @@ -0,0 +1,162 @@ +--- +title: "3. PXE Integration" +description: Running the Private eXecution Environment in a browser extension and extending BaseWallet +sidebar_position: 3 +references: ["docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts"] +--- + +# PXE Integration + +The Private eXecution Environment (PXE) is the core of any Aztec wallet. It handles private state, note management, and proof generation. This section shows how to run a PXE in the extension's offscreen document. + +## Why PXE in Extensions? + +Every Aztec wallet needs a PXE to: + +- **Sync private state** - Download and decrypt notes belonging to the user +- **Generate proofs** - Create zero-knowledge proofs for private functions +- **Manage contracts** - Register and track contract artifacts +- **Compute witnesses** - Provide private inputs for transaction execution + +The PXE is stateful and long-running, which is why the extension uses an offscreen document instead of the service worker. + +## Initializing PXE + +The offscreen document initializes PXE lazily on first use with deduplication to prevent multiple initializations: + +#include_code pxe-instance docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript + +Key configuration: +- `rollupAddress` - The L1 rollup contract address, read from the node, that identifies the rollup instance the PXE tracks +- `proverEnabled` - Enables client-side proof generation + +SponsoredFPC is registered lazily when the wallet's `completeFeeOptions()` is first called, rather than at PXE initialization time. + +## The Wallet Implementation + +The extension has two wallet-related classes with distinct responsibilities: + +1. **`ExtensionWalletManager`** (in `wallet-impl.ts`) - A static utility class that handles secret generation, address computation, and encrypted storage. It does **not** extend `BaseWallet`. + +2. **`OffscreenWallet`** (in `offscreen.ts`) - A `BaseWallet` subclass defined inline inside the `getWallet()` function that handles actual wallet operations. + +The `OffscreenWallet` extends `BaseWallet` from the wallet SDK: + +#include_code wallet-instance docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript + +By extending `BaseWallet`, you inherit: +- `sendTx()` - Transaction submission with proof generation +- `simulateTx()` - Transaction simulation +- `createAuthWit()` - Authorization witness creation +- `registerContract()` - Contract registration +- `getChainInfo()` - Network information +- And more... + +You implement: +- `getAccountFromAddress()` - Return the Account object for signing +- `getAccounts()` - List available accounts +- `completeFeeOptions()` - Configure fee payment (the SponsoredFPC override) +- `sendTx()` - Override with automatic auth witness extraction + +## SponsoredFPC Fee Payment + +The key override is `completeFeeOptions()`, which lazily registers the SponsoredFPC contract on first use: + +#include_code complete-fee-options docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript + +This ensures that by default: +1. If the payload already has a `feePayer` (e.g., during account deployment), the wallet respects it +2. Otherwise, the wallet injects `SponsoredFeePaymentMethod` so users don't need fee tokens to transact + +The `SponsoredFeePaymentMethod` creates an execution payload that: +- Calls the SponsoredFPC contract's fee payment function +- Gets merged with the user's transaction payload +- Results in SponsoredFPC paying the fee + +## Message Handling + +The offscreen document receives messages from the background via a persistent port. When the background connects with `chrome.runtime.connect({ name: 'offscreen' })`, the offscreen stores the port and listens for messages. Each message includes a `messageId` for request/response correlation: + +#include_code message-handler docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript + +Each message type maps to a handler: +- `INIT_PXE` - Ensures PXE is ready +- `GET_ACCOUNTS` - Lists stored accounts +- `CREATE_ACCOUNT` - Creates a new account +- `DEPLOY_ACCOUNT` - Deploys an account contract +- `WALLET_METHOD` - Handles dApp wallet calls +- `SETUP_PASSWORD` - Sets master password on first use +- `UNLOCK_WALLET` - Verifies password and registers accounts +- `EXPORT_WALLET` / `IMPORT_WALLET_ACCOUNTS` - Wallet backup and restore + +## Wallet Method Dispatch + +For wallet methods from dApps, the handler dispatches based on method name. The handler uses `WalletSchema` to parse incoming JSON arguments back into proper Aztec types (`AztecAddress`, `Fr`, `ExecutionPayload`, etc.), and `jsonStringify` to serialize results before returning through Chrome messaging: + +#include_code wallet-method-handler docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript + +This generic dispatch means any method on the `BaseWallet` interface (e.g., `getAccounts`, `sendTx`, `simulateTx`, `createAuthWit`, `getChainInfo`) is automatically available to dApps through the wallet SDK protocol. + +## Error Handling + +The port message handler wraps each operation in try/catch and posts the result back with the same `messageId` for correlation: + +```typescript +port.onMessage.addListener((message) => { + handleMessage(message) + .then((result) => { + port.postMessage({ messageId: message.messageId, success: true, result }); + }) + .catch((error) => { + port.postMessage({ messageId: message.messageId, success: false, error: error.message }); + }); +}); +``` + +Errors are serialized and sent back to the background, which can: +- Show them in the popup +- Return them to the dApp as wallet errors + +## PXE State Persistence + +The PXE uses IndexedDB for persistence (via `@aztec-labs/kv-store`). This means: +- Synced notes persist across extension restarts +- Registered contracts are remembered +- Account registrations survive restarts + +However, the `OffscreenWallet.accounts` Map is in-memory and needs reloading when the user unlocks the wallet. This is handled by `handleUnlockWallet()`, which verifies the password, derives a `CryptoKey`, initializes PXE, and registers all stored accounts. See [Account Management](./04-accounts.md) for details. + +## Integration with BaseWallet + +The `BaseWallet` base class does the heavy lifting for transactions: + +```typescript +// In BaseWallet (inherited) +async sendTx(executionPayload, opts) { + // 1. Complete fee options (our override uses SponsoredFPC) + const feeOptions = await this.completeFeeOptions(...); + + // 2. Create execution request + const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(...); + + // 3. Generate proof (WASM, can take time) + const provenTx = await this.pxe.proveTx(txRequest); + + // 4. Submit to node + const tx = await provenTx.toTx(); + await this.aztecNode.sendTx(tx); + + // 5. Optionally wait for confirmation + if (opts.wait !== NO_WAIT) { + const receipt = await waitForTx(this.aztecNode, txHash, waitOpts); + return { receipt }; + } + return { txHash }; +} +``` + +Our `OffscreenWallet` also overrides `sendTx()` to automatically extract authorization witnesses from offchain effects. This means dApps don't need to explicitly create auth witnesses - the wallet handles it by simulating with a stub account, collecting `CallAuthorizationRequest` objects, and signing them before the real transaction. + +## Next Steps + +Now that PXE is running, let's implement [Account Management](./04-accounts.md) - creating, storing, and managing user accounts with encrypted keys. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md new file mode 100644 index 00000000000..c10b0e4bbf5 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md @@ -0,0 +1,220 @@ +--- +title: "4. Account Management" +description: Key derivation, encrypted storage, and SchnorrAccountContract for Aztec wallet accounts +sidebar_position: 4 +references: ["docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts", "docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts", "docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts"] +--- + +# Account Management + +Aztec accounts are smart contracts with associated keys. This section covers how the wallet creates, stores, and manages accounts securely. + +## Aztec Account Model + +Unlike Ethereum's EOA model, Aztec accounts are: + +1. **Smart contracts** - Each account is a deployed contract +2. **Abstracted** - Custom authentication logic (Schnorr, ECDSA, multisig, etc.) +3. **Key-based** - Derived from a secret key using standardized derivation + +An Aztec account has: +- **Secret key** (`Fr`) - The root secret, never exposed +- **Signing key** (`GrumpkinScalar`) - Derived from secret, used for signatures +- **Public keys** - For viewing, tagging, and nullifying +- **Contract address** - Computed from keys and contract salt + +## Key Derivation + +The wallet generates a random signing key as the account's root and derives the privacy secret from it: + +```typescript +import { GrumpkinScalar } from '@aztec-labs/aztec.js/fields'; +import { deriveSecretKeyFromSigningKey } from '@aztec-labs/accounts/utils'; + +// The signing key is the account's root; the privacy secret is derived from it +const signingKey = GrumpkinScalar.random(); +const secret = await deriveSecretKeyFromSigningKey(signingKey); +``` + +The nullifier, viewing, and tagging keys are then derived from that secret with SHA-512 and domain separators, while the signing key is supplied to the account contract directly: + +```typescript +// From stdlib/src/keys/derivation.ts +export function deriveMasterIncomingViewingSecretKey(secretKey: Fr): GrumpkinScalar { + return sha512ToGrumpkinScalar([secretKey, DomainSeparator.IVSK_M]); +} + +export function deriveMasterNullifierHidingSecretKey(secretKey: Fr): GrumpkinScalar { + return sha512ToGrumpkinScalar([secretKey, DomainSeparator.NHK_M]); +} +``` + +## SchnorrAccountContract + +The wallet uses `SchnorrAccountContract` for authentication: + +```typescript +import { SchnorrAccountContract } from '@aztec-labs/accounts/schnorr/lazy'; + +const accountContract = new SchnorrAccountContract(signingKey); +``` + +This account contract: +- Verifies Schnorr signatures on transactions +- Is battle-tested and widely used +- Supports standard Aztec authorization patterns + +The "lazy" import defers loading the Noir artifact until needed. + +## Creating an Account + +Account creation is handled by the `ExtensionWalletManager` utility class: + +#include_code create-new-account docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts typescript + +Breaking this down: + +1. **Generate secrets** - Random `Fr` for secret and salt via `Fr.random()` +2. **Compute address** - Uses the shared `instantiateAccount()` utility to derive keys and compute the contract address deterministically +3. **Encrypt and store** - Secret is encrypted with the master `CryptoKey` before storage +4. **Auto-activate** - The first account is automatically set as active + +Note that PXE (Private eXecution Environment) registration happens separately - either when the user unlocks the wallet or when they deploy the account. This separation means account creation is fast (no PXE or network interaction required). + +## Encrypted Key Storage + +Keys are encrypted using PBKDF2 + AES-GCM with a non-extractable `CryptoKey`: + +#include_code derive-master-key docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript + +Key derivation parameters: +- **600,000 iterations** - OWASP 2023 recommendation for SHA-256, slows brute-force attacks +- **SHA-256** - Hash function for PBKDF2 +- **AES-256-GCM** - Authenticated encryption +- **Non-extractable** - The derived `CryptoKey` cannot be read from JavaScript, even if an attacker has a reference to the object + +Encryption and decryption use the derived `CryptoKey` directly - the raw password string is never stored or passed around: + +#include_code encrypt-decrypt docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript + +Each account gets a unique random IV for AES-GCM. The stored data includes: +- `encrypted` - The encrypted secret (base64) +- `iv` - AES-GCM IV, unique per account (base64) + +## Storage Operations + +The offscreen document cannot access `chrome.storage` directly, so all storage operations are proxied through the background script via Chrome messaging: + +#include_code account-operations docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript + +The stored account structure: + +```typescript +interface StoredAccount { + address: string; // Aztec address (hex) + encryptedSecret: string; // Encrypted Fr (base64) + iv: string; // AES-GCM IV (base64) + alias: string; // User-friendly name + isDeployed: boolean; // Whether contract is deployed + contractSalt: string; // Contract deployment salt (hex) +} +``` + +## Password Management + +Instead of storing a password hash (vulnerable to rainbow tables), the wallet uses an encrypt-then-verify approach: derive a `CryptoKey` via PBKDF2 with a random salt, encrypt a known plaintext, and verify by attempting to decrypt it: + +#include_code password-management docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript + +This provides: +- `setupPassword()` - Derives a master key, encrypts a known plaintext for future verification, returns the `CryptoKey` +- `verifyAndDeriveMasterKey()` - Re-derives the key, tries to decrypt the known plaintext; returns the `CryptoKey` on success or `null` on failure +- `hasPassword()` - Checks if a master password has been set + +The caller caches the `CryptoKey` in memory and discards the password string immediately. + +## Loading Accounts + +When the user unlocks the wallet, the extension verifies the password, derives the master `CryptoKey`, initializes PXE, and registers all stored accounts: + +#include_code load-accounts docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript + +Each account: +1. Has its secret decrypted using the master `CryptoKey` +2. Gets its keys derived and contract instantiated via `instantiateAccount()` +3. Is registered with PXE (contract instance, artifact, and secret key) +4. Gets an `AccountManager` created and account registered with the `OffscreenWallet` + +## Deploying Accounts + +Accounts must be deployed before they can receive notes or initiate transactions: + +#include_code deploy-account docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript + +The deployment: +1. Gets the cached master `CryptoKey` (wallet must be unlocked) +2. Decrypts the account secret +3. Instantiates the account contract with derived keys +4. Initializes PXE and registers the account +5. Registers SponsoredFPC for fee payment +6. Deploys via `AccountManager.getDeployMethod()` with `SponsoredFeePaymentMethod` +7. Uses a heartbeat interval to keep the service worker alive during the long-running proof generation +8. Marks the account as deployed in storage + +## Account Registration with PXE + +For PXE to track notes for an account, the extension registers the contract instance, artifact, and secret key. This is handled by the shared `registerAccountInWallet()` function: + +```typescript +async function registerAccountInWallet(address, secret, salt) { + const { secretFr, saltFr, accountContract, artifact, instance } = + await instantiateAccount(secret, salt); + + const wallet = await getWallet(); + await wallet.registerContract(instance, artifact, secretFr); + + const accountManager = await AccountManager.create(wallet, secretFr, accountContract, { salt: saltFr }); + const account = await accountManager.getAccount(); + wallet.registerAccount(address, account); +} +``` + +Registration includes: +- **Contract instance** - Address, class, initialization args +- **Artifact** - The Noir contract artifact for simulation +- **Secret key** - For note decryption + +## Account Recovery + +Since the wallet stores: +- Encrypted secret +- Contract salt + +Users can recover accounts by: +1. Entering their password +2. Decrypting the secret +3. Recomputing the account address (deterministic) + +The account address is derived from: +- Public keys (from secret) +- Contract class ID (SchnorrAccountContract) +- Contract salt + +## Security Considerations + +The tutorial wallet implements several security best practices: + +1. **Non-extractable CryptoKey** - The master key cannot be read from JavaScript +2. **600,000 PBKDF2 iterations** - Follows OWASP 2023 recommendations +3. **Per-account random IV** - Each account uses a unique AES-GCM initialization vector +4. **Auto-lock** - The wallet clears the cached key after 15 minutes of inactivity +5. **Secure random** - Uses `crypto.getRandomValues()` and `Fr.random()` (CSPRNG) + +For a production wallet, also consider: +- Hardware security modules or platform keychain APIs +- Seed phrase support for backup and restore +- Memory protection (zeroing secrets after use) + +## Next Steps + +With accounts created and stored, let's handle [Transaction Handling](./05-transactions.md) - the signing and proof generation flow. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/05-transactions.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/05-transactions.md new file mode 100644 index 00000000000..19fbd258790 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/05-transactions.md @@ -0,0 +1,377 @@ +--- +title: "5. Transaction Handling" +description: The sendTx flow, proof generation, and SponsoredFPC fee payment in Aztec wallet extensions +sidebar_position: 5 +references: ["docs/examples/webapp-tutorial/test-extension/src/background.ts", "docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts"] +--- + +# Transaction Handling + +Transaction handling is the core purpose of a wallet. This section explains how transactions flow through the extension, from dApp request to onchain settlement. + +## Transaction Flow Overview + +```text +1. dApp calls wallet.sendTx(payload, options) + ↓ +2. Content script encrypts and forwards to background + ↓ +3. Background stores as pending, shows in popup + ↓ +4. User reviews and clicks "Approve" + ↓ +5. Background forwards to offscreen + ↓ +6. Offscreen: + a. Deserializes ExecutionPayload + b. Gets fee options (SponsoredFPC) + c. Creates TxExecutionRequest + d. Generates proof (WASM, 10-60 seconds) + e. Submits to node + ↓ +7. Response flows back to dApp +``` + +## ExecutionPayload + +The dApp sends an `ExecutionPayload` containing: + +```typescript +interface ExecutionPayload { + calls: FunctionCall[]; // Contract calls to execute + authWitnesses: AuthWitness[]; // Pre-signed authorizations + capsules: any[]; // Encrypted data capsules + feePayer?: AztecAddress; // Optional explicit fee payer +} + +interface FunctionCall { + name: string; // Function name + to: AztecAddress; // Target contract + selector: FunctionSelector; // Function to call + type: FunctionType; // Private, public or utility + hideMsgSender: boolean; // Hide the caller from an enqueued public call? + isStatic: boolean; // View call? + args: Fr[]; // Function arguments +} +``` + +Example from a Pod Racing dApp: + +```typescript +const payload = new ExecutionPayload( + [ + FunctionCall.from({ + name: 'boost', + to: gameContractAddress, + selector: await FunctionSelector.fromSignature('boost()'), + type: FunctionType.PRIVATE, + hideMsgSender: false, + isStatic: false, + args: [], + }), + ], + [], // authWitnesses + [], // capsules +); + +const { receipt } = await wallet.sendTx(payload, { + from: playerAddress, +}); +``` + +## Approval Flow + +When a wallet message arrives, the background checks whether it needs approval: + +```typescript +// sendTx always requires approval — it's a state-changing operation. +// batch requires approval only if it contains a sendTx. +// Read-only calls (simulateTx, getAccounts, etc.) auto-execute. +const needsApproval = + message.type === 'sendTx' || + (message.type === 'batch' && + Array.isArray(message.args?.[0]) && + message.args[0].some((m: any) => m.name === 'sendTx')); + +if (needsApproval) { + const pending = { + sessionId: session.sessionId, + messageId: message.messageId, + method: message.type, + args: message.args, + from, + origin: session.origin, + timestamp: Date.now(), + }; + + pendingTransactions.push(pending); + updateBadge(); + // User must approve in popup +} +``` + +The badge shows the pending count, alerting the user. + +## Approval UI + +The popup displays transaction details: + +```typescript +function TransactionApproval({ transaction, onApprove, onReject }) { + return ( + <div className="approval-card"> + <div className="approval-header"> + <div className="approval-origin">{new URL(transaction.origin).host}</div> + <div className="approval-type">Send Transaction</div> + </div> + + <div className="approval-details"> + <div className="detail-row"> + <span>From</span> + <span>{truncateAddress(transaction.from)}</span> + </div> + + {/* Show function calls */} + {transaction.args?.executionPayload?.calls?.map((call, i) => ( + <div key={i} className="tx-call"> + <div>{call.name || 'Unknown'}</div> + <div>To: {truncateAddress(call.to)}</div> + </div> + ))} + </div> + + <div className="btn-group"> + <button onClick={onReject}>Reject</button> + <button onClick={onApprove}>Approve</button> + </div> + </div> + ); +} +``` + +## Processing Approved Transactions + +When the user approves: + +```typescript +async function handleTransactionApproval(pending) { + // Forward to offscreen for execution + const result = await sendToOffscreen({ + type: MessageTypes.WALLET_METHOD, + method: pending.method, + args: pending.args, + from: pending.from, + }); + + // Send response back to dApp + await handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + result, + walletId: WALLET_CONFIG.walletId, + }); + + return result; +} +``` + +## Offscreen Execution + +The offscreen document handles the actual transaction: + +```typescript +case 'sendTx': { + const { executionPayload, options } = args; + + // 1. Deserialize the payload + const payload = deserializeExecutionPayload(executionPayload); + const fromAddress = AztecAddress.fromStringUnsafe(from || options.from); + + // 2. Call wallet.sendTx (inherited from BaseWallet) + const result = await wallet.sendTx(payload, { + ...options, + from: fromAddress, + }); + + // 3. Serialize the result + if ('receipt' in result) { + return { + txHash: result.receipt.txHash.toString(), + status: result.receipt.status, + blockNumber: result.receipt.blockNumber?.toString(), + }; + } + return { txHash: result.txHash.toString() }; +} +``` + +:::note +The actual implementation uses `WalletSchema` to parse arguments in a type-safe way, as described in [PXE Integration](./03-pxe-integration.md). The above is a simplified view of the logic. +::: + +## BaseWallet.sendTx + +For details on how `BaseWallet.sendTx` works with `completeFeeOptions`, see [PXE Integration](./03-pxe-integration.md#sponsoredfpc-fee-payment). + +The inherited `sendTx` method does the heavy lifting: + +```typescript +// In BaseWallet (inherited by OffscreenWallet) +async sendTx(executionPayload, opts) { + // 1. Get fee options (our override uses SponsoredFPC!) + const feeOptions = await this.completeFeeOptions({ + from: opts.from, + feePayer: executionPayload.feePayer, + gasSettings: opts.fee?.gasSettings, + }); + + // 2. Create execution request + const txRequest = await this.createTxExecutionRequestFromPayloadAndFee( + executionPayload, + opts.from, + feeOptions + ); + + // 3. Generate proof (this is the slow part) + const provenTx = await this.pxe.proveTx(txRequest); + + // 4. Convert to onchain transaction + const tx = await provenTx.toTx(); + const txHash = tx.getTxHash(); + + // 5. Submit to node + await this.aztecNode.sendTx(tx); + + // 6. Optionally wait for confirmation + if (opts.wait !== NO_WAIT) { + const receipt = await waitForTx(this.aztecNode, txHash, opts.wait); + return { receipt }; + } + return { txHash }; +} +``` + +## SponsoredFPC Integration + +Our `completeFeeOptions` override in `OffscreenWallet` ensures SponsoredFPC is used, as shown in [PXE Integration](./03-pxe-integration.md#sponsoredfpc-fee-payment). + +The `SponsoredFeePaymentMethod`: +1. Creates a fee payment execution payload +2. Gets merged with the user's transaction +3. Calls the SponsoredFPC contract to pay fees +4. User transaction executes with paid fees + +## Proof Generation + +Proof generation is the slowest part (10-60 seconds): + +```typescript +const provenTx = await this.pxe.proveTx(txRequest); +``` + +This: +1. Executes private functions locally +2. Generates WASM-based zero-knowledge proofs +3. Creates the kernel proofs +4. Packages everything for submission + +The offscreen document handles this well because it: +- Doesn't have service worker timeouts +- Supports WASM +- Can use IndexedDB for intermediate state + +## Simulation + +For gas estimation or validation, dApps use `simulateTx`: + +```typescript +case 'simulateTx': { + const { executionPayload, options } = args; + const payload = deserializeExecutionPayload(executionPayload); + const fromAddress = AztecAddress.fromStringUnsafe(from || options.from); + + const result = await wallet.simulateTx(payload, { + ...options, + from: fromAddress, + }); + + return serializeTxSimulationResult(result); +} +``` + +Simulation: +- Executes the transaction locally +- Estimates gas usage +- Detects revert conditions +- Doesn't generate full proofs +- Much faster than `sendTx` + +## Error Handling + +Transactions can fail at several points: + +```typescript +try { + const result = await wallet.sendTx(payload, options); + return { success: true, result }; +} catch (error) { + // Could be: + // - Simulation failure (contract revert) + // - Proof generation failure + // - Node rejection (already settled, insufficient gas) + // - Network error + return { success: false, error: error.message }; +} +``` + +Errors are serialized and returned to the dApp, which should handle them gracefully. + +## Authorization Witnesses + +For delegated actions (like approving token spending), the wallet creates auth witnesses: + +```typescript +case 'createAuthWit': { + const { from: authFrom, messageHashOrIntent } = args; + const fromAddress = AztecAddress.fromStringUnsafe(authFrom); + + const authWit = await wallet.createAuthWit(fromAddress, messageHashOrIntent); + + return { + requestHash: authWit.requestHash.toString(), + witness: Array.from(authWit.witness), + }; +} +``` + +The account signs the authorization, which can be used by other contracts to verify permission. + +## Transaction status + +After submission, a transaction's receipt reports one of these statuses: + +- `pending` - Still in the mempool +- `proposed` - Included in a proposed block +- `checkpointed` - The block's checkpoint has been published on L1 +- `proven` - The block's checkpoint has been proven on L1 +- `finalized` - The L1 transaction that proved the checkpoint has reached L1 finality +- `dropped` - Dropped by the node + +See [Query transaction status](../../../aztec-js/how_to_send_transaction.md#query-transaction-status) for the receipt variants and their fields. + +The wallet can wait for a transaction to be included: + +```typescript +const receipt = await waitForTx(this.aztecNode, txHash, { + timeout: 60, // Seconds + interval: 1, // Seconds between receipt polls +}); + +console.log(receipt.status); // 'checkpointed' or later +console.log(receipt.blockNumber); +``` + +By default, `waitForTx` resolves once the transaction is `checkpointed`, and throws if the transaction is dropped or its execution reverts. + +## Next Steps + +With transactions flowing, let's build the [Approval UI](./06-approval-ui.md) - the React popup that makes all this user-friendly. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/06-approval-ui.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/06-approval-ui.md new file mode 100644 index 00000000000..037e10705ee --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/06-approval-ui.md @@ -0,0 +1,241 @@ +--- +title: "6. Approval UI" +description: Building React popups for connection and transaction approval in Aztec wallet extensions +sidebar_position: 6 +references: ["docs/examples/webapp-tutorial/test-extension/src/popup/*"] +--- + +# Approval UI + +The popup is the user-facing part of the wallet. It displays accounts, pending approvals, and handles user interactions. This section covers building the React-based popup. + +## Popup Structure + +The popup is a small React app rendered when clicking the extension icon: + +```text +popup/ +├── popup.html # HTML entry point +├── popup.css # Styles +└── src/popup/ + ├── popup.tsx # Top-level orchestrator (state machine + routing) + ├── helpers.ts # sendToBackground, waitForTask, truncateAddress + ├── types.ts # Shared TypeScript interfaces + ├── Header.tsx # Header + SubHeader components + ├── SetupScreen.tsx # First-time password setup + ├── LockScreen.tsx # Unlock with password + ├── MainScreen.tsx # Active account detail + deploy + ├── AccountSwitcher.tsx # Account list overlay + ├── CreateAccountView.tsx # New account creation + ├── ApprovalView.tsx # Connection + transaction approvals + └── SettingsPage.tsx # Export/import wallet +``` + +The HTML loads the compiled JavaScript: + +```html +<!DOCTYPE html> +<html> +<head> + <link rel="stylesheet" href="popup.css"> +</head> +<body> + <div id="root"></div> + <script type="module" src="../dist/popup.js"></script> +</body> +</html> +``` + +## Main App Component + +The popup is split into focused components. The top-level `popup.tsx` acts as an orchestrator with a state machine that routes between views: setup, lock, main, create-account, approvals, session-verification, and settings. + +#include_code main-app docs/examples/webapp-tutorial/test-extension/src/popup/popup.tsx typescript + +Key features: +- **Persistent port** to background for real-time push updates (no polling) +- Auto-reconnects if the background service worker restarts +- Auto-switches to Approvals if there are pending items +- Loads accounts and pending items on mount + +## Communication with Background + +The popup sends messages to the background script: + +#include_code send-message docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts typescript + +The popup targets the background explicitly with `target: MessageTarget.BACKGROUND` to distinguish from content script messages. + +## Main Page + +The main page shows the active account and provides key actions: + +#include_code main-page docs/examples/webapp-tutorial/test-extension/src/popup/MainScreen.tsx typescript + +Features: +- Active account with alias and truncated address +- Deployment status indicator with deploy button for undeployed accounts +- Account switcher for selecting between multiple accounts +- Navigation to create-account and settings views + +## Connection Approval + +When a dApp requests connection, the wallet shows the approval UI: + +#include_code connection-approval docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx typescript + +The approval shows: +- Origin URL (the dApp's domain) +- App ID if provided +- Connect/Reject buttons + +The wallet also includes emoji verification for secure channel confirmation — see the `session-verification` marker in the popup source. + +## Transaction Approval + +Transaction approvals show more detail: + +#include_code transaction-approval docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx typescript + +The popup displays: +- Origin and method type (sendTx, simulateTx, etc.) +- From address (the signing account) +- Function calls being made (if available) +- Approve/Reject buttons + +## Helper Functions + +Utility for address truncation: + +#include_code helpers docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts typescript + +## Styling + +The CSS provides a dark theme suited for wallet UIs: + +See the full stylesheet at [`popup/popup.css`](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/docs/examples/webapp-tutorial/test-extension/popup/popup.css). + +Key design choices: +- Dark background (`#1a1a2e`) for modern look +- Orange accent color (`#ff6b00`) for Aztec branding +- Compact cards for account and approval display +- Clear visual hierarchy with section titles + +## State Management + +The popup uses React's `useState` for local state: + +```typescript +const [view, setView] = useState<View>('loading'); +const [accounts, setAccounts] = useState<StoredAccount[]>([]); +const [activeAccount, setActiveAccount] = useState<string | null>(null); +const [discoveries, setDiscoveries] = useState<PendingDiscovery[]>([]); +const [transactions, setTransactions] = useState<PendingTransaction[]>([]); +const [connectedSites, setConnectedSites] = useState<ConnectedSite[]>([]); +const [sessionVerifications, setSessionVerifications] = useState<PendingSessionVerification[]>([]); +const [error, setError] = useState<string | null>(null); +``` + +For a production wallet, consider: +- Redux or Zustand for complex state +- React Query for async data fetching +- Local storage for UI preferences + +## Error Handling + +Errors are displayed inline: + +```typescript +{error && <div className="message message-error">{error}</div>} +{success && <div className="message message-success">{success}</div>} +``` + +Common errors: +- Wrong password (decryption fails) +- Network errors (node unreachable) +- Transaction failures (contract reverts) + +## Loading States + +The popup shows spinners during async operations: + +```typescript +{loading ? ( + <div className="loading"> + <div className="spinner" /> + Loading... + </div> +) : ( + // Content +)} +``` + +And disable buttons during processing: + +```typescript +<button disabled={processing}> + {processing ? 'Processing...' : 'Approve'} +</button> +``` + +## Building the Popup + +The popup is built by **Vite** (not esbuild) as part of the main extension build. Vite handles JSX transformation, React support, and bundling: + +```bash +node esbuild.extension.mjs +# Step 1: Vite builds background, offscreen, and popup (with React JSX support) +# Step 2: esbuild builds the content script separately as IIFE +# Step 3-4: Copy static files (offscreen HTML, WASM binaries) +``` + +## Popup Dimensions + +The popup size is controlled by CSS: + +```css +body { + width: 360px; + min-height: 400px; +} +``` + +Chrome allows popups up to 800x600, but 360x500 is typical for wallets. + +## Security Considerations + +For production popups: + +1. **Input validation** - Sanitize all displayed data +2. **Origin verification** - Always show full origin for approvals +3. **Confirmation dialogs** - For destructive actions +4. **Rate limiting** - Prevent rapid-fire approvals +5. **Session timeout** - Auto-lock after inactivity + +## Accessibility + +The current UI is minimal. Production improvements: + +- Keyboard navigation +- ARIA labels +- Screen reader support +- High contrast mode +- Focus management + +## Testing the Popup + +To test popup changes: + +1. Rebuild: `node esbuild.extension.mjs` +2. Go to `chrome://extensions/` +3. Click refresh on the extension +4. Click the extension icon + +DevTools for popup: +1. Right-click the popup +2. Select "Inspect" +3. Use Console and Elements tabs + +## Next Steps + +With the UI complete, let's put it all together in [Testing](./07-testing.md) - loading the extension and testing with the Pod Racing dApp. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/07-testing.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/07-testing.md new file mode 100644 index 00000000000..9f0b790adc6 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/07-testing.md @@ -0,0 +1,207 @@ +--- +title: "7. Testing" +description: Loading the wallet extension in Chrome and testing with the Pod Racing dApp +sidebar_position: 7 +references: ["docs/examples/webapp-tutorial/esbuild.extension.mjs", "docs/examples/webapp-tutorial/test-extension/*"] +--- + +# Testing the Wallet Extension + +This final section covers loading your wallet extension in Chrome and testing it with the Pod Racing dApp from the webapp tutorial. + +## Building the Extension + +From the `webapp-tutorial` directory: + +```bash +# Install dependencies if needed +yarn install + +# Build the extension +node esbuild.extension.mjs +``` + +You should see: + +```text +Extension build complete! +``` + +The build creates: +- `test-extension/dist/background.js` +- `test-extension/dist/content-script.js` +- `test-extension/dist/offscreen.js` +- `test-extension/dist/popup.js` + +## Loading in Chrome + +1. Open Chrome and navigate to `chrome://extensions/` +2. Enable **Developer mode** (toggle in top-right corner) +3. Click **Load unpacked** +4. Select the `test-extension` folder (not `dist/`) +5. The extension "Aztec Tutorial Wallet" should appear + +You should see: +- Extension icon in the toolbar +- "Service Worker" link under "Inspect views" + +## First launch + +Click the extension icon to open the popup: + +1. On the "Welcome to Aztec Wallet" screen, enter a password (at least 8 characters), confirm it, and click "Continue" +2. Under "Create Your First Account", enter an account name and click "Create Account" + +The account appears with the status "Not Deployed". + +## Deploying an account + +1. Click "Deploy Account" +2. Wait for the transaction (it uses SponsoredFPC, so no tokens are needed) +3. The status changes to "Deployed" + +If the deployment fails, the error appears above the "Deploy Account" button. See [Debugging](#debugging) for where to find the logs. + +## Running the dApp + +In another terminal, start the webapp tutorial: + +```bash +cd docs/examples/webapp-tutorial + +# Start the dev server +yarn dev +``` + +Open `http://localhost:5173` in Chrome. + +## Connecting the wallet + +1. In the dApp, set **Network** to "Browser Wallet" +2. The extension receives a **Connection Request** from `localhost:5173` and shows a badge. Open the popup and click "Connect" +3. Back in the dApp, click "Connect to Aztec Tutorial Wallet" +4. The dApp shows a grid of emojis, and the popup shows **Verify Connection** with the same emojis. If they match, click "Emojis Match" +5. The popup then shows a **Capabilities Request** listing the permissions the dApp asks for. Click "Approve" + +The dApp now shows your account address and the game lobby. + +## Creating a game + +In the dApp, click "Deploy Contract & Create Game". The popup shows a **Send Transaction** approval for each transaction: first the contract deployment, then `create_game`. Review the function calls and click "Approve" for each. + +Each transaction takes a little while because the proof is generated in the extension (WASM) and then included in a block. + +## Playing the game + +1. Set the track sliders (the total must be less than 10) and click "Submit Allocation" +2. Approve the `play_round` transaction in the popup +3. The dApp moves on to the next round + +Each round is a private transaction: your allocation is stored as a private note, and only the round counter is public. To finish a game, a second player joins with the contract address and game ID shown in the dApp. + +## Debugging + +### Background Script + +1. Go to `chrome://extensions/` +2. Find "Aztec Tutorial Wallet" +3. Click "Service Worker" under "Inspect views" +4. Check Console for `[background]` logs + +### Offscreen Document + +1. On the extensions page, look for "Offscreen document" +2. Click to open DevTools +3. Check Console for `[offscreen]` logs + +### Content Script + +1. Open DevTools on the dApp page (F12) +2. Check Console for `[content-script]` logs +3. May need to filter by extension + +### Common Issues + +**"Cannot read properties of undefined"** +- PXE (Private eXecution Environment) hasn't initialized yet +- Check offscreen console for initialization errors + +**"Account not found"** +- Account isn't loaded in memory +- Try entering password to unlock + +**"Transaction rejected"** +- Check if the Aztec node is reachable +- Verify SponsoredFPC is registered + +**Popup doesn't show pending items** +- Refresh the popup (close and reopen) +- Check background console for errors + +## Verifying on Explorer + +If your network has a block explorer, you can verify transactions: + +1. Copy your transaction hash from logs +2. Visit the explorer +3. Search for the transaction +4. Verify it's included in a block + +## Reloading After Changes + +When you modify the extension: + +1. Rebuild: `node esbuild.extension.mjs` +2. Go to `chrome://extensions/` +3. Click the refresh icon on the extension +4. Reload any open dApp pages + +## End-to-End Test Flow + +Complete test checklist: + +1. [ ] Build extension +2. [ ] Load in Chrome +3. [ ] Set a password and create an account in the popup +4. [ ] Deploy the account (SponsoredFPC) +5. [ ] Start the dApp +6. [ ] Connect the wallet (approve the connection, emojis and capabilities in the popup) +7. [ ] Verify the account address shows in the dApp +8. [ ] Deploy the Pod Racing contract and create a game (approve in the popup) +9. [ ] Submit a round (approve the `play_round` transaction) +10. [ ] Check transactions in explorer + +## Production Considerations + +Before releasing a wallet extension: + +1. **Security audit** - Professional review of crypto code +2. **Key management** - Consider hardware wallet support +3. **Network switching** - Support testnet, mainnet +4. **Error recovery** - Graceful handling of failures +5. **Backup/restore** - Seed phrase support +6. **Multi-account** - Better account management UI +7. **Transaction history** - Show past transactions +8. **Note management** - Display synced notes + +## Summary + +You've built a functional wallet extension that: + +- Creates and stores encrypted accounts +- Deploys contracts using SponsoredFPC +- Connects to dApps via the wallet SDK protocol +- Approves transactions with a popup UI +- Generates ZK proofs for private transactions + +This is the foundation for a production Aztec wallet. The architecture patterns - offscreen documents, message routing, BaseWallet extension - apply to any browser wallet. + +## What's Next? + +- Add more account types (ECDSA, multisig) +- Implement transaction history +- Add network switching +- Build a note browser +- Support hardware wallets + +Happy building! diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/index.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/index.md new file mode 100644 index 00000000000..f620673f228 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/index.md @@ -0,0 +1,156 @@ +--- +title: Building a Wallet Extension +description: Learn how to build a Chrome extension wallet for Aztec with encrypted key storage, SponsoredFPC fee payment, and transaction approval flows +sidebar_position: 3 +references: ["docs/examples/webapp-tutorial/test-extension/*"] +--- + +# Building a Wallet Extension for Aztec + +In this tutorial, you'll build a fully functional Chrome extension wallet that can: + +- Create and store encrypted accounts +- Deploy account contracts using SponsoredFPC (no fee tokens needed) +- Connect to dApps using the Aztec wallet SDK protocol +- Approve and sign transactions with a popup UI + +This is a **standalone tutorial** that complements the [Webapp Tutorial](../webapp/index.md). While the webapp tutorial uses an embedded wallet for simplicity, this tutorial shows how to build a real browser extension wallet like MetaMask or Rabby. + +## What You'll Learn + +1. **Extension Architecture** - Service workers, offscreen documents, and message passing +2. **Wallet SDK Protocol** - Discovery, ECDH key exchange, and secure messaging +3. **PXE Integration** - Running a Private eXecution Environment in a browser extension +4. **Account Management** - Key derivation, encrypted storage, and Schnorr signatures +5. **Transaction Handling** - Signing, proofs, and SponsoredFPC fee payment +6. **Approval UIs** - React popups for connection and transaction approval + +## Architecture Overview + +```text +┌─────────────────────────────────────────────────────────────┐ +│ Content Script │ +│ - Injected into every page │ +│ - Relays messages between page and background │ +└──────────────────────┬──────────────────────────────────────┘ + │ chrome.runtime messages +┌──────────────────────▼──────────────────────────────────────┐ +│ Service Worker (Background) │ +│ - Handles wallet SDK protocol │ +│ - Routes wallet method calls to offscreen document │ +│ - Manages popup for user approvals │ +└──────────────────────┬──────────────────────────────────────┘ + │ chrome.runtime messages +┌──────────────────────▼──────────────────────────────────────┐ +│ Offscreen Document │ +│ - Runs PXE instance (long-lived, supports WASM) │ +│ - Implements wallet methods with SponsoredFPC │ +│ - Manages account creation and signing │ +└──────────────────────────────────────────────────────────────┘ +``` + +### Why This Architecture? + +**Service workers** in Manifest V3 have a 5-minute inactivity timeout and limited WASM support. Since the PXE needs persistent state and long-running proof generation, the extension uses an **offscreen document** that: + +- Runs longer than service workers +- Supports IndexedDB for PXE storage +- Handles WASM-based proof generation +- Maintains state across requests + +The **service worker** handles the lightweight protocol layer (discovery, key exchange) and routes heavier operations to the offscreen document. + +## Prerequisites + +Before starting, you should be familiar with: + +- TypeScript and React basics +- Chrome extension development (Manifest V3) +- The Aztec concepts (accounts, transactions, PXE) + +You'll also need: + +- Node.js 22+ +- Chrome browser +- A local Aztec network running (`aztec start --local-network`) +- The [webapp-tutorial project set up](../webapp/index.md#clone-the-example) (for testing) + +## Project Structure + +We'll build on the existing `test-extension/` in the webapp tutorial: + +```text +test-extension/ +├── manifest.json # Chrome extension manifest +├── popup/ +│ ├── popup.html # Popup UI HTML +│ └── popup.css # Popup styles +├── src/ +│ ├── background.ts # Service worker - protocol + routing +│ ├── content-script.ts # Page <-> background relay +│ ├── config.ts # Constants and configuration +│ ├── account-utils.ts # Shared account instantiation logic +│ ├── aztec-imports.ts # Lazy import caching for Aztec modules +│ ├── utils.ts # Chrome runtime helpers and utilities +│ ├── offscreen/ +│ │ ├── offscreen.html # Offscreen document HTML +│ │ └── offscreen.ts # PXE host + OffscreenWallet (BaseWallet subclass) +│ ├── popup/ +│ │ └── popup.tsx # React popup component +│ └── wallet/ +│ ├── wallet-impl.ts # ExtensionWalletManager - secret generation and encrypted storage +│ └── storage.ts # Encrypted key storage with CryptoKey pattern +└── dist/ # Compiled output +``` + +## Tutorial Sections + +1. [**Architecture**](./01-architecture.md) - Understanding service worker limitations and offscreen documents +2. [**Wallet Protocol**](./02-wallet-protocol.md) - Implementing discovery, key exchange, and secure messaging +3. [**PXE Integration**](./03-pxe-integration.md) - Running PXE in an extension and extending BaseWallet +4. [**Account Management**](./04-accounts.md) - Key derivation, encrypted storage, and SchnorrAccountContract +5. [**Transaction Handling**](./05-transactions.md) - The sendTx flow, proofs, and SponsoredFPC +6. [**Approval UI**](./06-approval-ui.md) - Building React popups for user confirmations +7. [**Testing**](./07-testing.md) - Loading the extension and testing with the Pod Racing dApp + +## Quick Start + +If you want to try the completed wallet before reading the tutorial: + +```bash +git clone https://github.com/aztec-labs-eng/aztec-node.git +cd aztec-node +git checkout #include_aztec_version +cd docs/examples/webapp-tutorial +./setup.sh +node esbuild.extension.mjs +``` + +Then load it in Chrome: + +1. Make sure your local Aztec network is running (`aztec start --local-network`) +2. Open `chrome://extensions/` +3. Enable **Developer mode** (toggle in top-right corner) +4. Click **Load unpacked** and select the `test-extension` folder +5. Start the dApp with `yarn dev` and open `http://localhost:5173` +6. Select "Browser Wallet" to connect + +## Key Differences from Embedded Wallet + +| Feature | Embedded Wallet | Extension Wallet | +|---------|----------------|------------------| +| **Location** | Runs in dApp's page | Runs in extension context | +| **Storage** | localStorage (accessible to dApp) | chrome.storage (isolated) | +| **Security** | Keys visible to dApp | Keys encrypted, never exposed | +| **UX** | No approval needed | Popup for approvals | +| **Setup** | Just import library | User installs extension | +| **Network** | Local development | Local network (or any network) | + +## Next Steps + +Start with [Architecture](./01-architecture.md) to understand why this multi-component design is needed, then work through each section to build your wallet. + +:::tip Related Resources +- [Webapp Tutorial](../webapp/index.md) - Build a dApp that connects to this wallet +- [BaseWallet Source](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts) - The class you extend +::: diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/01-the-contract.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/01-the-contract.md new file mode 100644 index 00000000000..b65daa0c058 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/01-the-contract.md @@ -0,0 +1,192 @@ +--- +title: "1. The Contract" +sidebar_position: 1 +description: "Walk through the Pod Racing smart contract, compile it, and deploy and interact with it via a TypeScript script" +references: ["docs/examples/webapp-tutorial/contracts/src/*", "docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts"] +--- + +# The Contract + +:::note Prerequisites +Make sure you've completed the [setup steps](./index.md#clone-the-example) before continuing. +::: + +In this section, you walk through the Pod Racing smart contract, compile it, and run a standalone script that deploys and plays a full game against a local network. + +## Overview + +Before looking at code, here is how the contract is structured. + +**Storage:** +- `admin` — the address that deployed the contract +- `races` — a public map from game ID to `Race` struct (the shared game state) +- `progress` — a private map storing each player's per-round point allocations as encrypted notes +- `win_history` — a public map tracking each player's lifetime win count + +**Game lifecycle:** +1. `create_game` — player 1 calls this to create a new `Race` in public storage, setting a block deadline +2. `join_game` — player 2 joins, filling the second slot in the `Race` +3. `play_round` (private) — each player submits a `GameRoundNote` containing their point allocation for one round; a public follow-up increments the round counter without revealing points +4. `finish_game` (private) — reads all of your `GameRoundNote`s, sums totals per track, and publishes the aggregated scores publicly +5. `finalize_game` (public) — compares both players' track totals, declares the winner (best of 5), and updates win history + +**Key types:** +- `GameRoundNote` — a private note storing one round's point allocation (5 track values), the round number, and the owner +- `Race` — the public game state: both player addresses, round counters, final per-track scores, block deadline, and winner + +The core design principle is that **private functions** (`play_round`, `finish_game`) hide your strategy, while **public functions** (`create_game`, `join_game`, `finalize_game`) coordinate shared state that both players can see. + +## Nargo.toml + +Open `contracts/Nargo.toml`. This configures the Noir compiler for the contract: + +```toml +[package] +name = "pod_racing_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../noir-projects/aztec-nr/aztec" } +``` + +:::note +The `aztec` dependency path assumes you're working within the `aztec-node` repository. If you're working outside it, use the git dependency instead: +```toml +aztec = { git = "https://github.com/aztec-labs-eng/aztec-nr/", tag = "#include_aztec_version", directory = "aztec" } +``` +Replace the tag with your Aztec version. +::: + +## GameRoundNote + +Open `contracts/src/game_round_note.nr`. This is a private note that stores a player's point allocation for one round: + +#include_code game-round-note /docs/examples/webapp-tutorial/contracts/src/game_round_note.nr rust + +The fields `track1` through `track5` store the points allocated to each track for that round. `round` identifies which round (1, 2, or 3) the note belongs to. `owner` is the player's address — only the owner's PXE (Private eXecution Environment) can decrypt and read this note. + +### How private notes work + +In Aztec, private state is stored as **notes** — encrypted data objects that live in your PXE local database. When a note is created, it is encrypted with the owner's public key so that only the owner's PXE can decrypt and read it. The note is also committed to the network as a hash (called a commitment), which allows for proving the note exists without revealing its contents. + +When you call `play_round`, the contract creates a `GameRoundNote` with your allocation. The flow: +1. You call `play_round(gameId, round, 3, 2, 1, 2, 1)` — a **private function** +2. The contract creates a `GameRoundNote` with your allocation, stored privately +3. It then enqueues a **public** call to `validate_and_play_round` which increments your round counter (visible) without revealing your points (hidden). "Enqueues" means the private function schedules a public function to run after the private execution completes. Private functions cannot modify public state directly, so they use this mechanism to trigger public side effects (e.g., state changes or emitting logs). +4. Your opponent's PXE cannot decrypt your notes — they only see that you completed a round + +This means: +- **You** know your own allocations +- **Your opponent** only sees that you've played a round (public round counter) +- **The network** only sees encrypted data + +## Race struct + +Open `contracts/src/race.nr`. This is the public game state: + +#include_code race /docs/examples/webapp-tutorial/contracts/src/race.nr rust + +The `Race` struct holds both player addresses, per-player round counters, the final aggregated scores for each track (filled in when a player calls `finish_game`), and a block deadline. The winner calculation compares track-by-track totals: whoever wins more of the 5 tracks wins the game. + +## Main contract + +Open `contracts/src/main.nr`. This is the main contract file that defines the game flow. + +### Storage + +#include_code storage /docs/examples/webapp-tutorial/contracts/src/main.nr rust + +The storage maps directly to the overview above: `admin` for the deployer, `races` for game state, `progress` for private round notes, and `win_history` for tracking wins. + +### Creating and joining a game + +#include_code create-game /docs/examples/webapp-tutorial/contracts/src/main.nr rust + +#include_code join-game /docs/examples/webapp-tutorial/contracts/src/main.nr rust + +Both are public functions. `create_game` initializes a new `Race` with the caller as player 1 and sets a block deadline. `join_game` fills in player 2. + +### Playing a round (private) + +#include_code play-round /docs/examples/webapp-tutorial/contracts/src/main.nr rust + +This is a **private function** — the point allocation remains hidden from the opponent. It creates a `GameRoundNote`, then enqueues a public call to `validate_and_play_round` to increment the round counter without revealing points. + +### Finishing the game (reveal) + +#include_code finish-game /docs/examples/webapp-tutorial/contracts/src/main.nr rust + +Another private function. It reads all your `GameRoundNote`s, sums up totals per track, and publishes the aggregated scores to public state. Your per-round choices stay hidden, but your final totals become visible. + +### Finalizing the game + +#include_code finalize-game /docs/examples/webapp-tutorial/contracts/src/main.nr rust + +A public function that compares both players' track totals, declares the winner (best of 5 tracks), and updates the win history. Can only be called after the game's block deadline has passed. + +## Compile the contract + +With the Aztec CLI installed and the contract source in place: + +```bash +# Compile the Noir contract and generate TypeScript bindings +yarn prep +``` + +This runs `yarn compile && yarn codegen`, producing `src/artifacts/PodRacing.ts` (the typed contract class) and `src/artifacts/pod_racing_contract-PodRacing.json` (the compiled artifact). You'll import from `PodRacing.ts` throughout the app. + +## Deploy and interact via script + +To verify everything works, run a standalone TypeScript script that deploys the contract and plays a full game against a local network. + +Open `scripts/deploy-and-interact.ts`. It follows the same pattern as the [aztec.js Getting Started](../aztecjs-getting-started.md) guide. + +### Setup + +#include_code script-setup /docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript + +This uses `EmbeddedWallet` from `@aztec-labs/wallets/embedded` — a ready-made embedded wallet that handles PXE creation and account management. `getInitialTestAccountsData` provides the local network's genesis-funded test accounts, created here as initializerless Schnorr accounts. The `{ ephemeral: true }` option means PXE state is not persisted between runs. + +### Deploy the contract + +#include_code script-deploy /docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript + +### Create and join a game + +#include_code script-create-join /docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript + +### Play rounds + +#include_code script-play-rounds /docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript + +Each `play_round` call is a private transaction — the point allocations are encrypted as notes. Alice and Bob each play 3 rounds. + +### Finish and finalize + +#include_code script-finish-finalize /docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript + +Both players call `finish_game` to reveal their aggregated scores, then either player calls `finalize_game` to determine the winner. + +### Run it + +Make sure you have a local network running first: + +```bash +# Terminal 1: Start the local network +aztec start --local-network +``` + +Once the network is ready (you can check with `curl http://localhost:8080/status`), run the script in a separate terminal: + +```bash +# Terminal 2: Run the script +yarn interact +``` + +You should see output showing the contract deployment, game creation, rounds being played, and the winner being determined. The script takes a few minutes to complete as each transaction requires proof generation. + +## Next steps + +With the contract understood and verified, continue to [project setup](./02-project-setup.md) to see how the webapp is structured. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/02-project-setup.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/02-project-setup.md new file mode 100644 index 00000000000..ba7b75c3969 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/02-project-setup.md @@ -0,0 +1,87 @@ +--- +title: "2. Project Setup" +sidebar_position: 2 +description: "Understand the webapp project structure, Vite configuration, and environment setup" +references: ["docs/examples/webapp-tutorial/vite.config.ts", "docs/examples/webapp-tutorial/package.json"] +--- + +# Project Setup + +## Overview + +This section walks through the project structure and key configuration files. Since you [cloned the example](./index.md#clone-the-example) in the introduction, everything is already in place — this page explains what each piece does. + +## Project structure + +```text +webapp-tutorial/ +├── contracts/ # Noir smart contract source +│ ├── Nargo.toml +│ └── src/ +│ ├── main.nr +│ ├── game_round_note.nr +│ └── race.nr +├── scripts/ # Standalone scripts (deploy-and-interact) +├── src/ +│ ├── artifacts/ # Generated contract bindings (from yarn prep) +│ ├── components/ # React components (GameBoard, GameLobby, etc.) +│ ├── App.tsx # Main app component +│ ├── config.ts # Network configuration +│ ├── contract.ts # Contract deploy/call helpers +│ ├── embedded-wallet.ts # Embedded wallet for local dev +│ ├── fees.ts # SponsoredFPC fee payment utilities +│ ├── game-constants.ts # Shared game constants +│ ├── main.tsx # React entry point +│ └── wallet-connection.ts # Wallet SDK connection logic +├── test-extension/ # Tutorial wallet extension (browser wallet) +├── index.html # HTML entry point +├── vite.config.ts # Vite configuration +├── tsconfig.json # TypeScript configuration +├── .env.example # Environment variables to override +└── package.json +``` + +## Vite configuration + +Aztec uses WASM modules that require `SharedArrayBuffer`, which needs specific HTTP headers. Open `vite.config.ts`: + +#include_code vite-config /docs/examples/webapp-tutorial/vite.config.ts typescript + +Why each piece is needed: +- **COOP/COEP headers**: Enable `SharedArrayBuffer` for multithreaded WASM (barretenberg proving) +- **Node polyfills**: Aztec libraries use Node.js APIs that need polyfilling in the browser +- **optimizeDeps.exclude**: Prevents Vite from pre-bundling WASM-containing packages (`@aztec-foundation/bb.js`, `@aztec-foundation/noir-noirc_abi`, etc.) which would corrupt the WASM modules + +## TypeScript configuration + +The project uses a standard Vite + React TypeScript setup. `tsconfig.json` references `tsconfig.app.json`, which targets `ES2020` with `"jsx": "react-jsx"` and `"moduleResolution": "Bundler"`. No special configuration is needed for Aztec. + +## Environment variables + +Copy `.env.example` to `.env`: + +```bash +cp .env.example .env +``` + +and paste the below into the file. + +```text +AZTEC_NODE_URL=http://localhost:8080 +``` + +This tells the app where to find the Aztec node. For local development, this points to the default local network port. When using the wallet extension, it manages the node connection. + +## Compiling the contract + +If you haven't already compiled the contract from the [previous section](./01-the-contract.md#compile-the-contract): + +```bash +yarn prep +``` + +This produces `src/artifacts/PodRacing.ts` and `src/artifacts/pod_racing_contract-PodRacing.json`. The webapp imports the typed contract class from `PodRacing.ts`. + +## Next steps + +With the project structure understood, continue to [network and wallet setup](./03-network-and-wallet.md). diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/03-network-and-wallet.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/03-network-and-wallet.md new file mode 100644 index 00000000000..13f0852fad6 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/03-network-and-wallet.md @@ -0,0 +1,183 @@ +--- +title: "3. Network & Wallet" +sidebar_position: 3 +description: "Connect to Aztec using an embedded wallet for local dev or the wallet SDK for browser extensions" +references: ["docs/examples/webapp-tutorial/src/config.ts", "docs/examples/webapp-tutorial/src/embedded-wallet.ts", "docs/examples/webapp-tutorial/src/wallet-connection.ts", "docs/examples/webapp-tutorial/src/fees.ts", "docs/examples/webapp-tutorial/src/components/*"] +--- + +# Network & Wallet + +This section covers connecting your webapp to the Aztec network and setting up a wallet. You'll walk through three source files (`config.ts`, `embedded-wallet.ts`, `wallet-connection.ts`) plus shared fee utilities and UI components. + +## Key concepts + +Before looking at code, it helps to understand two pieces of infrastructure that every Aztec app relies on: + +**[PXE (Private eXecution Environment)](../../../foundational-topics/pxe/index.md)** is a client-side runtime that runs in the browser as WASM. It stores your private notes, manages your encryption keys, executes private functions, and generates zero-knowledge proofs. Because PXE runs locally, your private data never leaves the browser. + +Every Aztec app needs a PXE — either one it creates itself or one provided by a wallet extension. + +**Aztec node** is the server-side component that maintains the network's public state and sequences transactions into blocks. Your PXE connects to a node (a local network during development, or a remote node in production) to sync state and submit transactions. The node never sees your private data — it only receives proofs and encrypted outputs. + +The relationship is straightforward: PXE handles everything private (notes, keys, proofs), the node handles everything public (state, blocks, sequencing), and they communicate over a standard RPC interface. + +## Network configuration + +Open `src/config.ts`. This determines which Aztec node to connect to and provides a helper for creating an in-browser PXE: + +#include_code config /docs/examples/webapp-tutorial/src/config.ts typescript + +`createLocalPXE` sets up PXE in three steps: + +1. `createAztecNodeClient` — opens an RPC connection to the Aztec node so PXE can sync public state and submit transactions. +2. `getPXEConfig` + `getL1ContractAddresses` — fetches protocol configuration from the node, including L1 contract addresses and network parameters that PXE needs to construct valid proofs. +3. `createPXE` — starts a full PXE instance in the browser. From this point on, all private execution happens locally. + +## Wallet modes + +Aztec supports two wallet modes, and the app implements both: + +- **Embedded wallet** — your app creates PXE and manages accounts directly. Best for local development with the local network. +- **Wallet SDK** — your app connects to an external browser extension that owns PXE and accounts. Required for production. + +Both modes produce the same `Wallet` interface, so the rest of your app doesn't need to know which one is in use. + +## Embedded wallet (local development) + +For local development, the app uses a custom `EmbeddedWallet` class that extends the official `EmbeddedWallet` from `@aztec-labs/wallets/embedded`. The official wallet already provides account creation and persistence, transaction sending with gas estimation, automatic authwitness generation, and stub-account simulation. The tutorial subclass adds one thing: **SponsoredFPC fee payment** so users don't need to hold fee tokens. + +Open `src/embedded-wallet.ts`: + +### Imports + +#include_code embedded-wallet-imports /docs/examples/webapp-tutorial/src/embedded-wallet.ts typescript + +### Initialization + +#include_code initialize /docs/examples/webapp-tutorial/src/embedded-wallet.ts typescript + +The `initialize` factory calls the inherited `create()` method, which sets up an in-browser PXE and account storage. It then registers the SponsoredFPC contract with PXE so that fee payment works out of the box. + +### Connecting a test account + +The local network ships with test accounts that are funded at genesis. They are initializerless Schnorr accounts, so you can use them immediately without deploying an account contract. The inherited `createSchnorrInitializerlessAccount` registers the account with PXE and persists it in the wallet database. You select one by index (0, 1, 2, etc.). + +#include_code connect-test-account /docs/examples/webapp-tutorial/src/embedded-wallet.ts typescript + +### Fee payment + +Every Aztec transaction must pay a fee (similar to gas on Ethereum). Rather than requiring users to hold fee tokens during development, the embedded wallet overrides `completeFeeOptions` to inject SponsoredFPC as the default fee payer for every transaction. Callers never need to pass fee options manually. + +:::note +SponsoredFPC only works on the local network. Production Aztec networks deployed to Ethereum mainnet require an alternative fee payment strategy. +::: + +#include_code fee-options /docs/examples/webapp-tutorial/src/embedded-wallet.ts typescript + +### Full class + +The complete `EmbeddedWallet` — most of the heavy lifting (simulation, proving, gas estimation) is inherited from the official wallet: + +#include_code embedded-wallet-class /docs/examples/webapp-tutorial/src/embedded-wallet.ts typescript + +## Wallet SDK (browser extension) + +Users may connect via a browser extension wallet (like MetaMask on Ethereum). The wallet extension owns the PXE, manages keys, and signs transactions. Your app communicates with it through the **wallet SDK**, which handles discovery, secure channel setup, and verification. + +### Testing with the tutorial wallet extension + +The tutorial includes a fully functional wallet extension in `test-extension/`. Unlike a mock extension, this wallet can: + +- Create and store encrypted accounts +- Deploy account contracts using SponsoredFPC (no fee tokens needed) +- Sign and submit real transactions +- Show approval popups for connections and transactions + +To use it: + +1. Build the extension (from the `webapp-tutorial` directory): + ```bash + node esbuild.extension.mjs + ``` + +2. Load it in Chrome: + - Navigate to `chrome://extensions/` + - Enable "Developer mode" (toggle in top-right) + - Click "Load unpacked" + - Select the `test-extension/` folder + +3. Set up the wallet: + - Click the extension icon + - Create a master password on the setup screen + - Create your first account (enter an alias) + - Click "Deploy" to deploy the account contract + +4. After making changes to the extension source, rebuild and click the refresh icon in `chrome://extensions/` + +:::tip Learn How It Works +Want to build your own wallet extension? The [Wallet Extension Tutorial](../wallet-extension/index.md) explains the architecture and implementation in detail, covering service workers, offscreen documents, encrypted storage, and approval flows. +::: + +Open `src/wallet-connection.ts`: + +### Step 1: Discover available wallets + +Your app needs to find which wallet extensions the user has installed. The SDK does this through a `window.postMessage`-based discovery protocol: your app broadcasts a discovery request, and any installed wallet extension responds with its provider info (name, icon, supported chain). + +The `discoverWallets` function starts this process and calls your `onUpdate` callback each time a new wallet extension responds. You use the resulting list to show users a "pick your wallet" UI. + +#include_code discover-wallets /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript + +### Step 2: Connect and verify + +Once the user picks a wallet, you need to establish a secure communication channel. This is important because `window.postMessage` is visible to every script on the page — without encryption, a malicious script could intercept private data flowing between your app and the wallet. + +The connection uses an **ECDH key exchange**: your app and the wallet extension each generate an ephemeral key pair and derive a shared secret. All subsequent messages are encrypted with this shared secret. + +To guard against man-in-the-middle attacks (where a malicious script intercepts the key exchange and substitutes its own keys), the SDK produces a verification hash that gets converted to a short **emoji string**. Your app displays these emojis, and the user checks that their wallet extension shows the same emojis. If they match, the channel is secure. If they don't, the connection should be rejected. + +After the user confirms the emojis match, calling `confirm()` completes the handshake and returns a `Wallet` instance connected to the extension. + +#include_code connect-wallet /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript + +## Fee payment helpers + +Both wallet modes need access to the SponsoredFPC contract. **SponsoredFPC** (Fee Payment Contract) is a special contract deployed at a well-known deterministic address that agrees to pay transaction fees on behalf of any caller. It's available on the local network, making it useful for onboarding users who don't yet have fee tokens. + +Open `src/fees.ts`: + +#include_code get-sponsored-fpc /docs/examples/webapp-tutorial/src/fees.ts typescript + +PXE needs the SponsoredFPC artifact registered so it can include fee payment logic when constructing transaction proofs. Without this registration, PXE wouldn't know how to interact with the fee contract: + +#include_code register-fpc /docs/examples/webapp-tutorial/src/fees.ts typescript + +## Components + +With the wallet logic in place, three UI components wire it together. + +### Network picker (`src/components/NetworkPicker.tsx`) + +Lets the user choose between "Local" (embedded wallet) and "Browser Wallet" (wallet extension). Calls `onNetworkChange` with the selected `NetworkType`, which determines which wallet mode the app uses. + +#include_code network-picker /docs/examples/webapp-tutorial/src/components/NetworkPicker.tsx typescript + +### Wallet connect (`src/components/WalletConnect.tsx`) + +Handles both wallet modes in a single component. For local networks it shows the embedded wallet flow (pick a test account, connect). For the browser wallet it runs the SDK discovery and verification flow. Once connected, it calls `onWalletConnected` with the `Wallet` instance. + +Open `src/components/WalletConnect.tsx`: + +#include_code wallet-connect-imports /docs/examples/webapp-tutorial/src/components/WalletConnect.tsx typescript + +#include_code wallet-connect-component /docs/examples/webapp-tutorial/src/components/WalletConnect.tsx typescript + +### Account info (`src/components/AccountInfo.tsx`) + +Displays the connected account's address. Takes the wallet as a prop. + +#include_code account-info /docs/examples/webapp-tutorial/src/components/AccountInfo.tsx typescript + +## Next steps + +With a wallet connected, you can now [deploy and interact with the Pod Racing contract from the webapp](./04-contract-interaction.md). diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/04-contract-interaction.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/04-contract-interaction.md new file mode 100644 index 00000000000..313cfec1b4c --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/04-contract-interaction.md @@ -0,0 +1,95 @@ +--- +title: "4. Contract Interaction & Gameplay" +sidebar_position: 4 +description: "Deploy and call the Pod Racing contract from the webapp, handle game lobby and private gameplay" +references: ["docs/examples/webapp-tutorial/src/components/GameLobby.tsx", "docs/examples/webapp-tutorial/src/components/GameBoard.tsx", "docs/examples/webapp-tutorial/src/components/GameStatus.tsx"] +--- + +# Contract Interaction & Gameplay + +Now that you have a wallet, you can deploy the Pod Racing contract and interact with it. This section covers the contract helper functions, the game lobby, and the gameplay components that handle private state. + +## Contract helpers + +Before your PXE (Private eXecution Environment) can interact with a contract, it needs two pieces of information: the **artifact** (the compiled contract bytecode and ABI) and the **instance** (the deployed address and constructor parameters). Without these, PXE cannot construct proofs or route transactions to the correct contract. + +Open [`src/contract.ts`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples/webapp-tutorial/src/contract.ts). This file imports the generated `PodRacingContract` class from the compiled artifacts and wraps each contract method in a simple async function that the UI components call. The key functions are described below. + +### Deploying a new contract + +The `deployContract` function calls `PodRacingContract.deploy()` to construct a deployment request and `.send()` to submit it to the network, wait for it to be mined, and return a typed contract instance you can call methods on. The deploy flow handles artifact registration with PXE internally. + +### Attaching to an existing contract + +When joining someone else's game, you didn't deploy the contract, so your PXE doesn't know about it. `attachToContract` first registers the contract with your PXE by fetching the onchain instance from the node and providing the compiled artifact — this is required for private function execution, since PXE needs the contract bytecode locally to generate proofs. It then calls `PodRacingContract.at()` to create a typed contract handle bound to the existing contract address and wallet. + +### Game actions + +Each function maps to a step in the game lifecycle: + +- `createGame(gameId)` — **public**. Creates a new `Race` struct in public storage with the caller as player 1 and sets a block deadline. +- `joinGame(gameId)` — **public**. Updates the `Race` to add the caller as player 2. The game is now active. +- `playRound(gameId, round, tracks)` — **private**. Creates a `GameRoundNote` storing your point allocation for this round. The note is encrypted and only you can read it. Enqueues a public call to increment your round counter so your opponent can see you've completed a round (without seeing your points). +- `finishGame(gameId)` — **private**. Reads all your `GameRoundNote`s, sums up totals per track, and publishes the aggregated scores to public state. This is the "reveal" phase. +- `finalizeGame(gameId)` — **public**. Compares both players' track totals, declares the winner (best of 5 tracks), and updates the win history. + +## Game lobby component + +The lobby handles two-player coordination. Player 1 deploys a new contract and creates a game, then shares the contract address with their opponent. Player 2 pastes that address, attaches to the existing contract, and joins the game. + +Open `src/components/GameLobby.tsx`: + +#include_code game-lobby-imports /docs/examples/webapp-tutorial/src/components/GameLobby.tsx typescript + +### Creating a game + +#include_code handle-create /docs/examples/webapp-tutorial/src/components/GameLobby.tsx typescript + +This deploys a fresh contract and creates a game in a single flow. The contract address is displayed so the creator can copy and share it with an opponent. + +### Joining a game + +#include_code handle-join /docs/examples/webapp-tutorial/src/components/GameLobby.tsx typescript + +The opponent pastes the contract address and game ID to join. Under the hood, this calls `attachToContract` to register the contract with the joiner's PXE, then sends a `join_game` transaction. + +## The game board component + +Open [`src/components/GameBoard.tsx`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples/webapp-tutorial/src/components/GameBoard.tsx). The board lets you allocate points across 5 tracks each round. There is a constraint: your total points per round must sum to less than 10 (i.e., at most 9 points). This forces strategic trade-offs — you can't dominate every track. + +### Submitting a round (private transaction) + +#include_code submit-round /docs/examples/webapp-tutorial/src/components/GameBoard.tsx typescript + +When the `playRound` helper sends the transaction (via `.send()`), the following happens under the hood: +1. PXE executes the private function locally, creating a `GameRoundNote` +2. A ZK proof is generated (proving validity without revealing inputs) +3. The proof and encrypted note are sent to the network +4. The network validates the proof and includes the transaction + +### Finishing and finalizing + +After all 3 rounds, the game has two more phases: + +#include_code finish-and-finalize /docs/examples/webapp-tutorial/src/components/GameBoard.tsx typescript + +- **`finish_game`**: A private function that reads all your `GameRoundNote`s, sums up totals per track, and publishes the aggregated scores. This is the "reveal" — your per-round choices stay hidden, but your final totals become public. +- **`finalize_game`**: A public function that compares both players' totals track by track and declares the winner (best of 5 tracks). Can only be called after the game's block deadline. + +Why two separate phases? Both players must call `finish_game` before anyone can call `finalize_game`. This ensures neither player can see the other's totals before committing their own. The block deadline adds a time constraint: the game must reach a certain block number before finalization, preventing a player from waiting indefinitely. + +## Game status display + +Open `src/components/GameStatus.tsx`: + +#include_code game-status-component /docs/examples/webapp-tutorial/src/components/GameStatus.tsx typescript + +## Why opponents can't see your allocations + +Private functions execute in **your** PXE using **your** decryption keys. Your opponent's PXE doesn't have your keys, so it can't decrypt your `GameRoundNote`s. This is Aztec's privacy model: private state is truly private by construction. + +The only public information during gameplay is each player's round counter (which round they're on). The actual point allocations are only revealed when a player calls `finish_game`. + +## Next steps + +Continue to [how transactions and fee payment work](./05-transactions-and-fees.md). diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/05-transactions-and-fees.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/05-transactions-and-fees.md new file mode 100644 index 00000000000..3c0a0ff7c3c --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/05-transactions-and-fees.md @@ -0,0 +1,91 @@ +--- +title: "5. Transactions & Fees" +sidebar_position: 6 +description: "Learn about the Aztec transaction lifecycle and fee payment with SponsoredFPC" +references: ["docs/examples/webapp-tutorial/src/components/TxStatus.tsx", "docs/examples/webapp-tutorial/src/fees.ts"] +--- + +# Transactions & Fees + +This section covers what happens when you send a transaction on Aztec and how fee payment works with SponsoredFPC. + +## Transaction lifecycle + +When you call `.send()` on a contract method, it handles the entire lifecycle automatically — proving, submission, and waiting for confirmation: + +```text +.send() → PXE proves locally → Sent to node → Included in block → Receipt returned +``` + +1. **Private execution**: PXE (Private eXecution Environment) simulates the function locally using your private state and decryption keys, producing new notes, nullifiers (which mark old notes as spent), and any enqueued public function calls. +2. **Proof generation**: Barretenberg (Aztec's proving system) generates a ZK-SNARK proving that the execution was valid — without revealing your private inputs to anyone. +3. **Submission**: The proof, encrypted notes, nullifiers, and any public function calls are bundled together and sent to the Aztec node. +4. **Inclusion**: The node's sequencer validates the proof, executes any enqueued public functions, and includes the transaction in the next block. +5. **Confirmation**: The block is published to L1 (Ethereum), and a receipt with the transaction hash is returned to your app. + +## Tracking transaction status + +Open `src/components/TxStatus.tsx`: + +#include_code tx-status-component /docs/examples/webapp-tutorial/src/components/TxStatus.tsx typescript + +Integrate with contract calls: + +```typescript +setTxState('sending'); +try { + const receipt = await contract.methods + .play_round(gameId, round, t1, t2, t3, t4, t5) + .send({ from: account }); + setTxState('confirmed'); + setTxHash(receipt.receipt.txHash.toString()); +} catch (err) { + setTxState('error'); + setTxError(err.message); +} +``` + +`.send()` handles the full lifecycle: it generates a proof, submits the transaction to the node, waits for it to be included in a block, and returns the receipt. If you need to send without waiting, pass `wait: NO_WAIT` in the options to get a `TxHash` back immediately instead. `NO_WAIT` is exported from `@aztec-labs/aztec.js/contracts`. + +## Fee payment with SponsoredFPC + +Every Aztec transaction requires fee payment, similar to gas on Ethereum. Without a fee, the sequencer won't include your transaction. **SponsoredFPC** (Fee Payment Contract) is a special contract that agrees to pay fees on behalf of any transaction. This is useful for onboarding new users who don't yet have fee tokens. + +### How it works + +#include_code get-sponsored-fpc /docs/examples/webapp-tutorial/src/fees.ts typescript + +`SPONSORED_FPC_SALT` is a fixed constant so that the SponsoredFPC contract is deployed at a deterministic, well-known address across all networks (local network and beyond). Your app can always compute where it lives without querying a registry. + +### Registering with PXE + +PXE needs the SponsoredFPC contract artifact registered so it can include fee payment logic when constructing transaction proofs. Without registration, PXE wouldn't know how to interact with the fee contract. + +#include_code register-fpc /docs/examples/webapp-tutorial/src/fees.ts typescript + +### Manual fee payment + +For explicit control: + +#include_code create-fee-payment /docs/examples/webapp-tutorial/src/fees.ts typescript + +```typescript +const paymentMethod = await createSponsoredFeePayment(); + +await contract.methods + .play_round(gameId, round, t1, t2, t3, t4, t5) + .send({ + from: account, + fee: { paymentMethod }, + }); +``` + +The `EmbeddedWallet` handles this automatically via `completeFeeOptions` — you don't need to pass `fee` options manually when using it. + +:::info +SponsoredFPC is for development. Production apps use their own fee payment strategy. +::: + +## Next steps + +Now [put everything together](./06-putting-it-together.md). diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/06-putting-it-together.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/06-putting-it-together.md new file mode 100644 index 00000000000..0b18a392a37 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/06-putting-it-together.md @@ -0,0 +1,118 @@ +--- +title: "6. Putting It Together" +sidebar_position: 7 +description: "Wire all components into a complete Pod Racing webapp and run it" +references: ["docs/examples/webapp-tutorial/src/main.tsx", "docs/examples/webapp-tutorial/src/App.tsx"] +--- + +# Putting It Together + +In this final section, you wire all the components together into a working app and run it. + +## Entry point + +Open `src/main.tsx`: + +#include_code main /docs/examples/webapp-tutorial/src/main.tsx typescript + +## App component + +Open `src/App.tsx`. The app is structured as a simple state machine with three phases: + +- **connect**: The user picks a wallet mode (embedded or browser extension) and connects a wallet. During this phase, PXE (Private eXecution Environment) is initialized and an account is linked. +- **lobby**: The user creates a new game (deploying a contract) or joins an existing one (attaching to a contract). Once a game is active, the app transitions forward. +- **playing**: The user plays rounds, reveals scores, and determines the winner. All gameplay actions are contract calls. + +### Imports + +#include_code app-imports /docs/examples/webapp-tutorial/src/App.tsx typescript + +### State + +#include_code app-state /docs/examples/webapp-tutorial/src/App.tsx typescript + +### Event handlers + +Each handler transitions the app to the next phase: + +- `handleWalletConnected` — receives the wallet instance from the `WalletConnect` component. For embedded wallets it reads the connected account directly; for extension wallets it fetches the account list. Then it transitions from `connect` to `lobby`. +- `handleGameJoined` — receives the typed contract instance and game ID from the `GameLobby` component, and transitions from `lobby` to `playing`. + +#include_code app-handlers /docs/examples/webapp-tutorial/src/App.tsx typescript + +### Render + +The render function conditionally displays components based on the current phase. Props flow downward: `network` goes to `WalletConnect`, `wallet` and `account` go to `GameLobby`, `contract`, `gameId`, and `currentRound` go to `GameBoard`. Each child component calls back to the parent via the event handlers above when its phase is complete. + +#include_code app-render /docs/examples/webapp-tutorial/src/App.tsx typescript + +## Running the app + +```bash +# Terminal 1: Start the local network +aztec start --local-network + +# Terminal 2: Start the dev server +yarn dev +``` + +Open `http://localhost:5173`. + +### Playing a game (local, split-screen) + +When you select "Local" network, the app renders a two-player split-screen on a single page. Both players are side by side: + +1. **Player 1 panel**: Click "Connect Account #1" — initializes a PXE and connects the first test account +2. **Player 2 panel**: Click "Connect Account #2" — initializes a separate PXE and connects the second test account +3. **Player 1 panel**: Click "Deploy Contract & Create Game" — deploys the Pod Racing contract and creates a game +4. **Player 2 panel**: Click "Join Game" — registers the contract with Player 2's PXE and joins the game +5. **Both panels**: Adjust the track sliders (total must be < 10) and click "Submit Round" — repeat for rounds 1, 2, and 3 +6. **Both panels**: Click "Reveal Scores" to call `finish_game` +7. **Player 1 panel**: Click "Finalize Game" to call `finalize_game` and determine the winner + +### With browser extension wallet + +1. Build and install the tutorial wallet extension (see [`test-extension/README.md`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples/webapp-tutorial/test-extension) for build and install instructions) +2. Run `yarn dev` +3. Select "Browser Wallet" → connect via the wallet extension → verify emojis match +4. Play the same flow as above (share the contract address with your opponent) + +## Troubleshooting + +### "SQLite worker crashed" when connecting an account + +Under `yarn dev`, Vite's dependency pre-bundling breaks the path to the PXE store's SQLite worker, so the in-browser PXE can't start. Run a production build instead and open `http://localhost:4173`: + +```bash +yarn build +yarn preview +``` + +### "SharedArrayBuffer is not defined" + +`SharedArrayBuffer` is a browser API for shared memory between threads. Aztec's WASM proving engine (Barretenberg) uses it for multithreaded proof generation. It requires the `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy` headers to be set, which the Vite config handles. Make sure you're using `yarn dev` (not opening the HTML file directly). Check `vite.config.ts`. + +### PXE initialization is slow + +The first load downloads and compiles WASM modules. Subsequent loads use the browser cache. + +### "Account not found" errors + +Make sure the local network is running (`aztec start --local-network`) and that test accounts are deployed. + +### Transaction fails with fee errors + +Ensure SponsoredFPC is registered with PXE. The `EmbeddedWallet` does this automatically. If using the wallet SDK, the extension handles fees. + +### Contract compilation fails + +Make sure the `aztec` CLI is installed and matches your package versions. Run `aztec --version` to check. The `Nargo.toml` dependency path must point to a valid `aztec-nr` location. + +## Next steps + +You now have a working Aztec webapp. From here you could: + +- Add more game features (tournaments, betting, leaderboards) +- Deploy your own contract with custom game logic +- Integrate with a production wallet +- Add persistent storage for game history diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/_category_.json b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/_category_.json new file mode 100644 index 00000000000..be1614baff6 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Building a Webapp", + "position": 1 +} diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/index.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/index.md new file mode 100644 index 00000000000..c5c9f20eeec --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/index.md @@ -0,0 +1,95 @@ +--- +title: "Building a Webapp on Aztec" +sidebar_position: 0 +description: "Build a Pod Racing game webapp with Vite, React, and Aztec — featuring private state, wallet connections, and zero-knowledge proofs." +references: ["docs/examples/webapp-tutorial/*"] +--- + +# Building a Webapp on Aztec + +In this tutorial you'll build a **Pod Racing** game webapp — a fully functional application where players privately allocate points across racing tracks, with their strategies hidden from opponents using Aztec's privacy features. + +## What you'll build + +A two-player competitive game where: +- Each player distributes up to 9 points across 5 racing tracks per round (3 rounds total) +- Point allocations are **private** — stored as encrypted notes only you can read +- After all rounds, players reveal their totals and a winner is determined (best of 5 tracks) +- The app connects to a local Aztec network, optionally via browser extension wallet + +### How the game works + +Each round, you distribute up to 9 points across 5 tracks (think of each track as an independent race). After 3 rounds, each player's per-track totals are compared: whoever allocated more points to a track wins that track. The overall winner is the player who wins the majority of the 5 tracks (best of 5). + +### Why privacy matters + +Without privacy, your opponent could see your point allocations as you play and adjust their strategy to counter yours. Aztec keeps each player's allocations encrypted as private notes — your opponent only learns that you submitted a round, not how you distributed your points. Strategies are revealed only after both players finish, making the game fair. + +## What you'll learn + +- Writing and compiling a Noir smart contract with private state +- Deploying and interacting with the contract from a TypeScript script +- Setting up a Vite + React project that runs Aztec's WASM modules in-browser +- Connecting to Aztec via an **embedded wallet** (local dev) or the **wallet SDK** (browser extension) +- Sending **private transactions** and reading **private state** +- Paying transaction fees with the **SponsoredFPC** contract + +## Prerequisites + +- [Node.js](https://nodejs.org/) v22 or later +- The Aztec CLI installed (`aztec` command available) +- A local Aztec network running (`aztec start --local-network`) +- Basic familiarity with React and TypeScript + +## Clone the example + +The tutorial walks through a complete working example. Clone it first, then follow along: + +```bash +git clone https://github.com/aztec-labs-eng/aztec-node.git +cd aztec-node +git checkout #include_aztec_version +cd docs/examples/webapp-tutorial +./setup.sh +``` + +## Architecture + +```text +┌──────────────────────────────────────────────────┐ +│ Browser │ +│ │ +│ ┌──────────┐ ┌────────┐ ┌──────────────┐ │ +│ │ React UI │───▸│ Wallet │───▸│ PXE (WASM) │ │ +│ └──────────┘ └────────┘ └──────┬───────┘ │ +│ │ │ +└───────────────────────────────────────┼──────────┘ + │ RPC + ┌──────▼───────┐ + │ Aztec Node │ + │ (local or │ + │ remote) │ + └──────────────┘ +``` + +**PXE** (Private eXecution Environment) runs in the browser as WASM. It handles private state, note discovery, and proof generation — your secrets never leave the browser. + +## Tutorial sections + +1. [The Contract](./01-the-contract.md) — understand the Pod Racing contract, compile it, deploy and interact via script +2. [Project Setup](./02-project-setup.md) — project structure overview, Vite config, environment +3. [Network & Wallet](./03-network-and-wallet.md) — connecting to Aztec, embedded wallet, wallet SDK +4. [Contract Interaction](./04-contract-interaction.md) — deploying and calling contracts from the webapp, game lobby and gameplay +5. [Transactions & Fees](./05-transactions-and-fees.md) — tx lifecycle, SponsoredFPC +6. [Putting It Together](./06-putting-it-together.md) — full App component, running the app +7. [Wallet SDK](./wallet-sdk/index.md) — deep dive into discovery, encrypted channels, capabilities, and integration on both dApp and wallet sides + +## Completed example + +The full working example including the contract source is available at [`docs/examples/webapp-tutorial/`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples/webapp-tutorial). + +The example also includes a **functional wallet extension** (`test-extension/`) that can deploy accounts and send real transactions using SponsoredFPC for fee payment. See the [Network & Wallet](./03-network-and-wallet.md#testing-with-the-tutorial-wallet-extension) section for setup instructions. + +:::tip Building a Wallet Extension? +If you want to learn how to build a browser extension wallet for Aztec, check out the companion [Wallet Extension Tutorial](../wallet-extension/index.md). It covers service workers, offscreen documents, encrypted key storage, and transaction approval flows. +::: diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/01-dapp-integration.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/01-dapp-integration.md new file mode 100644 index 00000000000..d9b5b4d483c --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/01-dapp-integration.md @@ -0,0 +1,226 @@ +--- +title: "1. dApp Integration" +sidebar_position: 1 +description: "Connect your dApp to Aztec wallet extensions: discovery, secure channels, capabilities, and wallet usage" +references: ["docs/examples/webapp-tutorial/src/wallet-connection.ts", "docs/examples/webapp-tutorial/src/components/WalletConnect.tsx", "labs/yarn-project/wallet-sdk/src/*"] +--- + +# dApp integration + +This page covers how a dApp discovers wallet extensions, establishes an encrypted channel, requests permissions, and uses the wallet. The patterns here come from the [Pod Racing tutorial](../03-network-and-wallet.md) and [GregoSwap](https://github.com/AztecProtocol/gregoswap), a reference Aztec DEX. + +## Installation + +Your dApp needs two packages: + +```bash +npm install @aztec-labs/wallet-sdk@#include_version_without_prefix @aztec-labs/aztec.js@#include_version_without_prefix +``` + +The key imports: + +```typescript +import { WalletManager, type WalletProvider, type PendingConnection } from '@aztec-labs/wallet-sdk/manager'; +import { hashToEmoji } from '@aztec-labs/wallet-sdk/crypto'; +import { Fr } from '@aztec-labs/aztec.js/fields'; +import type { Wallet, AppCapabilities, GrantedAccountsCapability } from '@aztec-labs/aztec.js/wallet'; +``` + +## Step 1: Discover wallets + +Wallet discovery broadcasts a request via `window.postMessage`. Any installed Aztec wallet extension that the user has approved responds with its info. + +#include_code discover-wallets /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript + +`WalletManager.configure()` accepts options to filter extensions: + +```typescript +const manager = WalletManager.configure({ + extensions: { + enabled: true, + allowList: ['my-trusted-wallet-id'], // Optional: only allow specific wallets + blockList: ['unwanted-wallet-id'], // Optional: block specific wallets + }, +}); +``` + +`getAvailableWallets()` returns a `DiscoverySession` with: +- `wallets`: an `AsyncIterable<WalletProvider>` that yields providers as they're discovered +- `cancel()`: stops discovery +- `done`: a `Promise` that resolves when the discovery timeout expires + +The `onWalletDiscovered` callback fires each time a wallet extension responds. Each `WalletProvider` includes: +- `id`: unique wallet identifier +- `name`: display name (e.g., "Aztec Tutorial Wallet") +- `icon`: optional data URI or URL for the wallet icon +- `metadata`: optional version info + +:::note +Discovery requires user action in the wallet extension. The wallet won't reveal itself without the user clicking "Connect" in their extension popup. This is a privacy feature: websites can't silently detect which wallets you have installed. +::: + +## Step 2: Establish a secure channel + +Once the user picks a wallet, establish an encrypted channel using ECDH key exchange: + +#include_code connect-wallet /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript + +`establishSecureChannel()` performs the ECDH P-256 key exchange and returns a `PendingConnection` with: +- `verificationHash`: hex string that both sides compute independently +- `confirm()`: creates the encrypted `Wallet` proxy (call after user verifies emojis) +- `cancel()`: aborts the connection + +## Step 3: Emoji verification + +The verification hash is converted to a 9-emoji grid using `hashToEmoji()`. Both the dApp and wallet extension compute the same emojis independently. The user visually confirms they match, defending against man-in-the-middle attacks. + +```typescript +const emojis = hashToEmoji(pending.verificationHash); +// Display emojis to the user +``` + +Show the emojis prominently in your UI: + +```tsx +{verificationEmojis && ( + <div className="emoji-verification"> + <h3>Verify Connection</h3> + <p>Check that these emojis match what your wallet extension shows:</p> + <div className="emoji-grid">{verificationEmojis}</div> + </div> +)} +``` + +After the user confirms the emojis match in the wallet extension, call `confirm()` to create the wallet instance: + +```typescript +const wallet = await confirm(); +``` + +:::tip Trusted Origins +On subsequent visits, the wallet extension can auto-approve discovery and skip emoji verification for trusted origins. The user only goes through the full flow once per origin. +::: + +## Step 4: Request capabilities + +After connecting, your dApp should declare the permissions it needs using `requestCapabilities()`. The wallet extension shows the user an approval dialog. + +The `AppCapabilities` manifest describes your dApp's identity and the permissions it requires. Each capability type maps to a set of wallet operations. For example, `accounts` lets you read account addresses, `transaction` lets you send transactions, and `simulation` lets you simulate without sending. The wallet may grant all, some, or none of the requested capabilities. + +#include_code app-capabilities /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript + +### Capability types + +| Type | What it grants | Key properties | +|------|---------------|----------------| +| `accounts` | Read accounts, create auth witnesses | `canGet`, `canCreateAuthWit` | +| `contracts` | Register contracts, query metadata | `contracts: '*' \| AztecAddress[]`, `canRegister`, `canGetMetadata` | +| `contractClasses` | Query contract class metadata | `classes: '*' \| Fr[]`, `canGetMetadata` | +| `simulation` | Simulate transactions and utilities | `transactions.scope`, `utilities.scope` | +| `transaction` | Send transactions | `scope: '*' \| ContractFunctionPattern[]` | +| `data` | Read address book, private events | `addressBook`, `privateEvents: { contracts }` | + +### Checking granted capabilities + +The wallet may deny or reduce capabilities. Check what was actually granted: + +```typescript +const capabilities = await wallet.requestCapabilities(manifest); + +// Check if accounts were granted +const accountsCap = capabilities.granted.find( + (c): c is GrantedAccountsCapability => c.type === 'accounts' +); + +if (!accountsCap?.accounts?.length) { + throw new Error('No accounts granted by wallet'); +} + +// The wallet decides which accounts to share +const account = accountsCap.accounts[0]; +console.log(`Connected as: ${account.alias} (${account.item})`); +``` + +The `WalletCapabilities` response includes: +- `granted`: array of capabilities the wallet approved (missing = denied) +- `wallet`: wallet name and version + +## Step 5: Use the wallet + +After `confirm()`, you have a standard `Wallet` instance. All method calls are encrypted transparently, so you use it like any other wallet: + +```typescript +// Get accounts +const accounts = await wallet.getAccounts(); + +// Register a contract +await wallet.registerContract(contractInstance, contractArtifact); + +// Send a transaction +const { receipt } = await wallet.sendTx(executionPayload, { from: account.item }); + +// Simulate without sending +const simulation = await wallet.simulateTx(executionPayload, { from: account.item }); + +// Create an auth witness +const authWit = await wallet.createAuthWit(account.item, messageHashOrIntent); + +// Batch multiple calls +const results = await wallet.batch([ + { name: 'simulateTx', args: [payload1, opts1] }, + { name: 'simulateTx', args: [payload2, opts2] }, +]); +``` + +## Disconnect handling + +The wallet can disconnect unexpectedly (extension unloaded, user disconnects from popup). Register a callback to handle this: + +```typescript +// Register disconnect handler +const unsubscribe = wallet.onDisconnect(() => { + console.log('Wallet disconnected'); + // Fall back to embedded wallet or show reconnect UI +}); + +// Check connection status +if (wallet.isDisconnected()) { + // Need to reconnect +} + +// Graceful disconnect +await wallet.disconnect(); +``` + +GregoSwap uses this pattern to fall back to an embedded wallet on disconnect: + +```typescript +function handleUnexpectedDisconnect() { + // Restore embedded wallet if available + if (embeddedWallet) { + setWallet(embeddedWallet); + setAddress(embeddedAddress); + } else { + setWallet(null); + } +} + +const unsubscribe = provider.onDisconnect(handleUnexpectedDisconnect); +``` + +## Complete flow + +Here's the full connection flow in a React component: + +#include_code remote-connect /docs/examples/webapp-tutorial/src/components/WalletConnect.tsx typescript + +## State types + +For tracking wallet discovery state in your app: + +#include_code wallet-sdk-types /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript + +## Next steps + +- [Wallet extension integration](./02-wallet-integration.md): Build the other side by handling discovery, managing sessions, and extending `BaseWallet` +- [Contract interaction](../04-contract-interaction.md): Deploy and call contracts with the connected wallet diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/02-wallet-integration.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/02-wallet-integration.md new file mode 100644 index 00000000000..d66e6b8e580 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/02-wallet-integration.md @@ -0,0 +1,205 @@ +--- +title: "2. Wallet Extension Integration" +sidebar_position: 2 +description: "Build an Aztec wallet extension: handle discovery, manage sessions, route messages, and extend BaseWallet" +references: ["docs/examples/webapp-tutorial/test-extension/src/background.ts", "docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts", "labs/yarn-project/wallet-sdk/src/*"] +--- + +# Wallet extension integration + +This page is a reference for wallet extension developers. It walks through each component of the SDK integration; you don't need to follow it step-by-step. For the full source, see the [`test-extension/`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples/webapp-tutorial/test-extension) directory. + +## What you'll learn + +- How to set up a **content script** that relays messages between a dApp page and the extension background +- How to use `BackgroundConnectionHandler` to manage **discovery**, **ECDH key exchange**, and **encrypted sessions** +- How to **route wallet method calls**: deciding which need user approval and which auto-execute +- How to extend `BaseWallet` to implement your own **wallet methods** (accounts, transactions, fees) +- How to handle **session lifecycle**: trusted origins, cleanup, and state persistence across service worker restarts + +## Overview + +A wallet extension has three components that use the SDK: + +| Component | SDK class | Responsibility | +|-----------|-----------|----------------| +| Content script | `ContentScriptConnectionHandler` | Relay messages between page and background | +| Background service worker | `BackgroundConnectionHandler` | Manage sessions, route messages, trigger approvals | +| Offscreen document | `BaseWallet` subclass | Execute wallet methods (sendTx, simulateTx, etc.) | + +## Content script + +The content script is the simplest piece: it relays messages between the page and the background service worker, and never sees encryption keys. Create a `ContentScriptConnectionHandler` with a transport that provides `sendToBackground(message)` and `addBackgroundListener(handler)`, then call `handler.start()`. See [`test-extension/src/content-script.ts`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples/webapp-tutorial/test-extension/src/content-script.ts) for the full implementation. + +## Background service worker + +The background script is where most of the SDK integration happens. + +### Configuration + +Define your wallet's identity in a config object with `walletId`, `name`, `icon`, and `chainId`. See [`test-extension/src/config.ts`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples/webapp-tutorial/test-extension/src/config.ts). + +### Transport + +The background transport sends messages to content scripts via `chrome.tabs.sendMessage` and filters incoming messages. Only discovery, key exchange, and encrypted wallet messages reach the handler (popup and storage proxy messages are filtered out). See the transport setup in [`test-extension/src/background.ts`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples/webapp-tutorial/test-extension/src/background.ts). + +### Handler initialization + +Create the handler with your config, transport, and callbacks: + +```typescript +const handler = new BackgroundConnectionHandler(WALLET_CONFIG, transport, callbacks); +handler.initialize(); +``` + +## Callbacks + +The SDK provides four optional callbacks at different protocol stages. The tutorial uses three (the fourth, `onSessionTerminated`, fires when a session ends and is useful for cleanup): + +### onPendingDiscovery + +Called when a dApp broadcasts a discovery request. You decide whether to show an approval UI or auto-approve: + +#include_code callbacks /docs/examples/webapp-tutorial/test-extension/src/background.ts typescript + +Key responsibilities: +- **Stale session cleanup**: terminate sessions from the same tab (handles page refresh) +- **Deduplication**: reject duplicate discoveries from the same tab +- **Trusted origins**: auto-approve if the user previously connected to this origin +- **Show UI**: open the popup for the user to approve new connections + +Call `handler.approveDiscovery(requestId)` to proceed with key exchange, or `handler.rejectDiscovery(requestId)` to deny. + +### onSessionEstablished + +Called after ECDH key exchange completes. The session has a `verificationHash` for emoji verification: + +- For **trusted origins**: auto-confirm the session and restore saved capabilities +- For **new origins**: store the session as pending verification and show the emoji grid in the popup + +### onWalletMessage + +Called when a dApp sends an encrypted wallet method call. Messages arriving before emoji verification are queued and flushed after the user confirms: + +#include_code on-wallet-message /docs/examples/webapp-tutorial/test-extension/src/background.ts typescript + +## Message routing + +The core routing logic decides which methods need user approval: + +#include_code approval-check /docs/examples/webapp-tutorial/test-extension/src/background.ts typescript + +The approval matrix: + +| Method | Approval needed? | Why | +|--------|-----------------|-----| +| `sendTx` | Yes | State-changing transaction | +| `batch` containing `sendTx` | Yes | Contains state-changing calls | +| `requestCapabilities` | Yes (first time) | Grants permissions to the dApp | +| `simulateTx` | No | Read-only simulation | +| `executeUtility` | No | Unconstrained function call | +| `getAccounts` | No | Returns account info | +| `registerContract` | No | Registers contract with PXE | +| Everything else | No | Read-only or background operations | + +## Sending responses + +Every wallet message must get a response. Use `handler.sendResponse()`, which encrypts and sends via the secure channel: + +```typescript +// Success response +await handler.sendResponse(session.sessionId, { + messageId: message.messageId, + result: someResult, + walletId: WALLET_CONFIG.walletId, +}); + +// Error response +await handler.sendResponse(session.sessionId, { + messageId: message.messageId, + error: 'Something went wrong', + walletId: WALLET_CONFIG.walletId, +}); +``` + +For auto-executing methods, forward to the offscreen document and return the result: + +#include_code send-to-offscreen /docs/examples/webapp-tutorial/test-extension/src/background.ts typescript + +## Extending BaseWallet + +The offscreen document hosts your wallet implementation. Extend `BaseWallet` to get `sendTx`, `simulateTx`, `batch`, and other methods for free: + +#include_code wallet-instance /docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript + +### What you must implement + +| Method | Purpose | +|--------|---------| +| `getAccountFromAddress(address)` | Look up an `Account` by its `AztecAddress` | +| `getAccounts()` | Return all accounts (with aliases) | + +:::tip Custom Fee Payment +`completeFeeOptions` has a default implementation that uses the sender's fee juice balance. Override it to inject a custom fee payment strategy (e.g., `SponsoredFPC`). The tutorial wallet overrides this; see [`test-extension/src/offscreen/offscreen.ts`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts) for the implementation. +::: + +### What BaseWallet provides + +| Method | What it does | +|--------|-------------| +| `sendTx(payload, opts)` | Completes fee options, creates execution request, generates proof, submits to node | +| `simulateTx(payload, opts)` | Simulates without proving | +| `executeUtility(call, opts)` | Executes an unconstrained function call | +| `batch(methods)` | Batches multiple wallet method calls | +| `createAuthWit(from, intent)` | Creates authorization witnesses | +| `registerContract(instance, artifact)` | Registers contracts with PXE | +| `getChainInfo()` | Returns chain ID and version | + +:::note +`requestCapabilities()` is part of the `Wallet` interface but throws `"Not implemented"` in BaseWallet by default. The `BackgroundConnectionHandler` handles capability requests for extension wallets (see [Callbacks](#callbacks) above). +::: + +### Dynamic method dispatch + +The offscreen document handles all wallet methods dynamically using `WalletSchema` for type-safe argument parsing: + +#include_code wallet-method-handler /docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript + +## Session lifecycle + +### Trusted origins + +Store approved origins so returning users get auto-reconnected. When a trusted origin connects, the wallet: +1. Auto-approves discovery (no popup) +2. Auto-confirms the session (no emoji verification) +3. Auto-grants capabilities (if requesting the same set) + +### Cleanup + +Sessions are cleaned up when: +- **Page refresh**: `onPendingDiscovery` terminates stale sessions from the same tab +- **Tab closed**: `chrome.tabs.onRemoved` calls `handler.terminateForTab(tabId)` +- **User disconnects**: popup sends `DISCONNECT_SESSION`, which also removes the origin from trusted origins + +The handler provides cleanup methods: +- `handler.terminateSession(sessionId)`: end a specific session +- `handler.terminateForTab(tabId)`: end all sessions for a tab +- `handler.getPendingDiscoveries()`: list pending discovery requests +- `handler.getActiveSessions()`: list active sessions + +## State persistence + +Service workers restart frequently. Use `chrome.storage.session` to persist critical state (like trusted origins), which survives service worker restarts but clears when the browser closes. + +:::note +The `BackgroundConnectionHandler`'s internal state (active sessions, pending discoveries) is **not** persisted, so sessions don't survive extension reloads. On restart, dApps will re-discover and reconnect. Trusted origin auto-approve makes this seamless. +::: + +## Extension lifecycle + +On install/update, clear pending state since sessions don't survive reloads. On startup, restore persisted state and preload the offscreen document to warm up WASM. See the lifecycle handlers in [`test-extension/src/background.ts`](https://github.com/aztec-labs-eng/aztec-node/tree/#include_aztec_version/docs/examples/webapp-tutorial/test-extension/src/background.ts). + +## Next steps + +- [dApp integration](./01-dapp-integration.md): See how the other side connects to your wallet +- [Wallet extension tutorial](../../wallet-extension/index.md): Full step-by-step guide to building a wallet extension, including accounts, transactions, and approval UIs diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/_category_.json b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/_category_.json new file mode 100644 index 00000000000..363eb6f15fe --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Wallet SDK", + "position": 8 +} diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/index.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/index.md new file mode 100644 index 00000000000..e712c500ea0 --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/index.md @@ -0,0 +1,81 @@ +--- +title: "Wallet SDK" +sidebar_position: 0 +description: "Aztec wallet SDK: secure wallet discovery, encrypted messaging, and capability-based permissions for dApps and wallet extensions" +references: ["labs/yarn-project/wallet-sdk/src/*"] +--- + +# Wallet SDK + +The `@aztec-labs/wallet-sdk` package defines how dApps and wallet extensions communicate on Aztec. It handles wallet discovery, establishes encrypted channels via ECDH key exchange, and provides a capability-based permission system, analogous to EIP-1193 and MetaMask's provider on Ethereum, but with built-in encryption and visual verification. + +This section covers **both sides** of the integration: + +1. [**dApp integration**](./01-dapp-integration.md): Discover wallets, establish secure channels, request capabilities, and use the wallet +2. [**Wallet extension integration**](./02-wallet-integration.md): Handle discovery requests, manage sessions, route messages, and extend `BaseWallet` + +## Features + +| Feature | Description | +|---------|-------------| +| **Wallet discovery** | dApps broadcast via `window.postMessage`, wallet extensions respond after user approval | +| **ECDH key exchange** | P-256 ephemeral key pairs derive a shared secret for each session | +| **AES-256-GCM encryption** | All wallet method calls and responses are encrypted after key exchange | +| **Emoji verification** | A 9-emoji grid (72-bit security) lets users visually confirm there's no man-in-the-middle | +| **Capability permissions** | dApps declare what they need (`AppCapabilities`), wallets grant or deny each capability | +| **Trusted origin reconnect** | Previously approved origins auto-reconnect without re-verification | +| **BaseWallet** | Abstract class that wallet extensions extend; provides `sendTx`, `simulateTx`, `batch`, and more | + +## Architecture + +```text +┌──────────────┐ window.postMessage ┌──────────────────────────────┐ +│ │ ←─────────────────────────────→ │ Content Script │ +│ dApp │ (discovery only) │ ContentScriptConnectionHandler +│ │ └──────────┬───────────────────┘ +│ WalletManager│ MessagePort (encrypted) │ chrome.runtime +│ ExtensionWallet ←────────────────────────────→ ┌──────────▼───────────────────┐ +│ │ │ Background Service Worker │ +└──────────────┘ │ BackgroundConnectionHandler │ + └──────────┬───────────────────┘ + │ persistent port + ┌──────────▼───────────────────┐ + │ Offscreen Document │ + │ OffscreenWallet (BaseWallet) │ + │ PXE + WASM proofs │ + └──────────────────────────────┘ +``` + +**Discovery** uses `window.postMessage` (unencrypted, public). After the user approves, a `MessagePort` is transferred and **ECDH key exchange** establishes an encrypted channel. All subsequent wallet method calls flow through this encrypted `MessagePort`. + +## Package exports + +The SDK is split into focused entry points: + +| Import path | What it provides | +|-------------|------------------| +| `@aztec-labs/wallet-sdk/manager` | `WalletManager`, `WalletProvider`, `PendingConnection`, `DiscoverySession`: dApp-side discovery and connection | +| `@aztec-labs/wallet-sdk/crypto` | `hashToEmoji`: convert verification hash to emoji string | +| `@aztec-labs/wallet-sdk/extension/handlers` | `BackgroundConnectionHandler`, `ContentScriptConnectionHandler`: wallet extension handlers | +| `@aztec-labs/wallet-sdk/extension/provider` | `ExtensionWallet`, `ExtensionProvider`: low-level provider classes | +| `@aztec-labs/wallet-sdk/base-wallet` | `BaseWallet`: abstract wallet class that extensions subclass | + +## Security model + +The protocol has three phases with increasing trust: + +1. **Discovery (public)**: The dApp broadcasts a request. Wallet extensions only respond after the user explicitly approves the connection in the extension popup. No cryptographic material is exchanged. + +2. **Key exchange (authenticated)**: Both sides generate ephemeral ECDH P-256 key pairs. The shared secret is expanded via HKDF into an AES-256-GCM encryption key and an HMAC verification key. A 2-second timeout limits the window for interception. + +3. **Verified channel (encrypted)**: The HMAC verification key produces a hash that both sides independently convert to a 9-emoji grid. The user visually confirms the emojis match on both the dApp and wallet extension, defending against man-in-the-middle attacks. After confirmation, all messages are encrypted with AES-256-GCM. + +## Next steps + +- **Building a dApp?** Start with [dApp integration](./01-dapp-integration.md) +- **Building a wallet?** Start with [Wallet extension integration](./02-wallet-integration.md) +- **New to Aztec wallets?** Read [Network & Wallet](../03-network-and-wallet.md) first for the basics + +:::tip Related Tutorials +The [Wallet extension tutorial](../../wallet-extension/index.md) walks through building a complete Chrome extension wallet step by step, covering service workers, offscreen documents, encrypted storage, and approval flows. +::: diff --git a/docs/lines/v6/docs-developers/docs/tutorials/testing_governance_rollup_upgrade.md b/docs/lines/v6/docs-developers/docs/tutorials/testing_governance_rollup_upgrade.md new file mode 100644 index 00000000000..e2e421bd48d --- /dev/null +++ b/docs/lines/v6/docs-developers/docs/tutorials/testing_governance_rollup_upgrade.md @@ -0,0 +1,423 @@ +--- +title: Testing Governance Rollup Upgrade on Local Network +sidebar_position: 5 +tags: [local_network, governance, testing] +description: Deploy a new rollup and execute a governance upgrade on a local Aztec network for testing. +references: ["labs/yarn-project/cli/src/cmds/l1/*"] +--- + +This guide walks through deploying a new rollup and executing a governance upgrade on a local Aztec network. + +## Prerequisites + +- [Aztec tooling](../../getting_started_on_local_network.md) +- Node.js and yarn + +## Local Network Governance Timing + +The default governance configuration for local networks: + +| Parameter | Value | Description | +| -------------- | ---------------- | ------------------------------------------- | +| votingDelay | 60 seconds | Time before voting starts | +| votingDuration | 1 hour | Voting period length | +| executionDelay | 60 seconds | Delay after voting ends before execution | +| gracePeriod | 7 days | Window to execute after becoming executable | +| lockDelay | 30 days | Token lock period for proposers | +| lockAmount | 1,000,000 tokens | Tokens locked when proposing | + +--- + +## Step 1: Start Local Network + +Ensure you are on the correct Aztec version: + +```bash +aztec-up install #release_version +``` + +```bash +aztec start --local-network +``` + +Wait for output showing deployed contract addresses. To get the **Registry Address** and other L1 contract addresses, query the running node: + +```bash +curl -s http://localhost:8080 -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"aztec_getNodeInfo","params":[],"id":1}' | jq '.result.l1ContractAddresses' +``` + +Note the `registryAddress` from the output. + +--- + +## Step 2: Download the l1-contracts bundle + +The [`@aztec-foundation/l1-artifacts`](https://www.npmjs.com/package/@aztec-foundation/l1-artifacts) npm package bundles a self-contained Foundry project with the L1 contract sources, prebuilt artifacts, deploy scripts, and governance payload contracts. Download the release this version of Aztec builds against: + +```bash +npm pack @aztec-foundation/l1-artifacts@#include_foundation_version_without_prefix +mkdir l1-contracts +tar xzf aztec-foundation-l1-artifacts-#include_foundation_version_without_prefix.tgz --strip-components=2 -C l1-contracts package/l1-contracts +cd l1-contracts +``` + +No further setup is needed: the bundle includes the library dependencies and the generated verifier, and forge downloads the matching solc version automatically on first use. + +:::note +The Aztec installer ships Foundry as `aztec-forge`, `aztec-cast`, and `aztec-anvil`. The commands below use `aztec-forge` and plain `cast`: if you only have the installer's Foundry, use `aztec-cast` wherever a command says `cast`, and if you have your own Foundry, `forge` works in place of `aztec-forge`. +::: + +--- + +## Step 3: Set Environment Variables + +```bash +# Anvil's default account 0 +export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +export DEPLOYER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + +# Replace with actual address from Step 1 +export REGISTRY_ADDRESS=0x... + +# L1 RPC +export L1_RPC_URL=http://localhost:8545 +export L1_CHAIN_ID=31337 + +# Rollup configuration (local network defaults) +export AZTEC_SLOT_DURATION=36 +export AZTEC_EPOCH_DURATION=16 +export AZTEC_TARGET_COMMITTEE_SIZE=48 +export AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET=2 +export AZTEC_LAG_IN_EPOCHS_FOR_RANDAO=1 +export AZTEC_PROOF_SUBMISSION_EPOCHS=2 +export AZTEC_LOCAL_EJECTION_THRESHOLD=0 +export AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS=1 +export AZTEC_SLASHING_LIFETIME_IN_ROUNDS=10 +export AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS=1 +export AZTEC_SLASHING_OFFSET_IN_ROUNDS=0 +export AZTEC_SLASHER_ENABLED=false +export AZTEC_SLASHING_VETOER=0x0000000000000000000000000000000000000000 +export AZTEC_SLASHING_DISABLE_DURATION=0 +export AZTEC_MANA_TARGET=100000000 +export AZTEC_EXIT_DELAY_SECONDS=172800 +export AZTEC_PROVING_COST_PER_MANA=100 +export AZTEC_SLASH_AMOUNT_SMALL=0 +export AZTEC_SLASH_AMOUNT_MEDIUM=0 +export AZTEC_SLASH_AMOUNT_LARGE=0 +export AZTEC_INITIAL_ETH_PER_FEE_ASSET=10000000 +export ETHEREUM_SLOT_DURATION=12 + +# Validator entry queue (local network defaults) +export AZTEC_ENTRY_QUEUE_BOOTSTRAP_VALIDATOR_SET_SIZE=0 +export AZTEC_ENTRY_QUEUE_BOOTSTRAP_FLUSH_SIZE=0 +export AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN=48 +export AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT=2 +export AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE=48 + +# Genesis state: reuse the current rollup's, since the new rollup runs the same circuits +export CURRENT_ROLLUP=$(cast call $REGISTRY_ADDRESS "getCanonicalRollup()(address)" --rpc-url $L1_RPC_URL) +export VK_TREE_ROOT=$(cast call $CURRENT_ROLLUP "getVkTreeRoot()(bytes32)" --rpc-url $L1_RPC_URL) +export PROTOCOL_CONTRACTS_HASH=$(cast call $CURRENT_ROLLUP "getProtocolContractsHash()(bytes32)" --rpc-url $L1_RPC_URL) +export GENESIS_ARCHIVE_ROOT=$(cast call $CURRENT_ROLLUP "archiveAt(uint256)(bytes32)" 0 --rpc-url $L1_RPC_URL) +``` + +`AZTEC_EXIT_DELAY_SECONDS` must be non-zero: the deploy script rejects a zero exit delay. + +--- + +## Step 4: Deploy New Rollup + +```bash +aztec-forge script script/deploy/DeployRollupForUpgrade.s.sol:DeployRollupForUpgrade \ + --rpc-url $L1_RPC_URL \ + --broadcast \ + --private-key $PRIVATE_KEY +``` + +Note the **new rollup address** from the JSON output. + +```bash +export NEW_ROLLUP_ADDRESS=0x... +``` + +--- + +## Step 5: Deploy Governance Payload + +:::tip +The deploy script in Step 4 also deploys this payload and prints it as `payloadAddress` in its JSON output. You can export that address as `PAYLOAD_ADDRESS` and skip this step. +::: + +**Important:** Place flags before the contract path to avoid argument parsing issues. + +```bash +aztec-forge create \ + --rpc-url $L1_RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast \ + src/periphery/RegisterNewRollupVersionPayload.sol:RegisterNewRollupVersionPayload \ + --constructor-args $REGISTRY_ADDRESS $NEW_ROLLUP_ADDRESS +``` + +Note the **payload address** from the output. + +```bash +export PAYLOAD_ADDRESS=0x... +``` + +--- + +## Step 6: Deposit Governance Tokens + +Mint and deposit tokens to get voting power. You need at least 1,000,000 tokens (1e24 wei) to propose: + +```bash +aztec deposit-governance-tokens \ + -r $REGISTRY_ADDRESS \ + --recipient $DEPLOYER_ADDRESS \ + --amount "2000000000000000000000000" \ + --mint \ + --l1-rpc-urls $L1_RPC_URL \ + -c $L1_CHAIN_ID \ + --private-key $PRIVATE_KEY +``` + +--- + +## Step 7: Advance Time for Token Checkpoint + +:::warning Critical Step +Tokens must be deposited **before** the proposal is created. The governance contract snapshots voting power at the proposal creation timestamp. If your deposit checkpoint timestamp >= proposal creation timestamp, your voting power will be **0** and the proposal will be rejected. +::: + +Advance Anvil's time to ensure the checkpoint is in the past when the proposal is created: + +```bash +# Get current timestamp and add 120 seconds +CURRENT_TS=$(cast block latest --rpc-url $L1_RPC_URL --json | jq -r '.timestamp') +TARGET_TS=$((CURRENT_TS + 120)) +cast rpc anvil_setNextBlockTimestamp $TARGET_TS --rpc-url $L1_RPC_URL +cast rpc anvil_mine 1 --rpc-url $L1_RPC_URL +``` + +Verify the time has advanced: + +```bash +NEW_TS=$(cast block latest --rpc-url $L1_RPC_URL --json | jq -r '.timestamp') +echo "New timestamp: $NEW_TS (should be > $CURRENT_TS)" +``` + +:::note +`anvil_increaseTime` may not reliably update block timestamps. For consistent results, always use `anvil_setNextBlockTimestamp` with an explicit timestamp. +::: + +--- + +## Step 8: Create Proposal + +```bash +aztec propose-with-lock \ + -r $REGISTRY_ADDRESS \ + -p $PAYLOAD_ADDRESS \ + --l1-rpc-urls $L1_RPC_URL \ + -c $L1_CHAIN_ID \ + --private-key $PRIVATE_KEY \ + --json +``` + +Note the **proposal ID** from output. + +```bash +export PROPOSAL_ID=0 +``` + +--- + +## Step 9: Advance Time Past Voting Delay + +The proposal must transition from Pending to Active (votingDelay = 60 seconds): + +```bash +# Get current timestamp and add 120 seconds (buffer over 60s voting delay) +CURRENT_TS=$(cast block latest --rpc-url $L1_RPC_URL --json | jq -r '.timestamp') +TARGET_TS=$((CURRENT_TS + 120)) +cast rpc anvil_setNextBlockTimestamp $TARGET_TS --rpc-url $L1_RPC_URL +cast rpc anvil_mine 1 --rpc-url $L1_RPC_URL +``` + +Verify the proposal is now Active (state 1): + +```bash +# Get governance address from node info or use the one from Step 1 +cast call <GOVERNANCE_ADDRESS> "getProposalState(uint256)(uint8)" $PROPOSAL_ID --rpc-url $L1_RPC_URL +# Expected output: 1 (Active) +``` + +--- + +## Step 10: Vote on Proposal + +```bash +aztec vote-on-governance-proposal \ + -p $PROPOSAL_ID \ + --in-favor yea \ + --wait false \ + -r $REGISTRY_ADDRESS \ + --l1-rpc-urls $L1_RPC_URL \ + -c $L1_CHAIN_ID \ + --private-key $PRIVATE_KEY +``` + +Verify the vote was recorded with your voting power. The CLI output should show non-zero `summedBallot yea` values. If it shows `[0]`, your checkpoint timing was incorrect (see Troubleshooting). + +--- + +## Step 11: Advance Time Past Voting Duration + Execution Delay + +Voting duration is 1 hour (3600s) and execution delay is 60 seconds: + +```bash +# Get current timestamp and add 3700 seconds (voting duration + execution delay + buffer) +CURRENT_TS=$(cast block latest --rpc-url $L1_RPC_URL --json | jq -r '.timestamp') +TARGET_TS=$((CURRENT_TS + 3700)) +cast rpc anvil_setNextBlockTimestamp $TARGET_TS --rpc-url $L1_RPC_URL +cast rpc anvil_mine 1 --rpc-url $L1_RPC_URL +``` + +Verify the proposal is now Executable (state 3): + +```bash +cast call <GOVERNANCE_ADDRESS> "getProposalState(uint256)(uint8)" $PROPOSAL_ID --rpc-url $L1_RPC_URL +# Expected output: 3 (Executable) +``` + +--- + +## Step 12: Execute Proposal + +```bash +aztec execute-governance-proposal \ + -p $PROPOSAL_ID \ + -r $REGISTRY_ADDRESS \ + --wait false \ + --l1-rpc-urls $L1_RPC_URL \ + -c $L1_CHAIN_ID \ + --private-key $PRIVATE_KEY +``` + +## Step 13: Verify the Upgrade + +Confirm the new rollup is now the canonical rollup: + +```bash +# Check the canonical rollup address (should match NEW_ROLLUP_ADDRESS) +cast call $REGISTRY_ADDRESS "getCanonicalRollup()(address)" --rpc-url $L1_RPC_URL + +# Check the number of rollup versions (should be 2) +cast call $REGISTRY_ADDRESS "numberOfVersions()(uint256)" --rpc-url $L1_RPC_URL +``` + +--- + +## Helper Commands + +### Set Anvil timestamp directly + +If time advancement isn't working as expected, set the timestamp explicitly: + +```bash +# Get the target timestamp (current + desired seconds) +cast rpc anvil_setNextBlockTimestamp <UNIX_TIMESTAMP> --rpc-url $L1_RPC_URL +cast rpc anvil_mine 1 --rpc-url $L1_RPC_URL +``` + +### Check proposal state + +```bash +# States: 0=Pending, 1=Active, 2=Queued, 3=Executable, 4=Rejected, 5=Executed, 6=Dropped, 7=Expired +cast call <GOVERNANCE_ADDRESS> "getProposalState(uint256)(uint8)" $PROPOSAL_ID --rpc-url $L1_RPC_URL +``` + +### Check current block timestamp + +```bash +cast block latest --rpc-url $L1_RPC_URL | grep timestamp +``` + +### Check L1 addresses + +```bash +aztec get-l1-addresses \ + -r $REGISTRY_ADDRESS \ + -v canonical \ + --l1-rpc-urls $L1_RPC_URL \ + -c $L1_CHAIN_ID \ + --json +``` + +### Debug rollup state + +```bash +aztec debug-rollup \ + --rollup $NEW_ROLLUP_ADDRESS \ + --l1-rpc-urls $L1_RPC_URL \ + -c $L1_CHAIN_ID +``` + +The `--rollup` flag is required; without it the command may fail trying to resolve the default rollup address. + +--- + +## Quick Test (Empty Payload) + +If you just want to test the governance flow without deploying a real rollup: + +```bash +# Deploy empty payload (no constructor args needed) +aztec-forge create \ + --rpc-url $L1_RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast \ + test/governance/governance/TestPayloads.sol:EmptyPayload + +# Use the deployed address as PAYLOAD_ADDRESS and continue from Step 6 +``` + +--- + +## Troubleshooting + +### "Governance**CheckpointedUintLib**InsufficientValue" + +- You need more tokens. The minimum to propose is 1,000,000 tokens (1e24 wei). +- Deposit more tokens in Step 6. + +### "Governance**CheckpointedUintLib**NotInPast" + +- Tokens were deposited at or after the proposal creation time. +- Advance Anvil's time and mine a block before creating the proposal (Step 7). + +### "Proposal is not active" + +- The voting delay hasn't passed yet. +- Advance time past the votingDelay (60 seconds for local networks). + +### "Proposal is not executable" + +- Either voting period is not complete, or execution delay hasn't passed. +- Advance time past votingDuration (1 hour) + executionDelay (60 seconds). + +### Forge create fails with "Error accessing local wallet" + +- Constructor args may be parsing incorrectly. Place `--constructor-args` at the end of the command, after the contract path. + +### Time advancement not working + +- Anvil may have auto-mined blocks that reset the accumulated time. +- Use `anvil_setNextBlockTimestamp` to set an explicit timestamp instead of `anvil_increaseTime`. + +### Vote fails without explicit amount + +- If you see `NotInPast` errors during voting, the CLI may have a bug determining voting power. +- Workaround: specify `--vote-amount` explicitly with your deposited token amount. diff --git a/docs/lines/v6/docs-developers/getting_started_on_local_network.md b/docs/lines/v6/docs-developers/getting_started_on_local_network.md new file mode 100644 index 00000000000..8cdab5aa33d --- /dev/null +++ b/docs/lines/v6/docs-developers/getting_started_on_local_network.md @@ -0,0 +1,284 @@ +--- +title: Getting Started on Local Network +description: Guide for developers to get started with the Aztec local network, including account creation and contract deployment. +sidebar_position: 0 +tags: [local_network, testnet] +references: ["labs/aztec-up/*", "labs/yarn-project/aztec/src/cli/*", "labs/yarn-project/cli-wallet/src/*", "labs/noir-projects/noir-contracts/contracts/app/token_contract/*"] +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Get started on your local environment using a local network. If you'd rather deploy to a live network, read the [getting started on testnet guide](./getting_started_on_testnet.md). + +The local network is a local development Aztec network running fully on your machine, and interacting with a development Ethereum node. You can develop and deploy on it just like on a testnet or mainnet (when the time comes). The local network makes it faster and easier to develop and test your Aztec applications. + +The local network always owns the local chain it starts. It deploys its own Aztec protocol contracts to the local Ethereum node and is not a mode for connecting to an existing Aztec network. + +What's included in the local network: + +- Local Ethereum network (Anvil) +- Deployed Aztec protocol contracts (for L1 and L2) +- A set of test accounts with some test tokens to pay fees +- On-demand block production via the automine sequencer +- Development tools to compile contracts and interact with the network (`aztec` and `aztec-wallet`) + +This guide will teach you how to install the Aztec local network, run it using the Aztec CLI, and interact with contracts using the wallet CLI. To jump right into the testnet instead, click the `Testnet` tab. + +import YouTubeEmbed from '@site/src/components/YouTubeEmbed'; + +To see the whole flow before you start, watch this one-minute walkthrough (find more on the [video lessons](./docs/resources/video_lessons.mdx) page): + +<YouTubeEmbed videoId="_jgHNdNgFOg" title="Get Started on Aztec in Under 60 Seconds" /> + +## Prerequisites + +import { General, Fees } from '@site/src/components/Snippets/general_snippets'; + +- <General.node_ver /> + +### macOS-specific requirements + +- **Homebrew**: [Homebrew](https://brew.sh/) is required for installing dependencies on macOS. +- **Bash**: macOS ships with an outdated version of Bash (v3.2) that is known to cause issues with the Aztec installer. Install a modern version with `brew install bash`. Even if you use zsh as your default shell, the installer explicitly invokes `bash`. If the installer still picks up the old version, add the Homebrew `bash` to your `$PATH` or [set it as your default shell](https://support.apple.com/en-gb/guide/terminal/trml113/mac). + +## Install and run the local network + +### Install the Aztec toolchain + +Run: + +```bash +VERSION=#include_version_without_prefix bash -i <(curl -sL https://install.aztec.network) +``` + +This will install the following tools and add them to your `PATH`: + +- **aztec** - compiles and tests Aztec contracts and launches various infrastructure subsystems (full local network, sequencer, prover, PXE, etc.) and provides utility commands to interact with the network +- **aztec-up** - a version manager for the Aztec toolchain. Use `aztec-up install <version>` to install a new version, `aztec-up use <version>` to switch between installed versions, or `aztec-up list` to see installed versions. +- **aztec-wallet** - a tool for interacting with the Aztec network +- **aztec-bb** - the Barretenberg proving backend +- **aztec-nargo** - the Noir compiler and simulator +- **aztec-forge**, **aztec-cast**, **aztec-anvil**, **aztec-chisel** - the bundled Foundry tools + +Foundry, Noir, and Barretenberg are bundled at the versions `aztec` needs. Your own `forge` / `nargo` / `bb` installs still work under their bare names. + +For syntax highlighting and LSP support while editing contracts, see the [Noir VSCode Extension guide](./docs/aztec-nr/installation.md). + +### Start the local network + +Once these have been installed, to start the local network, run: + +```bash +aztec start --local-network +``` + +**Congratulations, you have just installed and run the Aztec local network!** + +```bash + /\ | | + / \ ___| |_ ___ ___ + / /\ \ |_ / __/ _ \/ __| + / ____ \ / /| || __/ (__ + /_/___ \_\/___|\__\___|\___| + +``` + +In the terminal, you will see some logs: + +1. Local network version +2. Contract addresses of rollup contracts +3. PXE (private execution environment) setup logs +4. Initial accounts that are shipped with the local network and can be used in tests + +You'll know the local network is ready to go when you see something like this: + +```bash +[INFO] Aztec Server listening on port 8080 +``` + +## Using the local network test accounts + +import { CLI_Add_Test_Accounts } from '@site/src/components/Snippets/general_snippets'; + +<CLI_Add_Test_Accounts /> + +To add the test accounts in the wallet, run this in another terminal: + +```bash +aztec-wallet import-test-accounts +``` + +We'll use the first test account, `test0`, throughout to pay for transactions. + +## Creating an account in the local network + +```bash +aztec-wallet create-account -a my-wallet -f test0 +``` + +:::info +`aztec-wallet` will generate transaction proofs by default. This is not required when sending transactions on the local network, but it is required when sending transactions on the devnet or mainnet. + +You can turn off proof generation by adding the `--prover none` flag to the command or setting `PXE_PROVER=none`. +::: + +This will create a new wallet with an account and give it the alias `my-wallet`. Accounts can be referenced with `accounts:<alias>`. You will see logs telling you the address, public key, secret key, and more. + +On successful deployment of the account, you should see something like this: + +```bash +New account: + +Address: 0x1653045495c127a6033ed7b2e19ed1e13598a027b66eed8eaad3d8d913ed01a0 +Public key: 0x26bc01e8531f455fdaf8e292a84d569a1d2a0b8c7e6a29c968cb9485a27bcff31378289311d6a6e6659c7a21eaff4684a4d6c45a797bd9edde3ef720d821485426f5bef32d4efd025505f944d39c2ea5ef386b4bf1037d53c626e1cbb418d20e2d33546468da0350fde817c09b1f31ccfcf82c7a64d5b120fe1bd7e0e41cd8c62342e51cc7aa750db8982540e4c35fba85fb171357e82d7703de16be0ffde54e24f2f2e494d8249c9c06e5e50ccc9885070dee5cf0d5a1b9d847c47dfb581db4141ed3b7388694ca40c71008b9461d43453a8f193d9f02e5e3fccf4f2d556fe0 +Signing key: 0x164c5ef7bfa426c2be1cd5b44dea22c214e9bbd2e4b0e9aa9f7e938f607df9bb +Secret key: 0x11dcaef4b44e4ccbdb151bb8d6a2ac3685c2ac1a99c9b9ff671773de7319e5b0 +Partial address: 0x18395659bf180a60e6a4784a48598709a41e4305a257074f2edadd66bcd6afa3 +Salt: 0x0000000000000000000000000000000000000000000000000000000000000000 +Init hash: 0x0ba38faf3fc4b22432ea4eceedaef55d096373130182cea8fe0d810c5f4ec121 +Using Fee Juice for fee payment with the balance of account 0x1653045495c127a6033ed7b2e19ed1e13598a027b66eed8eaad3d8d913ed01a0 +Waiting for account contract deployment... + Local processing time: 1.8s + Node inclusion time: 3.0s +Deploy tx hash: 0x154f0e6c3b42551aa8fdca86c2b1ea850c51b27cce48eedcd52f7e777cdf0f75 +Deploy tx fee: 590481708570000000 +Account stored in database with aliases last & my-wallet +``` + +You may need to scroll up as there are some other logs printed after it. + +You can double check by running `aztec-wallet get-alias accounts:my-wallet`. + +For simplicity we'll keep using the test account, let's deploy our own test token! + +## Deploying a contract + +The local network comes with some contracts that you can deploy and play with. One of these is an example token contract. + +Deploy it with this: + +```bash +aztec-wallet deploy TokenContractArtifact --from accounts:test0 --args accounts:test0 TestToken TST 18 -a testtoken +``` + +This takes + +- the contract artifact as the argument, which is `TokenContractArtifact` +- the deployer account, which we used `test0` +- the args that the contract constructor takes, which is the `admin` (`accounts:test0`), `name` (`TestToken`), `symbol` (`TST`), and `decimals` (`18`). +- an alias `testtoken` (`-a`) so we can easily reference it later with `contracts:testtoken` + +On successful deployment, you should see something like this: + +```bash +Contract deployed at 0x063a076da3e530c1f6cd7e598647d50c3ff0e4225c0767402f93f73f1c2c9174 +Contract partial address 0x09c1b45b9a75cfaccf90c867da84f5180b75fdc6e1ec0e940750aead964a4c5b +Contract init hash 0x04cb120f3df6e270bb342d816f54562ce5dddb45132065d70cc40e43bb47b527 +Deployment tx hash: 0x16edec75331f8b07da955c4cca4957c25857f6c0fe46fc68e7251bfbd74b1cbe +Deployment salt: 0x05a1f0a1bae0bb97eff002ac3de719c7e7fdd90db14161cb0c0898610120240a +Deployer: 0x0e900d357d0a7b9de3c0293c6b1a58dccfc1862f31c745ba5d3a5a5aaf4ac1fd +Transaction fee: 1101755679159600000 + Local processing time: 2.0s + Node inclusion time: 3.0s +Contract stored in database with aliases last & testtoken +``` + +In the next step, let's mint some tokens! + +## Minting public tokens + +Call the public mint function like this: + +```bash +aztec-wallet send mint_to_public --from accounts:test0 --contract-address contracts:testtoken --args accounts:test0 100 +``` + +This takes + +- the function name as the argument, which is `mint_to_public` +- the `from` account (caller) which is `accounts:test0` +- the contract address, which is aliased as `contracts:testtoken` (or simply `testtoken`) +- the args that the function takes, which is the account to mint the tokens into (`test0`), and `amount` (`100`). + +This only works because we are using the secret key of the admin who has permissions to mint. + +A successful call should print something like this: + +```bash +Transaction hash: 0x1cf00af686c20860bc3621a47c6a892d9797c918ba09019431acf26fb16fa13e +Transaction has been proposed + Tx fee: 609443487226800000 + Status: checkpointed + Block number: 6 + Block hash: 0x23470fd2c6688da45b40df538eb3753f405deabc3ca17dc06f350aad764dbda6 + Local processing time: 1.5s + Node inclusion time: 3.0s +Transaction hash stored in database with aliases last & mint_to_public-bd68d89b090987cb1a4cb7bd5b627361 +``` + +You can double-check by calling the function that checks your public account balance: + +```bash +aztec-wallet simulate balance_of_public --from test0 --contract-address testtoken --args accounts:test0 +``` + +This should print + +```bash +Simulation result: 100n +``` + +## Playing with hybrid state and private functions + +In the following steps, we'll move some tokens from public to private state and check our private and public balance. + +```bash +aztec-wallet send transfer_to_private --from accounts:test0 --contract-address testtoken --args accounts:test0 25 +``` + +The arguments for `transfer_to_private` function are: + +- the account address to transfer to +- the amount of tokens to send to private + +A successful call should print something similar to what you've seen before. + +Now when you call `balance_of_public` again you will see 75! + +```bash +aztec-wallet simulate balance_of_public --from test0 --contract-address testtoken --args accounts:test0 +``` + +This should print + +```bash +Simulation result: 75n +``` + +And then call `balance_of_private` to check that you have your tokens! + +```bash +aztec-wallet simulate balance_of_private --from test0 --contract-address testtoken --args accounts:test0 +``` + +This should print + +```bash +Simulation result: 25n +``` + +**Congratulations, you now know the fundamentals of working with the Aztec local network!** You are ready to move onto the more fun stuff. + +## What's next? + +Want to build something cool on Aztec? + +- Check out the [Token Contract Tutorial](./docs/tutorials/contract_tutorials/token_contract.md) for a beginner tutorial, or jump into more advanced ones +- Test your contracts with `aztec test`: see [Testing contracts](./docs/aztec-nr/testing_contracts.md) +- Ready for a live network? Try [deploying on testnet](./getting_started_on_testnet.md) +- Start on your own thing and check out the How To Guides to help you! + +:::tip Need help? +If something does not work, see the [support guide](./support.md). It tells you when to ask in [Discord](https://discord.gg/aztec) or the [forum](https://forum.aztec.network), when to [open a GitHub issue](https://github.com/AztecProtocol/aztec-packages/issues/new?template=bug_report.yml), and how to disclose security issues responsibly. +::: diff --git a/docs/lines/v6/docs-developers/getting_started_on_testnet.md b/docs/lines/v6/docs-developers/getting_started_on_testnet.md new file mode 100644 index 00000000000..ef460fc6bc2 --- /dev/null +++ b/docs/lines/v6/docs-developers/getting_started_on_testnet.md @@ -0,0 +1,220 @@ +--- +title: Getting Started on Testnet +sidebar_position: 1 +tags: [testnet] +description: Deploy contracts and send transactions on the Aztec testnet using the CLI wallet and the Sponsored FPC for fee payment. +references: ["labs/aztec-up/*", "labs/yarn-project/cli-wallet/src/*", "labs/noir-projects/noir-contracts/contracts/app/token_contract/*", "labs/noir-projects/noir-contracts/contracts/fees/sponsored_fpc_contract/*"] +--- + +import { General } from '@site/src/components/Snippets/general_snippets'; + +This guide walks you through deploying your first contract on the Aztec testnet. You will install the CLI tools, create an account using the Sponsored FPC (so you don't need to bridge Fee Juice yourself), and deploy and interact with a contract. + +## Testnet vs Local Network + +| Feature | Local Network | Testnet | +|---------|-------------|---------| +| **Environment** | Local machine | Decentralized network on Sepolia | +| **Fees** | Free (test accounts prefunded) | Sponsored FPC available | +| **Proving** | Optional | Required | +| **Accounts** | Test accounts pre-deployed | Must create and deploy your own | + +:::info +If you want to develop and iterate quickly, start with the [local network guide](./getting_started_on_local_network.md). The local network has instant blocks and no proving, making it faster for development. +::: + +## Prerequisites + +- <General.node_ver /> + +## Install the Aztec toolchain + +Install the testnet version of the Aztec CLI: + +```bash +VERSION=#include_testnet_version bash -i <(curl -sL https://install.aztec.network/#include_testnet_version) +``` + +:::warning +Testnet is version-dependent. It is currently running version `#include_testnet_version`. Maintain version consistency when interacting with the testnet to avoid errors. +::: + +This installs: + +- **aztec** - Compiles and tests Aztec contracts, launches infrastructure, and provides utility commands +- **aztec-up** - Version manager for the Aztec toolchain (`aztec-up install`, `aztec-up use`, `aztec-up list`) +- **aztec-wallet** - CLI tool for interacting with the Aztec network + +## Getting started on testnet + +### Step 1: Set up your environment + +Set the required environment variables: + +```bash +export NODE_URL=https://aztec-testnet.drpc.org +``` + +### Step 2: Register the Sponsored FPC + +The Sponsored FPC (Fee Payment Contract) pays transaction fees on your behalf, so you don't need to bridge Fee Juice from L1. Your wallet already knows its address under the `contracts:SponsoredFPC` alias, but it still needs the contract's artifact to build transactions that use it. Register it in your wallet: + +```bash +aztec-wallet register-contract \ + --node-url $NODE_URL \ + --alias sponsoredfpc \ + contracts:SponsoredFPC SponsoredFPC \ + --salt 0 +``` + +### Step 3: Create and deploy an account + +Unlike the local network, testnet has no pre-deployed accounts. Create and deploy your own: + +```bash +aztec-wallet create-account \ + --node-url $NODE_URL \ + --alias my-wallet \ + --payment method=fpc-sponsored,fpc=contracts:SponsoredFPC +``` + +:::note +The first transaction will take longer as it downloads proving keys. If you see `Timeout awaiting isMined`, the transaction is still processing: this is normal on testnet. +::: + +### Step 4: Deploy a contract + +Deploy a token contract as an example: + +```bash +aztec-wallet deploy \ + --node-url $NODE_URL \ + --from accounts:my-wallet \ + --payment method=fpc-sponsored,fpc=contracts:SponsoredFPC \ + --alias token \ + TokenContract \ + --args accounts:my-wallet Token TOK 18 +``` + +This deploys the `TokenContract` with: +- `admin`: your wallet address +- `name`: Token +- `symbol`: TOK +- `decimals`: 18 + +You can check the transaction status on [Aztecscan](https://testnet.aztecscan.xyz). + +### Step 5: Interact with your contract + +Mint some tokens: + +```bash +aztec-wallet send mint_to_public \ + --node-url $NODE_URL \ + --from accounts:my-wallet \ + --payment method=fpc-sponsored,fpc=contracts:SponsoredFPC \ + --contract-address token \ + --args accounts:my-wallet 100 +``` + +Check your balance: + +```bash +aztec-wallet simulate balance_of_public \ + --node-url $NODE_URL \ + --from accounts:my-wallet \ + --contract-address token \ + --args accounts:my-wallet +``` + +This should print: + +``` +Simulation result: 100n +``` + +Move tokens to private state: + +```bash +aztec-wallet send transfer_to_private \ + --node-url $NODE_URL \ + --from accounts:my-wallet \ + --payment method=fpc-sponsored,fpc=contracts:SponsoredFPC \ + --contract-address token \ + --args accounts:my-wallet 25 +``` + +Check your private balance: + +```bash +aztec-wallet simulate balance_of_private \ + --node-url $NODE_URL \ + --from accounts:my-wallet \ + --contract-address token \ + --args accounts:my-wallet +``` + +This should print: + +``` +Simulation result: 25n +``` + +## Viewing transactions on the block explorer + +You can view your transactions, contracts, and account on the testnet block explorers: + +- [Aztecscan](https://testnet.aztecscan.xyz) +- [Aztec Explorer](https://aztecexplorer.xyz/?network=testnet) + +Search by transaction hash, contract address, or account address to see details and status. + +## Registering existing contracts + +To interact with a contract deployed by someone else, you need to register it in your local PXE first: + +```bash +aztec-wallet register-contract \ + --node-url $NODE_URL \ + --alias mycontract \ + <CONTRACT_ADDRESS> <ArtifactName> +``` + +For example, to register a `TokenContract` deployed by someone else: + +```bash +aztec-wallet register-contract \ + --node-url $NODE_URL \ + --alias external-token \ + 0x1234...abcd TokenContract +``` + +After registration, you can interact with it using `aztec-wallet send` and `aztec-wallet simulate` as shown above. + +## Paying fees without the Sponsored FPC + +The Sponsored FPC is convenient for getting started, but you can also pay fees directly by bridging Fee Juice from Ethereum Sepolia. See [Paying Fees](./docs/aztec-js/how_to_pay_fees.md#bridge-fee-juice-from-l1) for details on bridging and other fee payment methods. + +## Getting Fee Juice from the faucet + +If you want to pay fees directly instead of using the Sponsored FPC, you can request **Fee Juice** from the testnet faucet: + +- [Aztec Fee Juice Faucet](https://aztec-faucet.nethermind.io/) - dispenses testnet Fee Juice to your account + +:::note Fee Juice is not the AZTEC token +This faucet dispenses **Fee Juice**, the asset used to pay transaction fees (gas) on Aztec. Fee Juice lives on Aztec (L2) and is only used to pay fees. It is **not** the AZTEC token, which is a separate asset that lives on Ethereum (L1). This faucet does not dispense AZTEC tokens. +::: + +## Testnet information + +For complete testnet technical details including contract addresses and network configuration, see the [Networks page](/networks#testnet). + +## Next steps + +- Check out the [Tutorials](./docs/tutorials/contract_tutorials/counter_contract.md) for building more complex contracts +- Learn about [paying fees](./docs/aztec-js/how_to_pay_fees.md) with different methods +- Explore [Aztec Playground](https://play.aztec.network/) for an interactive development experience + +:::tip Need help? +If something does not work, see the [support guide](./support.md). It tells you when to ask in [Discord](https://discord.gg/aztec) or the [forum](https://forum.aztec.network), when to [open a GitHub issue](https://github.com/AztecProtocol/aztec-packages/issues/new?template=bug_report.yml), and how to disclose security issues responsibly. +::: diff --git a/docs/lines/v6/docs-developers/overview.md b/docs/lines/v6/docs-developers/overview.md new file mode 100644 index 00000000000..54b7ea41e59 --- /dev/null +++ b/docs/lines/v6/docs-developers/overview.md @@ -0,0 +1,97 @@ +--- +title: Aztec Overview +sidebar_position: 0 +tags: [protocol] +description: Overview of Aztec, the decentralized Ethereum L2 for unstoppable private apps, with private and public state and execution. +--- + + +This page outlines Aztec's fundamental technical concepts. It is recommended to read this before diving into building on Aztec. + +## What is Aztec? + +Aztec is a decentralized Layer 2 on Ethereum for building unstoppable private apps. It supports smart contracts with both private & public state and private & public execution: private functions run and are proven on the user's device, public functions run on the network, and you decide which is which. + +<Diagram name="aztec-overview" alt="Private side: the PXE in the user's wallet executes and proves private functions. Public side: decentralized sequencers order transactions into rollup blocks and publish block data to the data availability layer." /> + +## Getting started + +Learn about Aztec, what it is, how it works and how to get start writing smart contracts on Aztec with programmable privacy by watching this video course: + +<div style={{position: 'relative', paddingBottom: '56.25%', height: 0, overflow: 'hidden', maxWidth: '100%', marginBottom: '2rem'}}> + <iframe + style={{position: 'absolute', top: 0, left: 0, width: '100%', height: '100%'}} + src="https://www.youtube.com/embed/cQIPG_J1W9g" + title="Aztec Video Course" + frameBorder="0" + allowFullScreen> + </iframe> +</div> + +## High level view + +<Diagram name="transaction-lifecycle" alt="Transaction lifecycle: a user calls aztec.js, the PXE simulates and proves privately, an Aztec node sequences the transaction, the prover network proves it, and the rollup contract on Ethereum verifies it." /> + +1. A user interacts with Aztec through Aztec.js (like web3js or ethersjs) +2. Private functions are executed in the PXE, which is client-side +3. Proofs and tree updates are sent to the Public VM (running on an Aztec node) +4. Public functions are executed in the Public VM +5. The Public VM rolls up the transactions that include private and public state updates into blocks +6. The block data and proof of a correct state transition are submitted to Ethereum for verification + +## Private and public execution + +Private functions are executed client side, on user devices to maintain maximum privacy. Public functions are executed by a remote network of nodes, similar to other blockchains. These distinct execution environments create a directional execution flow for a single transaction--a transaction begins in the private context on the user's device then moves to the public network. This means that private functions executed by a transaction can enqueue public functions to be executed later in the transaction life cycle, but public functions cannot call private functions. + +### Private Execution Environment (PXE) + +Private functions are executed on the user's device in the Private Execution Environment (PXE, pronounced 'pixie'), then it generates proofs for onchain verification. It is a client-side library for execution and proof-generation of private operations. It holds keys, notes, and generates proofs. It is included in aztec.js, a TypeScript library, and can be run within Node or the browser. + +Note: It is easy for private functions to be written in a detrimentally unoptimized way, because many intuitions of regular program execution do not apply to proving. For more about writing performant private functions in Noir, see [this page](https://noir-lang.org/docs/guides/thinking_in_circuits) of the Noir documentation. + +### Aztec Virtual Machine (AVM) + +Public functions are executed by the Aztec Virtual Machine (AVM), which is conceptually similar to the Ethereum Virtual Machine (EVM). As such, writing efficient public functions follow the same intuition as gas-efficient solidity contracts. + +The PXE is unaware of the Public VM. And the Public VM is unaware of the PXE. They are completely separate execution environments. This means: + +- The PXE and the Public VM cannot directly communicate with each other +- Private transactions in the PXE are executed first, followed by public transactions + +## Private and public state + +Private state works with UTXOs, which are chunks of data that we call notes. To keep things private, notes are stored in an [append-only UTXO tree](./docs/foundational-topics/advanced/storage/indexed_merkle_tree.mdx), and a nullifier is created when notes are invalidated (aka deleted). Nullifiers are stored in their own [nullifier tree](./docs/foundational-topics/advanced/storage/indexed_merkle_tree.mdx). + +Public state works similarly to other chains like Ethereum, behaving like a public ledger. Public data is stored in a public data tree. + +<Diagram name="public-private-state" alt="Public state is a data tree whose leaves are updated in place. Private state is a UTXO model: spending a note emits a nullifier and creates new notes." /> + +Aztec [smart contract](./docs/aztec-nr/framework-description/contract_structure.md) developers should keep in mind that different data types are used when manipulating private or public state. Working with private state is creating commitments and nullifiers to state, whereas working with public state is directly updating state. + +## Accounts and keys + +### Account abstraction + +Every account in Aztec is a smart contract (account abstraction). This allows implementing different schemes for authorizing transactions, nonce management, and fee payments. + +Developers can write their own account contract to define the rules by which user transactions are authorized and paid for, as well as how user keys are managed. + +Learn more about account contracts [here](./docs/foundational-topics/accounts/index.md). + +### Key pairs + +Each account in Aztec is backed by 3 key pairs: + +- A **nullifier key pair** used for note nullifier computation +- A **incoming viewing key pair** used to encrypt a note for the recipient +- A **outgoing viewing key pair** used to encrypt a note for the sender + +As Aztec has native account abstraction, accounts do not automatically have a signing key pair to authenticate transactions. This is up to the account contract developer to implement. + +## Noir + +Noir is a zero-knowledge domain specific language used for writing smart contracts for the Aztec network. It is also possible to write circuits with Noir that can be verified on or offchain. For more in-depth docs into the features of Noir, go to the [Noir website](https://noir-lang.org/). + +:::tip Need help? +If something does not work, or you are not sure where to ask, see the [support guide](./support.md). It explains the right channel for questions, bug reports, feature requests, and security disclosures. +::: diff --git a/docs/lines/v6/docs-developers/support.md b/docs/lines/v6/docs-developers/support.md new file mode 100644 index 00000000000..f6c255e3a54 --- /dev/null +++ b/docs/lines/v6/docs-developers/support.md @@ -0,0 +1,130 @@ +--- +title: Support +sidebar_position: 5 +description: Where to ask questions, report bugs, request features, and disclose security issues for the Aztec stack. +references: ["labs/.github/ISSUE_TEMPLATE/bug_report.yml", "labs/.github/ISSUE_TEMPLATE/feature_request.yml", "labs/SECURITY.md", "labs/CONTRIBUTING.md"] +--- + +This page tells you where to go when something does not work, when you want to file a bug, when you have a feature idea, or when you have found a possible security issue. Pick the section that matches your situation. + +:::warning Security issues are different +If your issue involves loss of funds, key or seed disclosure, leakage of private notes, a way to forge or replay transactions, or anything you suspect could harm users, **do not open a public GitHub issue or post in Discord**. Use the [security disclosure process](#security-issues) instead. +::: + +## Quick decision tree + +| Your situation | Where to go | +| --- | --- | +| Possible security issue (funds, keys, privacy, exploit) | [Security disclosure](#security-issues) | +| You are not sure if the bug is real, or you cannot reproduce it yet | [Ask first: Forum or Discord](#ask-first-forum-and-discord) | +| You have a reproducible bug in `aztec-packages` (PXE, aztec.js, aztec-nr, local network, CLI, AVM, barretenberg, L1 contracts) | [File a bug on GitHub](#file-a-bug) | +| You have a Noir compiler or language bug | [Noir issues on `noir-lang/noir`](https://github.com/noir-lang/noir/issues) | +| You have a feature request or enhancement idea | [File a feature request](#feature-requests) | +| You are running a node, sequencer, or prover and hit an operator problem | [Operator support](#operator-and-node-issues) | +| You want to suggest a documentation change | [File a docs issue](#documentation-issues) | + +## Ask first: Forum and Discord + +If you are not yet sure whether your problem is a real bug or a configuration issue, start in the community channels. You can often get a faster answer there, and the team can help you build a minimal reproduction before you open a GitHub issue. + +- [Noir Discord](https://discord.com/invite/JtqzkdeQ6G): the developer-focused channel for syntax issues, compiler questions, and language-level help with Aztec.nr contracts. +- [Aztec Forum](https://forum.aztec.network): long-form Q&A, best for design discussions, complex bug reports, and conversations you want indexed and searchable. +- [Aztec Discord](https://discord.gg/aztec): the broader Aztec community space, and the live support channel for node operators (sequencers, provers, RPC nodes). + +Once you have a clear reproduction, the right next step is to file a GitHub issue using the form below. + +## File a bug + +Reproducible bugs in the Aztec stack belong on GitHub. Use the bug template, which automatically labels your issue and helps a maintainer triage it. + +[Open a new bug report](https://github.com/AztecProtocol/aztec-packages/issues/new?template=bug_report.yml) + +### What a high-quality bug report includes + +The template asks for these, and your issue will be triaged faster if you provide all of them. + +1. **Aztec version**, for example `0.85.0-alpha-testnet.2`. Use `aztec-up list` if you are not sure. +2. **What you were trying to do**, in one or two sentences. +3. **A minimal, runnable reproduction**. A code snippet that compiles, or a link to a public repo branch, is much more useful than prose. If your reproduction is large, please trim it before filing. +4. **Expected vs. actual behavior**. +5. **Environment**: operating system, Node.js version, and browser if relevant. +6. **Logs and errors**: the failing block, ideally with `LOG_LEVEL=debug` for the module that failed. +7. **What you already tried**: workarounds, version downgrades, related issues you read. + +### Where bugs in specific components go + +All of the components below live in [`AztecProtocol/aztec-packages`](https://github.com/AztecProtocol/aztec-packages), so use the bug template above and let triage attach the component label. + +- **PXE, wallet, CLI**: `aztec`, `aztec-wallet`, `aztec.js`, `bb.js`, local network. +- **Aztec.nr framework**: the `aztec-nr` smart contract framework. +- **Protocol circuits or protocol specs**: the rollup, kernels, and other circuits. +- **Barretenberg, AVM, L1 contracts**: the prover backend, the Aztec Virtual Machine, and the Ethereum-side rollup contracts. + +For the **Noir compiler** or the Noir language itself, file on [`noir-lang/noir`](https://github.com/noir-lang/noir/issues) instead. + +## Feature requests + +Use the feature request template for new functionality, enhancements to existing components, or proposed changes to the developer experience. + +[Open a feature request](https://github.com/AztecProtocol/aztec-packages/issues/new?template=feature_request.yml) + +Include: + +- The problem you are trying to solve. +- A concrete example or use case, if you have one. +- Why this is impactful: what is unblocked or made easier if it ships. + +## Documentation issues + +If something in the docs is wrong, outdated, or missing, please **[open an issue](https://github.com/AztecProtocol/aztec-packages/issues/new?template=bug_report.yml)** with the bug template and quote the URL and paragraph that needs fixing. Docs issues are routed to [`@AztecProtocol/devrel`](https://github.com/orgs/AztecProtocol/teams/devrel) so a docs maintainer can pick them up. + +This includes typos and small text issues: please file them as issues rather than opening single-line PRs. A maintainer can fix several at once, which is faster to review than a stream of one-line pull requests. + +For larger restructures or new pages, open an issue first so a maintainer can confirm the direction before you write the change. Every docs page in this site has an "Edit this page" link at the bottom that takes you to the right file in [`docs-developers/`](https://github.com/AztecProtocol/aztec-packages/tree/next/docs/docs-developers) once the direction is agreed. + +## Operator and node issues + +For real-time help, the [Aztec Discord](https://discord.gg/aztec) is the fastest way to reach other operators and the Aztec team. Use it to sanity-check a failure mode before filing, or to coordinate with the team during an incident. + +If you are running a node, sequencer, or prover and you hit a reproducible operational problem (sync failures, missed proposals, prover crashes, deployment errors), [open a bug report](https://github.com/AztecProtocol/aztec-packages/issues/new?template=bug_report.yml) and include the following alongside the standard bug-report fields: + +- **Network** (mainnet, testnet, devnet, or local). +- **Role** (sequencer, prover, RPC node). +- **Block height at failure** and approximate UTC timestamp. +- **Hardware**: CPU model, RAM, disk type and size. +- **Container runtime and image version**. +- **Configuration** (your `config.json` or environment block, with secrets redacted). +- **Logs**: the last few hundred lines around the failure, with sensitive keys redacted. + +Operator-specific guides live in the [Operate section](/operate/operators). + +## Security issues + +**Do not open a public GitHub issue for a suspected vulnerability.** Public disclosure can put users at risk before a fix is available. + +Use one of the following, in order of preference: + +1. **[Aztec Network Bug Bounty on Cantina](https://cantina.xyz/bounties/80e74370-10d8-4e52-8e4b-7294deb7c9ee)** if the issue is in scope of the bounty program. +2. **[GitHub Private Vulnerability Reporting (PVR)](https://github.com/AztecProtocol/aztec-packages/security/advisories/new)** for any other suspected vulnerability. Go to the "Security" tab of the repository and click "Report a vulnerability". +3. **Email `security@aztec.foundation`** if neither Cantina nor PVR is available to you. Send a brief impact summary first, without exploit details or reproduction steps, and wait for the team to confirm a secure channel before sharing them. + +If you believe a vulnerability is being actively exploited or has severe impact (loss of funds, key compromise, or broad user impact), mark the report as **CRITICAL** in the PVR or email subject. + +See the full [security policy](https://github.com/AztecProtocol/aztec-packages/blob/next/SECURITY.md) for more. + +## What happens after you file + +When you file a GitHub issue using one of the templates above, it is automatically tagged so the team can triage it. A maintainer will: + +1. Confirm the component the issue belongs to. +2. Set a priority based on impact. +3. Ask follow-up questions if the report is missing a reproduction or context. +4. Route the issue to the owning team. + +The fastest way to a fix is a small, runnable reproduction. If you can attach one, please do. + +## See also + +- [`CONTRIBUTING.md`](https://github.com/AztecProtocol/aztec-packages/blob/next/CONTRIBUTING.md) for contribution guidelines. +- [`SECURITY.md`](https://github.com/AztecProtocol/aztec-packages/blob/next/SECURITY.md) for the full security disclosure policy. +- The [Aztec project board](https://github.com/orgs/AztecProtocol/projects/22) for in-flight work. diff --git a/docs/lines/v6/docs-operate/operators/_node_hardware.mdx b/docs/lines/v6/docs-operate/operators/_node_hardware.mdx new file mode 100644 index 00000000000..4f54db2decf --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/_node_hardware.mdx @@ -0,0 +1,6 @@ +| Resource | Spec | +|---|---| +| CPU | 8 cores / 16 vCPU, **Skylake or newer** | +| RAM | 8 GB | +| Storage | 1 TB SSD (NVMe recommended) | +| Network | 25 Mbps | diff --git a/docs/lines/v6/docs-operate/operators/concepts/claiming-rewards.md b/docs/lines/v6/docs-operate/operators/concepts/claiming-rewards.md new file mode 100644 index 00000000000..e1818c07756 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/concepts/claiming-rewards.md @@ -0,0 +1,103 @@ +--- +id: claiming-rewards +title: Claiming rewards +description: When rewards show up, how much you earn, and how to claim across many sequencers and rollups from the staking dashboard. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/cli/src/cmds/validator_keys/*"] +--- + +This page explains when Aztec rewards become claimable and how to claim them. The [staking dashboard](https://stake.aztec.network) is the primary path: it finds your rewards across every rollup and bundles the claims into a single wallet flow, which is what makes claiming practical once you run more than one sequencer or operate at provider scale. The `cast` recipes for a single attester live in [Advanced operations](/operate/operators/sequencer-management/claiming-rewards). + +## TL;DR + +- Rewards accrue **on the Rollup contract** under your coinbase address. Nothing reaches your wallet until they are claimed. +- You earn **350 AZTEC per checkpoint you propose** (70% of the 500 AZTEC checkpoint reward). +- After a rollup upgrade, rewards earned before the upgrade stay on the old Rollup contract. The dashboard claims across both. +- Claim from the [staking dashboard](https://stake.aztec.network): **self-stakers** use the per-sequencer Claim in the stake breakdown on the My position page; **providers** use the Operator Commission tab. +- Slashing reduces your stake, not your accrued rewards. + +## When rewards show up + +Rewards are credited to your coinbase **after the epoch containing your proposed checkpoint has been proven**, not at proposal time. Proving happens within the proof submission window (typically a few epochs after the block). + +Practical timing: most rewards land within 1 to 2 hours of the corresponding block. During gas-spike periods or proving incidents, the lag can extend. If an epoch is pruned (provers missed the window), the rewards for that epoch do not credit at all. + +## How much you earn + +You earn **350 AZTEC** to your coinbase for each checkpoint you propose. This can change by governance proposal. + +## Claiming from the staking dashboard + +The dashboard reads pending rewards across every rollup the Registry has ever made canonical, so it surfaces rewards on both the current and previous Rollup contracts in one place, and routes the claim through a single transaction cart. Connect the wallet that holds your stake position: for a direct stake that is the **withdrawer** address, and for a stake from a token vault it is the vault **beneficiary** wallet. The dashboard then shows the path that applies to you. + +### Self-stakers: claiming your own sequencer rewards + +If you run your own sequencers (whether one or many), open your position on [stake.aztec.network/my-position](https://stake.aztec.network/my-position). The **Stake breakdown** section lists each sequencer you run, with a **Claim** button on each row. + +![Stake breakdown on the staking dashboard position page, listing each self-staked sequencer with a per-row Claim button](/img/claim_self_stake_breakdown.jpg) + +That per-sequencer Claim opens a dialog with a **Coinbase address** field: you enter the coinbase, and the dashboard finds the pending rewards for it across every rollup and adds one claim to the transaction cart per rollup. + +![Claim Self-Stake Rewards dialog, with the coinbase address entered and a claim available per rollup](/img/claim_self_stake_dialog.jpg) + +Which address you enter depends on how you configured your node. Rewards always accrue on the Rollup contract under the **coinbase address**, so the coinbase is what you claim against. + +#### Scenario 1: coinbase is a separate address + +If you set the coinbase to an address you control when you generated your keystore (`aztec validator-keys new --coinbase <address>`) or manually added a coinbase address to your keystore, rewards accrue to that address. In the per-sequencer Claim dialog, enter that **coinbase address**. The claimed AZTEC lands at the coinbase, which is already an address you hold the keys to. + +To make all sequencer rewards claimable in one pass, add your coinbase addresses to the dashboard's tracking list (see [Tracking addresses for the Claimable Rewards total](#tracking-addresses-for-the-claimable-rewards-total) below). The **Claimable Rewards** card at the top of the page then sums the pending rewards across the coinbases you have added and every rollup. When that total is above zero, expanding the card reveals **Claim All Rewards**, which batches the claims into a single flow rather than one Claim per sequencer. + +#### Scenario 2: coinbase is the attester address + +If you did not set a coinbase when you generated your keystore, the coinbase **defaults to the attester address**. Rewards accrue to the attester address itself. + +Two consequences: + +- In the per-sequencer Claim dialog, enter the **attester address** in the Coinbase address field (the dialog shows the attester address, so you can copy it from there). +- The claimed AZTEC lands at the attester address. To move it afterward you need that address's key, which lives in your node's keystore. + +The **Claimable Rewards** card does not auto-detect self-stake rewards earned to a coinbase that equals the attester address. So when your coinbase is the attester, the card's **Self-Stake Rewards** line can read zero even while each sequencer has earned, until you add the attester address to the tracking list (see below). The per-sequencer Claim still works regardless. + +#### Tracking addresses for the Claimable Rewards total + +The **Claimable Rewards** card discovers your delegation rewards automatically, but for **self-stake** rewards it only counts the coinbase addresses you have told the dashboard to track. The tracking list is stored in your browser per connected wallet, so it does not carry across browsers or devices. To populate it: + +1. On [stake.aztec.network/my-position](https://stake.aztec.network/my-position), expand the **Claimable Rewards** card at the top of the page. +2. Click **Manage Addresses**. + + ![Expanded Claimable Rewards card showing the Claim All Rewards and Manage Addresses buttons](/img/claim_claimable_rewards_card.png) + +3. On the **Coinbase** tab, paste a coinbase address (in scenario 2, that is your attester address) and click **Add**. Repeat for each address. + + ![Manage Reward Addresses dialog open on the Coinbase Addresses tab, with the address input and Add button](/img/claim_manage_addresses.png) + +The card then sums pending rewards across the addresses you added and every rollup. Removing an address only stops the dashboard tracking it; it does not affect your rewards or your ability to claim per sequencer. + +### Providers: claiming commission across delegations + +If you operate as a **staking provider**, rewards from your delegators' sequencers route into a **Splits contract** (one per delegation) that divides rewards between you and each delegator. Your **commission** routes to the `providerRewardsRecipient` address you set on the provider record; the remainder is claimable by each delegator. + +Open the **Operator Commission** tab at [stake.aztec.network/operator](https://stake.aztec.network/operator). The tab is visible when your connected wallet is the admin or rewards-recipient for at least one provider. It surfaces every split that pays you and bundles the full claim chain (claim the rollup rewards onto the split, distribute the split, withdraw your share) into the transaction cart, across all of your delegations and rollups at once. + +![Operator Commission tab on the staking dashboard, listing the splits that pay a provider with per-split pending commission and batch actions](/img/claim_operator_commission.jpg) + +This tab claims your **commission** on delegated stake. Rewards from any sequencers you self-stake are claimed on your position page, described above. + +## Prover rewards + +This page covers sequencer and provider rewards. Provers earn separately, per epoch, and claim with a different contract function. See [Claiming prover rewards](/operate/operators/prover/claiming-rewards). + +## Can my rewards disappear after a slash? + +Slashing reduces your **stake**, not your **pending rewards**. Rewards already accrued to your coinbase are unaffected by slashing events. You can still claim them. + +If you have been ejected (stake fell below the local 190,000 AZTEC threshold), you stop *earning new* rewards on subsequent epochs, but the rewards you have already accrued are still claimable. + +## See also + +- [Detailed claim guide](/operate/operators/sequencer-management/claiming-rewards): `cast` recipes including keystore and hardware-wallet variants, and gas-limit troubleshooting +- [Identity model](./identity-model): what the coinbase, publisher, and withdrawer addresses each do +- [Slashing](./slashing): what happens to your stake (not your rewards) when offenses occur +- [Claiming prover rewards](/operate/operators/prover/claiming-rewards): the separate per-epoch prover claim flow +- [Operator tooling](/operate/operators/tooling): the staking dashboard and other tools operators rely on diff --git a/docs/lines/v6/docs-operate/operators/concepts/hardware.md b/docs/lines/v6/docs-operate/operators/concepts/hardware.md new file mode 100644 index 00000000000..a430970a52c --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/concepts/hardware.md @@ -0,0 +1,89 @@ +--- +id: hardware +title: Hardware spec +description: What your machine actually needs to run an Aztec full node, sequencer, provider, or prover, including the CPU instruction-set floor and the extra disk for co-hosting an L1 node. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/prover-client/src/proving_broker/config.ts"] +--- + +import NodeHardware from "../_node_hardware.mdx"; + +A single-screen view of what each role needs. + +## Sequencer and full node (single machine) + +<NodeHardware /> + +These numbers are the same on testnet and mainnet: the node binary and its workload do not change with the network. What changes by network is the L1 node you connect to or co-host (see [Co-hosting an L1 node](#co-hosting-an-l1-node) below). + +### One node, many attesters: load stays about the same + +The spec above is for one node. Running more attester identities on the same node does **not** multiply the load. Stake scales in units of 200,000 AZTEC, and each unit is a separate attester identity, but the node's heavy work is validating each block proposal, which it does once per proposal regardless of how many of your attesters are on the committee. When several of your attesters are selected together, the node validates the proposal once and then just signs an attestation for each, which is cheap. So one node running 1 attester and one node running 100 attesters need roughly the same CPU and RAM. + +What does grow with the number of nodes (not attesters) is running more than one machine, for example for high availability. Size each node to the spec above. + +### Disk and IOPS + +1 TB is enough for the Aztec node itself. A normal SSD works; NVMe is recommended because random I/O performance matters as much as capacity, and cheap drives without consistent random-write performance can struggle under sustained load. SATA SSDs typically fall short. As a target, aim for **50,000+ random read/write IOPS**. + +You need more than 1 TB only if you co-host an L1 node on the same machine, covered next. + +### Co-hosting an L1 node + +If you run your own Ethereum execution and consensus clients on the **same machine** as the Aztec node (a common way to avoid hosted-RPC cost and rate limits), add the L1 node's disk on top of the Aztec node's 1 TB. The L1 footprint depends on the network: + +| Network | L1 node disk (execution + consensus + blobs, approximate) | Total with Aztec node | +|---|---|---| +| Testnet (Sepolia) | around 1.3 TB | ~2 TB | +| Mainnet (Ethereum) | around 2.5 to 3.5 TB, depending on the client | ~4 TB | + +These L1 figures are external to Aztec and drift over time as the chains grow; check current Ethereum client guidance when you provision. If you point at a hosted or separate L1 endpoint instead, the Aztec node stays at 1 TB. See [L1 RPC requirements](./l1-rpc) for the endpoints a node needs. + +### Network + +25 Mbps is the floor. Sustained P2P load spikes above the average at epoch boundaries, so provision headroom; symmetric 100 Mbps is comfortable for production. + +## Staking provider + +Same per-node spec as the solo sequencer. As above, the number of delegated attester positions you run does not change the per-node load: 10, 100, or 1,000 attesters on one node is roughly the same work, because the node validates each proposal once and signs cheaply per attester. + +What multiplies the spec is the number of **nodes**, not attesters: + +- Each delegated stake position you operate is a separate attester identity, but they can share a node. +- For high availability, run one publisher plus one or more redundant nodes with a coordinated shared keystore. +- A provider running across 2 HA-paired nodes is operating roughly 2x the spec above because there are 2 machines, not because of the attester count. + +Plan for `(number of nodes) x per-node-spec`. + +## Prover + +The prover has a different architecture: one **prover node** plus one **broker** plus N **prover agents** (the workers that do the heavy proof generation). + +| Component | Spec per instance | +|---|---| +| Prover node | 16 cores / 32 vCPU, 16 GB RAM, 1 TB NVMe, 25 Mbps | +| Prover broker | 8 cores / 16 vCPU, 16 GB RAM, 10 GB SSD | +| Prover agent (each) | 32 cores / 64 vCPU, 128 GB RAM, 10 GB SSD | + +Agents carry the Skylake-or-newer CPU floor too, and lean on it hardest. They do the heavy lifting, and their hardware scales roughly linearly with how many you run (set with `PROVER_AGENT_COUNT`): + +| Agents | Cores | RAM | +|---|---|---| +| 1 | 32 | 128 GB | +| 2 | 64 | 256 GB | +| 3 | 96 | 384 GB | +| 4 | 128 | 512 GB | + +### How many agents do you need + +A full prover for the heaviest sustained workload runs on the order of 200 agents. Real network load is usually well below that. + +If you are provisioning, start with one broker and 2 to 4 agents, then watch the proving queue depth and scale agents up if it grows. The broker exports the queue size as the `aztec.proving_queue.size` OpenTelemetry metric (number of jobs waiting); see the [Job Queue metrics](/operate/operators/monitoring/metrics-reference#job-queue) for the full set. + +## See also + +- [Prerequisites](/operate/operators/prerequisites): the full operator-prerequisites checklist (OS, Docker, port-forwarding) +- [L1 RPC requirements](./l1-rpc): the execution, consensus, and debug endpoints a node needs +- [Full node setup](/operate/operators/full-node/setup): concrete Docker Compose with these specs +- [Prover overview](/operate/operators/prover/overview): the broker and agent architecture in detail +- [Topology and infrastructure](/operate/operators/setup/high_availability_sequencers): HA patterns for providers diff --git a/docs/lines/v6/docs-operate/operators/concepts/identity-model.md b/docs/lines/v6/docs-operate/operators/concepts/identity-model.md new file mode 100644 index 00000000000..f6f365fb0a5 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/concepts/identity-model.md @@ -0,0 +1,79 @@ +--- +id: identity-model +title: Identity model +description: Which keys earn rewards, which are slashable, which can be rotated. The role each address plays in operating an Aztec sequencer or prover. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/node-keystore/src/*"] +--- + +A single sequencer is identified by **several different addresses**, each with a different job. Confusing them can result in misrouted rewards or an inability to initiate withdrawal. + +## The five-second mental model + +- The **attester** is the validator's identity. It signs, it gets slashed. +- The **publisher** pays L1 gas to do the attester's work. +- The **coinbase** receives the block rewards. +- The **withdrawer** is the only one who can move your stake. +- The **fee recipient** is an unused placeholder in the protocol today. Transaction fees do not route to it. + +Providers add two more (provider admin, provider rewards recipient) for managing the provider entry and receiving commission; see below. + +## Address roles in detail + +| Role | Type | Purpose | Slashable? | Rotatable? | +|---|---|---|---|---| +| **Attester** | L1 address plus a BLS key pair | Signs attestations and proposals. Identifies the validator onchain. | Yes (offenses listed in [Slashing](./slashing)) | No. The attester address and its BLS key are bound permanently the first time they register. If the attester exits or is ejected, that address cannot return to sequencer duties; generate a new attester identity and stake again. | +| **Publisher** | L1 EOA | Pays L1 gas to submit checkpoints and proposals. Holds ETH only. | No directly. A single missed proposal is not slashable, but a publisher that stays out of ETH long enough for the attester to miss almost all its duties over a full epoch hits the inactivity threshold, which is slashable (see [Slashing](./slashing)). | Yes (edit publisher config and restart) | +| **Coinbase** | L1 EOA, or a Split contract (provider default flow) | Receives sequencer block rewards. Configured per attester in the node's keystore. | No | Yes (edit keystore and restart) | +| **Fee recipient** | L2 (Aztec) address | An unused placeholder in the protocol today. Transaction fees do not route here; they go to the coinbase, see below. | No | Yes | +| **Withdrawer** | L1 EOA | The only address authorized to initiate stake withdrawal. At withdrawal time the withdrawer can send unstaked AZTEC to **any** destination (it does not have to be the withdrawer itself). | No | No (set at registration) | +| **Provider admin** | L1 EOA | Provider-only. Manages the provider entry on the Staking Registry contract. | No | Yes | +| **Provider rewards recipient** | L1 EOA | Provider-only. Receives the operator's commission share when delegators claim. | No | Yes | + +## Which address gets which kind of reward + +- **Block rewards** (per checkpoint you propose) go to your **coinbase**. +- **Transaction fees**: part of each fee is burned, and the sequencer's share is credited to your **coinbase**. The **fee recipient** header field is not used for this and does not receive fees today. +- **Stake** is held against the **attester address**; only the **withdrawer** can move it. When stake leaves, it does not return to the attester: a voluntary withdrawal goes to the recipient the withdrawer chooses, and an ejection (stake slashed below the threshold) creates an exit that returns the stake to the withdrawer. +- **Provider commission** routes through the splits contract to the **provider rewards recipient**. + +Each of these is configured in a different place (keystore for coinbase, the registration deposit for withdrawer, splits contract for provider). A misconfigured coinbase routes rewards to the wrong address until you edit `key1.json` and reload the keystore; see [Claiming rewards](./claiming-rewards). + +:::warning Provider coinbase is the Split contract (default flow) +For provider-run attesters, the default flow sets each delegated attester's coinbase to that delegation's **Split contract**, which enforces the commission split onchain; a coinbase pointing anywhere else routes the delegation's rewards around the Split, so it must be a deliberate choice (see below). Each Split's address is on the [staking dashboard](https://stake.aztec.network/providers): open your provider page and expand the **Sequencer Registered** dropdown, which maps each attester to its Split. The per-delegation update procedure is in [Configure environment](/operate/operators/provider/configure-environment#after-delegation-set-coinbase-to-the-split-contract). +::: + +:::info Alternative: EOA coinbase with the payout script +The Split flow is the default, not the only option. A provider can set the coinbase to an address they control and run the [payout script](https://github.com/AztecProtocol/aztec-staking-payout) to calculate and distribute delegator rewards at a published commission and cadence. See [the configuration step](/operate/operators/provider/configure-environment#alternative-eoa-coinbase-with-the-payout-script) and the [forum announcement](https://forum.aztec.network/t/operator-commission-adjustments/8588). +::: + +## Where each address lives and how to check it + +Your node's keystore (`key1.json`) is the source of truth for what **you** configured. Each validator entry holds: + +- `attester.eth` and `attester.bls`: the attester identity (address and signing key) +- `publisher`: the key(s) that pay L1 gas +- `coinbase`: where block rewards accrue +- `feeRecipient`: currently unused, set to zeros + +For the **onchain side**, check dashboards first: + +- **[Aztec staking dashboard](https://stake.aztec.network/my-position)**: your registered sequencers, stake, and pending rewards. The dashboard does not display your coinbase (it cannot read it from the chain). To claim self-stake rewards: open **Stake breakdown**, click **Claim** next to a sequencer, and enter the coinbase in the **Coinbase Address** field of the **Claim Self-Stake Rewards** window. If you never set `--coinbase` during setup, the rewards flow to that sequencer's attester address, so paste the attester address there. Add the claim transaction to the batch and repeat for each sequencer. Providers see each delegated attester's coinbase (the Split contract) in the **Sequencer Registered** dropdown on their provider page. +- **[Dashtec](https://dashtec.xyz/)** or **[Aztec Vision](https://aztec.vision/)**: paste any attester address to see its status, queue position, and attestation activity. + +The coinbase has no onchain getter: the node sends it with each proposal, so the keystore is where to look (and where to fix it). + +To query the contracts from the CLI, use the `check-attester.sh` script from the [verify page](/operate/operators/solo-sequencer/verify#3-p2p-port-reachable): it reports each attester's status, effective balance, exit state, and withdrawer, with a health verdict per attester. + +## Frequently asked questions + +- **"I sent rewards to my coinbase but the staking dashboard still shows them pending."** Rewards accrue on the Rollup contract; the dashboard shows accrued, not delivered. Claiming moves them to the coinbase wallet. See [Claiming rewards](./claiming-rewards). +- **"I lost access to my coinbase, can I change it?"** Yes. Edit the keystore on your node and restart. Future rewards route to the new coinbase. Rewards already accrued on the old coinbase are claimed by whoever still controls that address. +- **"I changed the coinbase in my keystore but rewards still accrue to the old address."** The node reads the keystore at startup. Edits take effect only after you reload the keystore (admin API call or node restart). Until then, proposals still carry the old coinbase. +- **"Can someone else claim my rewards?"** Anyone can pay the gas to call `claimSequencerRewards(coinbase)`, but the AZTEC goes to the coinbase. + +## See also + +- [Keystore management](/operate/operators/keystore): how the keys are generated and stored locally +- [Claiming rewards](./claiming-rewards): claiming block rewards from the Rollup contract +- [Slashing](./slashing): which conditions slash the attester diff --git a/docs/lines/v6/docs-operate/operators/concepts/l1-rpc.md b/docs/lines/v6/docs-operate/operators/concepts/l1-rpc.md new file mode 100644 index 00000000000..eed2e9baaa0 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/concepts/l1-rpc.md @@ -0,0 +1,112 @@ +--- +id: l1-rpc +title: L1 RPC requirements +description: What kind of Ethereum L1 endpoint an Aztec node needs, why a blob-serving consensus client is required, how much RPC load a node generates, and how to avoid the most common L1-side failures. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/ethereum/src/l1_reader.ts", "labs/yarn-project/archiver/src/config.ts", "labs/yarn-project/blob-client/src/client/config.ts", "labs/yarn-project/foundation/src/config/env_var.ts"] +--- + +Every Aztec node needs L1 access through **three endpoints**, all of which can point at the same Ethereum node: + +| Endpoint | Env var | What it does for Aztec | +|---|---|---| +| **Execution RPC** | `ETHEREUM_HOSTS` | Reads Rollup-contract state, submits proposals and attestations, queries gas prices. | +| **Consensus (beacon) RPC** | `L1_CONSENSUS_HOST_URLS` | Serves blob data (the transaction data Aztec posts to Ethereum as blobs). Must serve blob sidecars, see below. | +| **Debug/trace RPC** | `ETHEREUM_DEBUG_HOSTS` | Extracts `propose()` calldata from L1 transactions via `debug_traceTransaction`. Many hosted RPCs do not offer trace methods. | + +The execution and debug endpoints are often the same URL when you self-host, because a full Geth, Nethermind, Besu, or Erigon node serves both. Hosted RPCs frequently separate or omit the trace methods, which is the second most common reason a hosted endpoint does not work (the first is the blob requirement below). + +## The blob-serving consensus client + +Your consensus client must serve **blob sidecars**. After Ethereum's Fusaka upgrade (PeerDAS), a regular beacon node no longer serves the blob sidecar endpoint at all: blobs are distributed as erasure-coded data columns, and only a node configured to hold the full set can answer a blob request. Aztec posts its transaction data as blobs and reads it back through this endpoint, so a node pointed at a non-serving beacon falls behind and cannot sync. + +Two configurations serve blobs: + +- **Supernode**: holds the full set of data columns. Always serves blob sidecars. +- **Semi-supernode**: holds enough columns to reconstruct blobs. Sufficient for an Aztec node and lighter on bandwidth and storage than a full supernode. + +Run one of these. For example, you can run **Lighthouse v8.0.0 or later** with `--super-node` or `--semi-supernode`, or **Prysm v7.10 or later** with `--super-node` or `--semi-supernode`. These are the versions that introduced the flags. + +:::warning Symptom of a non-serving beacon +On startup the node logs a blob-source summary. A line like `consensusSuperNodes=0 ... consensus client(s) ignored because they are not running in supernode or semi-supernode mode` means your beacon is reachable but does not serve blobs. The node will not sync from it. Reconfigure the beacon with one of the flags above. +::: + +If you cannot run a blob-serving beacon yourself, the node can fall back to a blob archive or a shared blob store; see [Blob retrieval](/operate/operators/setup/blob_storage) and [Syncing best practices](/operate/operators/setup/syncing_best_practices). + +## Debug traces + +The node uses `debug_traceTransaction` to recover proposal data in some cases (see below), so a trace-capable execution endpoint that retains about the **last 12 hours (roughly 3,600 blocks)** of history is required. + +`ETHEREUM_DEBUG_HOSTS` is optional. When it is unset, the node uses your `ETHEREUM_HOSTS` execution RPC for trace calls, so a self-hosted node that already serves `debug` methods needs nothing extra. Set `ETHEREUM_DEBUG_HOSTS` separately only when your execution RPC does not serve trace methods and you have a different endpoint that does (for example, a hosted execution RPC paired with a separate archive provider). + +The node checks at startup that whichever endpoint it traces with serves `debug_traceTransaction` or `trace_transaction`, and refuses to start if it serves neither. You can set `ETHEREUM_ALLOW_NO_DEBUG_HOSTS=true` to start without one, but the node then stops syncing at the first checkpoint it cannot read without a trace. + +The node normally reads a proposal's data straight from its L1 transaction. When a proposal's transaction has a non-standard structure, the node falls back to `debug_traceTransaction` to recover the data. Without a debug-capable RPC that holds recent history, the node can fail to follow the chain. This is also why a node syncing from a snapshot needs trace history back to that snapshot. + +Whether you need to change anything depends on the client and its sync mode. A Reth full node and any archive node serve this window with no extra configuration; other clients prune historical state too aggressively by default and need the history window set explicitly: + +| Client | Configuration for ~3,600 blocks of trace history | +|---|---| +| **Geth** | Full and snap sync keep only 128 blocks and cannot be configured higher without an archive node. Use an archive node or a hosted RPC instead. | +| **Nethermind** | Set `Pruning.FullPruningTrigger=StateDbSize` and `Pruning.FullPruningThresholdMb` to at least `256000` (256 GB). | +| **Besu** | `--bonsai-historical-block-limit=3600` (default is 512). | +| **Reth** | Full node keeps 10,064 blocks, no action needed. On a pruned node, set prune `distance = 3600` for `sender_recovery`, `transaction_lookup`, `receipts`, `account_history`, `storage_history`, and `bodies`. | +| **Erigon** | `--prune.distance 3600 --prune.distance.blocks 3600`. A minimal node cannot be configured for this; use another client or a hosted RPC. | + +If you use a hosted provider, Infura, QuickNode, and Alchemy all support `debug_traceTransaction`, though it may sit behind a higher paid tier. + +## How much RPC load a node generates + +Aztec polls L1 continuously, which is why a free hosted RPC tier usually rate-limits a node off the chain within minutes. + +The baseline is the archiver poll loop. By default it calls `eth_blockNumber` every **500 ms** (`ARCHIVER_POLLING_INTERVAL_MS`), backed by a viem cache refreshed every **1000 ms** (`ARCHIVER_VIEM_POLLING_INTERVAL_MS`). That floor is roughly: + +- **~2 calls per second**, about **7,200 per hour** and **170,000 per day**, just to track the chain tip, per node. + +On top of that floor, each new L1 block triggers heavier reads: log queries for rollup events, a `debug_traceTransaction` on each `propose()` to recover calldata, an `eth_getBlockByHash`, and one or more beacon blob-sidecar fetches. The execution-and-debug load scales with L1 block rate (one pass per ~12-second L1 block), not with how many sequencers you run, because one node archives the chain once regardless of how many attester identities it holds. The publisher side scales with how often one of your attesters is selected to propose: each proposal is an L1 transaction your publisher sends. Attestations are not separate L1 transactions; they are gossiped over P2P and bundled by the proposer into the proposal calldata, so they cost your publisher no L1 gas. + +For the per-method breakdown by node component, see the [Ethereum RPC call reference](/operate/operators/reference/ethereum_rpc_reference). + +## Self-hosted vs hosted RPC + +Most production operators self-host their L1 endpoints: + +- A blob-serving beacon plus trace methods is hard to find on hosted tiers, and the tiers that offer both are priced for sustained load. +- Free and low tiers rate-limit a node off the chain, and falling behind the tip is a slashable inactivity condition. +- Self-hosting once gives predictable cost and unlimited polling. + +The minimum self-hosted setup is one execution client (Geth, Nethermind, Besu, or Erigon) plus one consensus client (Lighthouse or Prysm) running with a blob-serving flag. Both can share the machine with the Aztec node if you have the disk: Ethereum state plus beacon data add several hundred GB on top of the Aztec footprint. See [Hardware](./hardware) for the disk-growth caveat. + +For installers and helper scripts that set up an execution plus consensus client with the right flags, see [Operator tooling](/operate/operators/tooling). + +If you do use a hosted provider, confirm it offers all three: a blob-serving beacon API (`/eth/v1/beacon/blob_sidecars/{block_id}`), execution RPC, and `debug`/`trace` method support. + +Server-side filter methods (`eth_newFilter` / `eth_getFilterChanges`) are **not** required. As of v5.2.0 the node's L1 event watchers poll `eth_getLogs` over a bounded block range instead of installing server-side filters. Before this, a load-balanced RPC endpoint could route filter polls to a backend that had never seen the filter, producing request storms of 30 to 40 req/s and silently missing slashing and governance events. If you saw that on an earlier version behind a load balancer, upgrading is the fix. + +## Tuning polling intervals + +The defaults suit a self-hosted RPC. On a rate-limited hosted RPC, raise the archiver intervals to cut call volume: + +``` +ARCHIVER_POLLING_INTERVAL_MS=1000 +ARCHIVER_VIEM_POLLING_INTERVAL_MS=2000 +``` + +Raising the interval trades tip-detection latency for fewer calls. The maximum observed delay for a new block is `ARCHIVER_VIEM_POLLING_INTERVAL_MS + ARCHIVER_POLLING_INTERVAL_MS`, so the values above push worst-case detection to about 3 seconds, still within slot timing. If you are seeing 429s, the more reliable fix is to self-host. + +## The most common L1-side errors + +| Error / symptom | Likely cause | +|---|---| +| Blob-source summary shows `consensusSuperNodes=0` | Beacon client is not serving blob sidecars; restart it with a supernode or semi-supernode flag. | +| Archiver stuck, blob fetch failures | Same as above, or no blob archive fallback configured. | +| Archiver fails to start, complains about debug/trace | The endpoint in `ETHEREUM_DEBUG_HOSTS` (or `ETHEREUM_HOSTS` if unset) does not expose `debug`/`trace` methods. Use a node that does, or a hosted tier that offers them. | +| `429 Too Many Requests` | RPC rate-limited; raise `ARCHIVER_POLLING_INTERVAL_MS` or self-host. | +| `ECONNREFUSED 127.0.0.1:8545` at startup | The L1 RPC env var points at the container's own loopback, not your L1 node. See [Sequencer troubleshooting](./sequencer-troubleshooting). | + +## See also + +- [Hardware](./hardware): the consensus-client disk-growth caveat +- [Blob retrieval](/operate/operators/setup/blob_storage): S3, GCS, and R2 blob-store backends and archive fallbacks +- [Ethereum RPC call reference](/operate/operators/reference/ethereum_rpc_reference): per-component RPC method breakdown +- [Operator tooling](/operate/operators/tooling): L1-RPC setup scripts and installers diff --git a/docs/lines/v6/docs-operate/operators/concepts/monitoring.md b/docs/lines/v6/docs-operate/operators/concepts/monitoring.md new file mode 100644 index 00000000000..1d105be63cc --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/concepts/monitoring.md @@ -0,0 +1,137 @@ +--- +id: monitoring +title: Monitoring and metrics +description: The handful of metrics that actually matter for keeping your sequencer alive, plus what to alert on and where the dashboards live. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/telemetry-client/src/*", "docs/static/scripts/aztec-monitoring.sh"] +--- + +The Aztec node exposes over 250 Prometheus metrics. Most operators only need to watch a small handful to keep their sequencer healthy. + +## TL;DR + +- Alert on **L2 block height not advancing** for 15 minutes. +- Alert on **publisher ETH running low** (for example below 0.1 ETH). A dry publisher misses proposals. A single miss is not slashable, but missing almost all duties over a full epoch (about 38 minutes on mainnet) hits the inactivity threshold, which is slashable. Leave headroom: a single proposal during an L1 gas spike can burn through a thin balance before you can act. +- Alert on **peer count below 5**. Network reachability problem. +- For everything else, the community dashboards (dashtec.xyz, aztec.vision, slashveto.me) cover you. + +## The metrics that matter + +Aztec emits OpenTelemetry metrics, which Prometheus scrapes as snake-cased names (`aztec.archiver.block_height` becomes `aztec_archiver_block_height` on the wire). + +| Prometheus metric | What it tells you | Alert if | +|---|---|---| +| `aztec_archiver_block_height` | Your local view of the L2 chain tip | No advance for 15 minutes | +| `aztec_archiver_l1_block_height` | Your local view of L1 progression | No advance for 5 minutes (likely your L1 RPC is down) | +| `aztec_peer_manager_peer_count_peers` | P2P connectivity to other nodes | Drops below 5 | +| `aztec_l1_publisher_balance_eth` | ETH in your publisher account for paying L1 gas, read every minute | Drops below 0.1 ETH | +| `aztec_l1_publisher_sends_affordable` | How many proposals your publisher can still pay for at current L1 fees | Below 1 (alert right away) | +| `aztec_mempool_tx_count` | Transactions waiting to be included | Sustained growth or sudden spike | + +Plus standard system metrics (`process_cpu_utilization`, `process_memory_usage`) for the underlying host. + +To confirm the sequencer is producing, watch `aztec_sequencer_block_count` (see [Metrics reference](/operate/operators/monitoring/metrics-reference) for the full list and the alert rules). + +## What every operator should alert on + +In priority order: + +1. **No L2 blocks processed in the last 15 minutes** (critical). Your node is stuck or the network has stalled. Either way, you need to know immediately. +2. **Publisher balance below 0.1 ETH, or no publisher able to afford a proposal** (critical). When the publisher runs dry, you stop being able to publish proposals. Stay dry for a full epoch and the missed duties hit the inactivity threshold, which is slashable. Keep enough headroom that an L1 gas spike cannot drain the balance in a single proposal. `aztec_l1_publisher_sends_affordable` below 1 means no publisher can pay for a proposal at current fees; see [Metrics reference](/operate/operators/monitoring/metrics-reference#l1-publisher-eth-balance) for alert rules. +3. **Peer count below 5** (warning). Network reachability problem. Check port forwarding and firewall. +4. **L1 block height stalled for 5+ minutes** (warning). Your L1 RPC is degraded. See [L1 RPC](./l1-rpc) for common causes. +5. **CPU sustained above 70% of cores** (warning). May indicate the node is struggling to keep up; check disk IOPS and RAM headroom. + +## Set up monitoring with the installer + +The fastest way to a complete stack is the maintained installer script. It sets up an OpenTelemetry collector next to your node and, on a monitoring machine, a Grafana dashboard, Prometheus, and Alertmanager with Telegram alerts. Everything runs on infrastructure you control. It is optional, and adaptable or replaceable with your own monitoring; nothing here is required to operate a node. + +The script is one self-contained file: the dashboard, alert rules, and Alertmanager template are embedded in it. It targets Aztec `v5.0.0` and works with sequencer nodes run via Docker Compose. + +**Prerequisites:** Docker Engine with the Docker Compose plugin on every machine. Verify with `docker compose version`. The monitoring machine must be able to reach each node on port `8889`; it can be one of the node servers, but a separate machine keeps alerting alive even if a node host goes down. + +**Step 1: on the sequencer node.** Download and run the script, then choose **option 1** (set up node metrics): + +```bash +curl -fsSLO https://docs.aztec.network/scripts/aztec-monitoring.sh +bash aztec-monitoring.sh +``` + +The script finds your node's `docker-compose.yml`, writes a `docker-compose.override.yml` that enables metrics export (your base compose file is never modified), shows it to you for confirmation, and restarts the node. Afterward a collector serves Prometheus-format metrics on port `8889`. Open port `8889` only to the monitoring machine's IP, not the public internet: + +```bash +sudo ufw allow from <monitoring-machine-ip> to any port 8889 proto tcp +``` + +**Step 2: repeat on every additional node** (for high availability or a larger fleet). Each node gets its own collector on its own port `8889`. + +**Step 3: on the monitoring machine.** Download and run the script, then choose **option 2** (set up the monitoring stack): + +```bash +curl -fsSLO https://docs.aztec.network/scripts/aztec-monitoring.sh +bash aztec-monitoring.sh +``` + +The script asks for each node as `<node-ip>:8889` with a label you choose (for example `sequencer-1`), offers Telegram setup (it walks you through creating a bot with `@BotFather` and detects your chat id), then starts Grafana, Prometheus, and Alertmanager and confirms every node target is up. If a target is down, the usual cause is the port `8889` firewall rule from Step 1. + +**Step 4: open the dashboard** at `http://localhost:3000` on the monitoring machine (or tunnel with `ssh -L 3000:localhost:3000 <user>@<monitoring-machine>`). Log in with `admin` / `admin` and change the password. Pick your node in the **instance** dropdown at the top. + +**Day-2:** re-run `bash aztec-monitoring.sh` on the relevant machine at any time; settings are kept between runs. Option 2 adds or removes nodes, option 3 manages Telegram, option 4 shows what is installed, option 5 uninstalls (node and monitoring sides independently). To update, re-download the script and re-run the option you use on that machine. + +Keep ports `3000` (Grafana), `9090` (Prometheus), and `9093` (Alertmanager) closed to the public internet. The stack is read-only toward your node: it scrapes metrics and never touches keys, the database, or node configuration beyond the metrics override. + +## Set up monitoring manually + +If you prefer to run your own stack instead of the installer: + +1. **Enable metrics on your node** by setting the OpenTelemetry environment variables. Point the exporter at your OTLP collector and give the node a service name: + + ```bash + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://otel-collector:4318/v1/metrics + OTEL_SERVICE_NAME=aztec-sequencer + ``` + + `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` is the OTLP HTTP endpoint of your collector (port `4318` is the OTLP/HTTP default). The node also emits a `service.instance.id` that changes on every restart; pin a stable `instance` label in Prometheus so your series survive restarts, see [Keep your time series stable across restarts](#keep-your-time-series-stable-across-restarts). +2. **Run Prometheus** to scrape `http://your-node:metrics-port/metrics` at 15-second intervals. +3. **Run Grafana** pointed at Prometheus. +4. **Import a dashboard.** A community-maintained Grafana dashboard for Aztec validators is available at [grafana.com/grafana/dashboards/23054](https://grafana.com/grafana/dashboards/23054-aztec-validator/). + +For step-by-step instructions on each, see [Monitoring setup guides](/operate/operators/monitoring) in Advanced operations. Both Prometheus and Grafana configurations are covered in detail there. + +## Keep your time series stable across restarts + +Prometheus identifies a time series by its full label set. Aztec generates a fresh `service.instance.id` each time the node process starts, so the `instance` label changes on every restart. Each restart then looks like a brand-new series to Prometheus, and dashboards and alert rules built on the old label go blank. + +Pin the `instance` label in your Prometheus scrape config with a relabel rule. This is independent of what the node reports, so it survives restarts: + +```yaml +scrape_configs: + - job_name: "aztec-node" + static_configs: + - targets: ["your-node:metrics-port"] + relabel_configs: + - target_label: instance + replacement: my-sequencer-01 +``` + +Use a distinct, durable value for each node (for example `my-sequencer-01`, `my-sequencer-02`), and keep it the same across restarts and upgrades of that node. The node's own generated id is preserved as `exported_instance` (Prometheus keeps the original under an `exported_` prefix when a scrape label collides, unless you set `honor_labels: true`). + +## What about alerting on slashing risk + +Since v5.2.0 the node warns you directly when the network votes to slash one of your own validators. It watches slashing votes that name your attesters, logs `Own validator 0x... targeted by slashing vote (N of Q votes needed to slash)` on every such vote, and logs `Own validator 0x... was slashed for ...` once a round executes. The same signal is exported as metrics: alert on `aztec_slasher_own_validator_current_round_votes_max` approaching `aztec_slasher_quorum_size`. Votes cast while your node is offline are not counted, so treat the metric as a floor. + +The slashing vote itself still happens on L1 through the TallySlashingProposer contract, so once a slash payload is queued it is too late to fix the underlying behavior. Use these additional proxies as early warning: + +- Alert on **missed attestations and missed proposals**. Inactivity slashing is the accumulation of missed duties, so sustained failures are the direct early warning: they precede an inactivity penalty. The [installer](#set-up-monitoring-with-the-installer) ships alert rules for both (failed attestations and failed proposals in the last hour), so a stack set up that way warns you before the pattern becomes slashable. +- Watch `aztec_l1_publisher_balance_eth` (above). A dry publisher is a common upstream cause of missed proposals; the installer alerts on it too. +- Check **slashveto.me**, where the community veto council surfaces pending slash payloads before they execute. +- Watch your performance on **dashtec.xyz**. A drop in your attestation rate is the same signal, visible without your own monitoring. + +See [Slashing](./slashing) for the full slashing context. + +## See also + +- [Monitoring](/operate/operators/monitoring): full Prometheus, Grafana, and OTel setup guides +- [Metrics reference](/operate/operators/monitoring/metrics-reference): the full metric list and their meanings +- [Monitoring troubleshooting](/operate/operators/monitoring/troubleshooting): fixing common scraping problems +- [Operator tooling](/operate/operators/tooling): community monitoring scripts and dashboards diff --git a/docs/lines/v6/docs-operate/operators/concepts/sequencer-troubleshooting.md b/docs/lines/v6/docs-operate/operators/concepts/sequencer-troubleshooting.md new file mode 100644 index 00000000000..2940517b614 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/concepts/sequencer-troubleshooting.md @@ -0,0 +1,78 @@ +--- +id: sequencer-troubleshooting +title: Sequencer troubleshooting +description: A symptom-first guide to diagnosing a sequencer that is running but not proposing or attesting, covering publisher balance, L1 RPC, peer count, and coinbase configuration. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/sequencer-client/src/publisher/*", "labs/yarn-project/p2p/src/config.ts", "labs/yarn-project/ethereum/src/l1_reader.ts", "labs/yarn-project/node-keystore/src/*"] +--- + +Your node is running but proposals or attestations are not landing. This page is a decision tree for the most common causes, in the order worth checking them. Each branch points to the page with the full fix. + +## Start here + +Confirm the symptom first. A sequencer only proposes during slots it is assigned, so a quiet period is normal. Before troubleshooting, check that the node is actually missing assigned duties rather than simply waiting for a slot: + +- **`docker compose ps`** should show the sequencer as `(healthy)`. The health check in the reference compose file requires the node to be answering its API with a proven L2 tip and a synced chain time within the last five minutes, so this one line covers API readiness, container stability, and whether the node is keeping up. If it shows `(unhealthy)` or `starting` long after startup, the node is not keeping up. See [Verify everything works](../solo-sequencer/verify) for what each status means. +- **Look up your attester on an explorer** (Dashtec or Aztec Vision) and check its recent attestation and proposal record. A drop in your attestation rate is the signal that something is wrong. + +You do not need a monitoring stack for these. Where a section below names a Prometheus metric, that is the equivalent check if you run Prometheus and Grafana; the `docker compose ps`, log, and explorer checks work without one. + +If duties are genuinely being missed, work through the checks below in order. + +## 1. Is the node synced and seeing new blocks? + +A node that has fallen behind the chain tip cannot propose. + +- **Check (no monitoring stack):** run `docker compose logs -f aztec-sequencer` and watch for the block number to keep climbing. A node that is following the chain logs steady progress; one that is stuck stops advancing or repeats the same errors. `docker compose ps` showing `(healthy)` (above) also confirms the node has a recent proven tip. +- **Check (with monitoring):** `aztec_archiver_block_height` (your L2 view) and `aztec_archiver_l1_block_height` (your L1 view) are both advancing. +- **If the L2 view is stuck:** the node is not following the chain. Check the L1 RPC (next section) first, since most sync stalls trace back to it. +- **If the L1 view is stuck:** your L1 RPC is degraded or unreachable. See [L1 RPC requirements](./l1-rpc). + +## 2. Is the publisher funded? + +The publisher account pays L1 gas to submit proposals. When it runs dry, the node stops being able to propose. A single missed proposal is not slashable, but if the node misses almost all its duties over a full epoch (about 38 minutes on mainnet) it hits the inactivity threshold, which is slashable. See [Slashing](./slashing#the-inactivity-threshold). + +- **Check (no monitoring stack):** query the publisher's L1 balance directly with `cast balance <your-publisher-address> --rpc-url <your-L1-RPC> --ether`. You can also paste the publisher address into a block explorer ([Etherscan](https://etherscan.io) for mainnet, [Sepolia Etherscan](https://sepolia.etherscan.io) for testnet) and read its balance there. If it is near zero, this is your cause. +- **Check (with monitoring):** `aztec_l1_publisher_balance_eth` is the ETH balance of each publisher, and `aztec_l1_publisher_sends_affordable` is how many proposals it can still pay for at current L1 fees. Both are refreshed every minute. If `sends_affordable` is `0` for every publisher, this is your cause. See [Metrics reference](../monitoring/metrics-reference#l1-publisher-eth-balance). +- **Check (logs):** search for `No publisher can afford a proposal`, logged at most every 10 minutes while every publisher is short, and `Skipping checkpoint proposal`, logged when the node skips a slot it was due to propose. Both name the publisher to fund and how much ETH it is missing. The `Publisher balances` line at startup lists every publisher's balance. +- **Fix:** top up the publisher account. Keep enough headroom that a single proposal during an L1 gas spike cannot drain the balance. See [Fund accounts](../solo-sequencer/fund-accounts) and the publisher-funding calculator there. + +## 3. Is the L1 RPC serving everything the node needs? + +A node needs an execution RPC, a blob-serving consensus client, and trace methods. A gap in any one stops the node from following or publishing. + +- **`ECONNREFUSED 127.0.0.1:8545` (or your L1 port) at startup:** nothing is listening at the address the node is dialing. From inside a container, `127.0.0.1` is the container's own loopback, not the host, so an L1 RPC reachable on the host is unreachable under this setting. Point the L1 RPC environment variable at an address the container can reach. +- **`429 Too Many Requests`:** the RPC is rate-limiting the node off the chain. Raise the archiver polling intervals or self-host. +- **Startup blob-source summary shows `consensusSuperNodes=0`:** the consensus client is not serving blob sidecars. Restart it with a supernode or semi-supernode flag. +- **Startup logs warn that `debug_traceTransaction` failed:** the execution RPC does not expose trace methods, or does not retain enough recent history for them. The node validates trace availability at startup and warns when it is missing. + +Full causes and fixes for all of these are in [L1 RPC requirements](./l1-rpc#the-most-common-l1-side-errors). + +## 4. Is the node connected to enough peers? + +A node isolated from the network cannot receive the transactions and attestations it needs, or broadcast its own. + +Since v5.2.0 a node with **zero** connected peers stops acting rather than acting blindly, and says so in its logs: + +- It logs `Node has no connected peers; gossip and tx propagation are unavailable` at warn, repeated roughly once a minute, and one info line when connectivity returns. +- It skips building and proposing checkpoints while below `SEQ_MIN_PEERS_TO_PROPOSE` (default 1), logging `Skipping checkpoint proposal for slot N since we have too few connected peers`. L1-only duties (governance and slashing votes, prune, invalidation) still run. +- It does not file data-withholding slashing offenses for slots probed while peerless, since an empty local mempool is not evidence without gossip. +- It rejects incoming `sendTx` calls instead of silently dropping them. + +Setups that disable p2p entirely (local network, single node) are unaffected by all four gates. + +- **Check (no monitoring stack):** confirm your P2P port is reachable from outside your network with `nc -zv <your-external-IP> 40400`. A closed port is the usual reason a node cannot find peers. The node logs also report peer activity as it connects. +- **Check (with monitoring):** `aztec_peer_manager_peer_count_peers`. Below 5 indicates a reachability problem. +- **Check (health endpoint):** `curl http://localhost:8080/status` reports p2p connectivity per component. Set `P2P_HEALTH_MIN_PEERS` (default 0, report-only) to make a low peer count fail the health check. +- **Fix:** confirm your external IP is correct and that the P2P port is forwarded and open in your firewall (TCP and UDP). See [Monitoring and metrics](./monitoring#what-every-operator-should-alert-on). + +## 5. Is the coinbase set correctly? + +A misconfigured coinbase does not stop a node from proposing, but it routes rewards to the wrong place, which often surfaces as an "I am proposing but not getting rewards" report. Check this if duties are landing but rewards are not. + +- **Solo sequencer:** the coinbase is set in your keystore. If you never set `--coinbase` at setup, rewards accrue against the attester address. See [Identity model](./identity-model). +- **Staking provider:** in the default flow, each delegated attester's coinbase must point at that delegation's Split contract; a coinbase pointing elsewhere routes the delegation's rewards around the Split. If instead you use the [Aztec staking payout tool](https://github.com/AztecProtocol/aztec-staking-payout), every coinbase points at a single address you control and the tool distributes each delegator's share offchain, so the per-delegation Split check does not apply. See [Provider operations](../provider/provider-operations) for the per-delegation update procedure. + +## Still stuck? + +If none of the above explains the missed duties, gather your node logs and the metric values above and ask in [Discord](https://discord.com/invite/aztec). See also the [operator FAQ](../operator-faq) for error-specific entries. diff --git a/docs/lines/v6/docs-operate/operators/concepts/slashing.md b/docs/lines/v6/docs-operate/operators/concepts/slashing.md new file mode 100644 index 00000000000..d90cb1d07c7 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/concepts/slashing.md @@ -0,0 +1,121 @@ +--- +id: slashing +title: Slashing +description: What gets you slashed, how much you lose, the ejection thresholds, zombie status, the veto council, and how to recover after being slashed or ejected. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/slasher/src/*"] +--- + +Slashing is the protocol's way of penalizing validators who fail their duties. For the deep technical guide on how your node votes on slashings, see [Slashing and offenses](/operate/operators/sequencer-management/slashing_and_offenses) in Advanced operations. + +## TL;DR + +- Slashing is live on mainnet. +- Offenses are penalised in tiers: most deduct **2,000 AZTEC** (the small tier), and duplicate proposals or attestations deduct **5,000 AZTEC** (the large tier). +- Your stake is ejected from the active set if it falls below **190,000 AZTEC** (the local ejection threshold). Once ejected, you cannot top the same attester back up: recovering means a full exit and registering a new attester identity. A second, deeper threshold at **100,000 AZTEC** (the GSE floor) is where you are also out at the protocol-wide level. +- That gives you **10,000 AZTEC of headroom** from a full stake before ejection: five small-tier slashes, or two large-tier ones. +- A community **veto council** can stop a benign slash before it executes. + +## What gets you slashed + +Your node detects these offenses on other validators and votes to slash them; the same rules apply to you. Each offense maps to a penalty tier: **small** (2,000 AZTEC) or **large** (5,000 AZTEC). Some offenses are defined in the protocol but not yet enabled (their penalty is set to zero), so they cannot slash you today. + +| Offense | Penalty | What it means | +|---|---|---| +| Inactivity | 2,000 (small) | You missed almost all your committee duties (attesting, or proposing when selected) over a full epoch. Almost every slash comes from this. See the threshold below. | +| Invalid attestations in a proposal | 2,000 (small) | You submitted a checkpoint with too few attestations, with invalid signatures, or with signatures from non-committee members. | +| Invalid block over p2p | 2,000 (small) | You broadcast an invalid block proposal over p2p. | +| Invalid checkpoint proposal | 2,000 (small) | You broadcast an invalid checkpoint proposal. | +| Duplicate proposal | 5,000 (large) | You sent two proposals for the same slot, almost always two of your nodes sharing a key, not malice. | +| Duplicate attestation | 5,000 (large) | You signed conflicting attestations for the same slot, again usually a shared-key misconfiguration. | +| Data withholding | not yet enabled | As a committee member you did not make a checkpoint's transaction data available. Defined but not currently penalised. | +| Proposed a descendant of a checkpoint with invalid attestations | not yet enabled | You proposed a checkpoint that descends from one whose attestations were invalid. Defined but not currently penalised. | +| Attested to an invalid checkpoint proposal | not yet enabled | You attested to a checkpoint proposal that was invalid. Defined but not currently penalised. | + +Inactivity is what almost every slash comes from; the others are configured but rarely fire. The duplicate-proposal and duplicate-attestation offenses almost always trace back to two of your nodes sharing keys, not malice. Penalty amounts are network parameters, so confirm the current values for your network rather than treating these as fixed. + +### The inactivity threshold + +Inactivity is measured per epoch and needs two conditions to be met before a slash payload is created: + +- You missed at least **`SLASH_INACTIVITY_TARGET_PERCENTAGE`** of your duties in an epoch. The current network value is **0.9** (90%). The percentage is taken over the slots that actually had a proposal to attest to, not over every slot in the epoch, so a node that is fully offline is counted as missing close to 100%. +- This held for **`SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD`** consecutive epochs. The current network value is **1**, so one full epoch over the threshold is enough. + +A single missed slot is not slashable. Being effectively offline for one full epoch is. Both values are network parameters set by governance, so confirm the current ones for your network rather than assuming these defaults. An epoch is `AZTEC_EPOCH_DURATION` slots; at the current mainnet slot time that is roughly 38 minutes. + +## How much you lose + +Slashing uses three penalty tiers (`SLASH_AMOUNT_SMALL`, `SLASH_AMOUNT_MEDIUM`, `SLASH_AMOUNT_LARGE`), and each offense maps to one of them. On mainnet the small tier is **2,000 AZTEC** and the large tier is **5,000 AZTEC** (the medium tier is also set to 5,000). Most offenses, including inactivity, are small-tier; duplicate proposals and attestations are large-tier. + +The 2,000 AZTEC small-tier figure is 1% of the 200,000 AZTEC activation stake. Repeat offenses stack. + +## Thresholds you should know + +| Threshold | Value | What happens at this level | +|---|---|---| +| Activation | 200,000 AZTEC | What you stake to become an active validator | +| Local ejection | **190,000 AZTEC** | Below this, you are ejected from the active validator set on the canonical rollup. Recovering means a full exit and a new attester identity; you cannot top the same attester back up. | +| Protocol-wide ejection (GSE) | 100,000 AZTEC | A deeper threshold. Below this you are also out at the protocol-wide GSE level, not just on this rollup. | + +The local ejection threshold is configurable per rollup deployment by governance. The GSE floor is a protocol-wide constant. + +Starting from a full 200,000 AZTEC stake, you have 10,000 AZTEC of headroom before the local threshold: five small-tier slashes, or two large-tier ones, bring you to it. The next slash ejects you. + +Ejection happens at an epoch boundary, not the instant your stake crosses the threshold, and a single slashing round can carry several offenses for the same attester. So a validator can record **more than five slashes**. The headroom figure is a planning guide, not a hard cap on how many slashes can be recorded against you. + +## How you know you have been slashed + +Each slash emits a `Slashed(address, uint256)` event on the Rollup contract (the address is your attester, the amount is the slashed stake in wei), and it shows up as a reduction in your GSE stake balance. + +The community surfaces that catch this for you: + +- **slashveto.me** publishes pending slash payloads before they execute, so you get warning before a slash lands. +- **dashtec.xyz** shows per-epoch performance and flags slashing events against tracked attesters. +- **haveibeenslashed.xyz** is a focused tool for checking a single attester address against historical slashing events. + +These dashboards typically surface pending or executed slashes before the information becomes visible in your own node logs. + +## The veto council + +A community group operates as the **Vetoer** for the Slasher contract (`0xBbB4aF368d02827945748b28CD4b2D42e4A37480`, a multisig). When the TallySlashingProposer reports a pending slash payload, the council reviews it. If the slash looks benign (a known HA misconfiguration, a one-time outage on a usually-reliable operator, a bug in the slashing software, etc.) they call `vetoPayload(address)` and the slash does not execute. + +The Vetoer can also pause slashing entirely for up to 3 days at a time via `setSlashingEnabled(false)`. + +### Requesting a veto + +Formal requests to veto or pause slashing are made through the [council's GitHub repository](https://github.com/aztec-slash-veto/council): open an issue with the veto-request template. + +The council's scope is deliberately narrow. Requests are **in scope** for events outside an operator's control, such as a confirmed bug in the node software or contracts, a network relaunch needing a halt, or an upstream bug hitting many sequencers at once. They are **out of scope** for anything that is a consequence of not following operator best practices (for example, a single operator's outage or misconfiguration). The full scope and process are in the repository. + +The veto is preventative (it stops a slash before it lands, within 72 hours of the voting period ending), not restitutive (it does not restore stake after a slash has executed). + +## What to do if you have been slashed + +The action depends on how far below thresholds you are. + +### Stake is still above 190,000 AZTEC + +You are still an active validator. The only loss is the slashed amount. + +Action: investigate root cause. Review your node logs around the offense time, identify whether it was an inactivity, proposing, or attestation issue, and fix it so it does not repeat. + +### Stake has fallen below 190,000 AZTEC (local ejection, zombie status) + +You have been removed from the active validator set on this rollup and stop earning new rewards. Onchain your attester now reports status `2`, which the protocol calls **zombie** (you can check it with `getStatus(attester)` on the Rollup, or `getAttesterView(attester)`). The staking dashboard shows this status as **Inactive**. + +Recovery is a fresh start, not a top-up: stake cannot be added to an existing attester, and an exited or ejected attester address can never be registered again. The path differs by stake type: + +- **Everyone except genesis sequencers**: ejection does not return your stake to your wallet on its own. The slash that crosses the threshold moves your remaining stake into the rollup's exit queue and starts the withdrawal delay (about 10 days), but the funds stay locked until you claim them. The [staking dashboard](https://stake.aztec.network) handles this in two steps, both on the stake's details panel, connected with the withdrawer wallet. First, **Add Initiate Unstake**; this does not restart the delay, which is already running from the ejection. After the delay has passed, **Add Finalize Withdraw** returns the stake (to your wallet for a direct stake, or to your token vault if you staked from one). Both steps queue into the batch cart, so you confirm them from there. Then top the balance back up to 200,000 AZTEC and register a new sequencer identity (new attester address and BLS key) through the dashboard. +- **Genesis sequencers**: you can run the same two steps, but the funds do not reach your wallet until the genesis vault unlocks on November 13, 2026. **Add Initiate Unstake** and, after the delay, **Add Finalize Withdraw** both work, and they return the stake to your token vault rather than to your wallet, because the vault is the withdrawer. The vault itself stays locked until November 13, so the recovered stake sits there, idle, until then. Re-staking before the unlock also depends on the vault holding a full 200,000 AZTEC, which a slashed-and-recovered position no longer does. After the unlock, you can move the tokens to your wallet, top the balance back up, and register a new sequencer identity. + +### Stake has fallen below 100,000 AZTEC (protocol-wide ejection) + +This is the deeper threshold, and reaching it takes far more than the handful of slashes that cross local ejection. Local ejection at 190,000 removes you from the active set on a rollup, but it does not stop slash payloads that are already in flight, and the same attester can keep accruing offenses (for example a node left misconfigured and offline through many rounds) until the stake erodes all the way to the GSE floor. In normal operation you would notice and act long before this; it is a sign of an attester left unattended through a sustained problem. + +You are out at the GSE level as well. The recovery path is the same as above: withdraw the remaining stake, replenish to 200,000 AZTEC, and register a new sequencer identity. The full exit-and-redeposit cycle takes a minimum of about 10 days plus whatever entry-queue depth exists at the time of re-staking. + +## See also + +- [Slashing and offenses](/operate/operators/sequencer-management/slashing_and_offenses): the deep technical guide to slashing rounds, offsets, and how your node votes +- [Identity model](./identity-model): which key is at risk when slashing happens (your attester) +- [Claiming rewards](./claiming-rewards): what happens to unclaimed rewards if you are ejected diff --git a/docs/lines/v6/docs-operate/operators/full-node/overview.md b/docs/lines/v6/docs-operate/operators/full-node/overview.md new file mode 100644 index 00000000000..e5a5be48185 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/full-node/overview.md @@ -0,0 +1,27 @@ +--- +id: overview +title: Full node overview +description: What a full node is and why you might run one. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/aztec/src/cli/cmds/start_node.ts", "labs/yarn-project/aztec/src/cli/aztec_start_options.ts"] +--- + +import NodeHardware from "../_node_hardware.mdx"; + +## Overview + +This guide covers the steps required to run a full node on Aztec using Docker Compose. + +A full node allows you to connect and interact with the network, providing an interface to send and receive transactions and state updates without relying on third parties. + +You should run your own full node if you want to interact with the network in the most privacy-preserving way. It's also a great way to support the Aztec network and get involved with the community. + +### Minimum Hardware Requirements + +<NodeHardware /> + +These requirements are subject to change as the network throughput increases. + +**Before proceeding:** Ensure you've reviewed and completed the [prerequisites](../prerequisites.md). + +This setup includes only essential settings. The `--network #release_network` flag applies network-specific defaults—see the [CLI reference](../reference/cli-reference.md) for all available configuration options. diff --git a/docs/lines/v6/docs-operate/operators/full-node/setup.md b/docs/lines/v6/docs-operate/operators/full-node/setup.md new file mode 100644 index 00000000000..1f7900789f3 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/full-node/setup.md @@ -0,0 +1,112 @@ +--- +id: setup +title: Full node setup +description: How to run a full node on the Aztec network using Docker Compose. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/aztec/src/cli/cmds/start_node.ts", "labs/yarn-project/aztec/src/cli/aztec_start_options.ts", "labs/yarn-project/ethereum/src/config.ts", "labs/yarn-project/p2p/src/config.ts"] +--- + +## Setup + +### Step 1: Set Up Directory Structure + +Create the directory structure for node data: + +```bash +mkdir -p aztec-node/data +cd aztec-node +touch .env +``` + +### Step 2: Configure Environment Variables + +Add the following to your `.env` file: + +```bash +DATA_DIRECTORY=./data +LOG_LEVEL=info +ETHEREUM_HOSTS=[your L1 execution endpoint] +L1_CONSENSUS_HOST_URLS=[your L1 consensus endpoint] +# Optional: only set ETHEREUM_DEBUG_HOSTS if your trace-capable endpoint is different from ETHEREUM_HOSTS. +# If left unset, the node uses ETHEREUM_HOSTS for trace calls. See the note below. +ETHEREUM_DEBUG_HOSTS=[a separate trace-capable L1 endpoint, if you have one] +P2P_IP=[your external IP address] +P2P_PORT=40400 +AZTEC_PORT=8080 +AZTEC_ADMIN_PORT=8880 +``` + +:::tip +Find your public IP address with: `curl ipv4.icanhazip.com` +::: + +:::warning Trace-capable L1 endpoint required +The node needs an L1 execution endpoint that serves `trace_transaction` or `debug_traceTransaction`. A checkpoint published through a non-standard contract, rather than by a direct call to the rollup, cannot be read from the transaction calldata alone. The node traces that transaction to recover the checkpoint, and without a trace it stops syncing at that checkpoint. + +Set `ETHEREUM_DEBUG_HOSTS` to your trace-capable endpoint. If you leave it unset, the node uses `ETHEREUM_HOSTS` for trace calls. The node only uses `ETHEREUM_DEBUG_HOSTS` for traces, not for regular L1 interactions. + +At startup the node checks that it can trace a recent L1 transaction on that endpoint, and refuses to start if neither method works. To start anyway, set `ETHEREUM_ALLOW_NO_DEBUG_HOSTS=true`. The node then logs a warning and runs, but stalls on the first checkpoint it cannot read until you point it at a trace-capable endpoint and restart it. +::: + +### Step 3: Create Docker Compose File + +Create a `docker-compose.yml` file in your `aztec-node` directory: + +```yaml +services: + aztec-node: + image: "azteclabs/aztec:#release_version" + container_name: "aztec-node" + ports: + - ${AZTEC_PORT}:${AZTEC_PORT} + - ${P2P_PORT}:${P2P_PORT} + - ${P2P_PORT}:${P2P_PORT}/udp + volumes: + - ${DATA_DIRECTORY}:/var/lib/data + environment: + DATA_DIRECTORY: /var/lib/data + LOG_LEVEL: ${LOG_LEVEL} + ETHEREUM_HOSTS: ${ETHEREUM_HOSTS} + L1_CONSENSUS_HOST_URLS: ${L1_CONSENSUS_HOST_URLS} + ETHEREUM_DEBUG_HOSTS: ${ETHEREUM_DEBUG_HOSTS} + P2P_IP: ${P2P_IP} + P2P_PORT: ${P2P_PORT} + AZTEC_PORT: ${AZTEC_PORT} + AZTEC_ADMIN_PORT: ${AZTEC_ADMIN_PORT} + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --node + --network #release_network + networks: + - aztec + restart: always + +networks: + aztec: + name: aztec +``` + +:::warning Security: Admin Port Not Exposed +The admin port (8880) is intentionally **not exposed** to the host machine for security reasons. The admin API provides sensitive operations like configuration changes and database rollbacks that should never be accessible from outside the container. + +If you need to access admin endpoints, use `docker exec`: +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' +``` + +The admin API requires an API key by default. The node generates one at first startup and prints it once to the logs; pass it with the `x-api-key` header (or `Authorization: Bearer [ADMIN_API_KEY]`). See [admin API key authentication](../reference/changelog/v4.md#admin-api-key-authentication). +::: + +### Step 4: Start the Node + +Start the node: + +```bash +docker compose up -d +``` diff --git a/docs/lines/v6/docs-operate/operators/full-node/troubleshooting.md b/docs/lines/v6/docs-operate/operators/full-node/troubleshooting.md new file mode 100644 index 00000000000..e99e173c5d4 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/full-node/troubleshooting.md @@ -0,0 +1,41 @@ +--- +id: troubleshooting +title: Full node troubleshooting +description: Common issues operators hit when running a full node, and how to fix them. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/p2p/src/config.ts", "labs/yarn-project/ethereum/src/config.ts"] +--- + +## Troubleshooting + +### Port forwarding not working + +**Issue**: Your node cannot connect to peers. + +**Solutions**: + +- Verify your external IP address matches the `P2P_IP` setting +- Check firewall rules on your router and local machine +- Test connectivity using: `nc -zv [your-ip] 40400` + +### Node not syncing + +**Issue**: Your node is not synchronizing with the network. + +**Solutions**: + +- Check L1 endpoint connectivity +- Verify both execution and consensus clients are fully synced +- Review logs for specific error messages +- Ensure L1 endpoints support high throughput + +### Docker issues + +**Issue**: Container won't start or crashes. + +**Solutions**: + +- Ensure Docker and Docker Compose are up to date +- Check disk space availability +- Verify the `.env` file is properly formatted +- Review container logs: `docker compose logs aztec-node` diff --git a/docs/lines/v6/docs-operate/operators/full-node/verification.md b/docs/lines/v6/docs-operate/operators/full-node/verification.md new file mode 100644 index 00000000000..d76f8b866c9 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/full-node/verification.md @@ -0,0 +1,49 @@ +--- +id: verification +title: Verifying your full node +description: Checks to confirm your full node is healthy and reachable on the network. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/stdlib/src/interfaces/aztec-node.ts", "labs/yarn-project/aztec-node/src/aztec-node/server.ts"] +--- + +## Verification + +Once your node is running, verify it's working correctly: + +### Check Node Sync Status + +Check the current sync status: + +```bash +curl -s -X POST -H 'Content-Type: application/json' \ +-d '{"jsonrpc":"2.0","method":"aztec_getChainTips","params":[],"id":67}' \ +http://localhost:8080 | jq -r ".result.proven.block.number" +``` + +Compare the output with block explorers (see [Networks page](/networks) for explorer links). + +### Check Node Status + +```bash +curl http://localhost:8080/status +``` + +### Verify Port Connectivity + +```bash +# Check TCP connectivity on port 40400 +nc -vz [YOUR_EXTERNAL_IP] 40400 +# Should return: "Connection to [YOUR_EXTERNAL_IP] 40400 port [tcp/*] succeeded!" + +# Check UDP connectivity on port 40400 +nc -vu [YOUR_EXTERNAL_IP] 40400 +# Should return: "Connection to [YOUR_EXTERNAL_IP] 40400 port [udp/*] succeeded!" +``` + +### View Logs + +```bash +docker compose logs -f aztec-node +``` + +If all checks pass, your node should be up, running, and connected to the network. diff --git a/docs/lines/v6/docs-operate/operators/index.mdx b/docs/lines/v6/docs-operate/operators/index.mdx new file mode 100644 index 00000000000..e2dde24f60c --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/index.mdx @@ -0,0 +1,37 @@ +--- +id: index +title: Run a Node +description: Pick what you want to run on Aztec (full node, solo sequencer, staking provider, or prover). Each role has its own setup path. +displayed_sidebar: operatorsSidebar +slug: /operators +--- + +import RolePicker from "@site/src/components/RolePicker"; +import PopularQuestions from "@site/src/components/PopularQuestions"; + +# Run a Node + +Aztec is a permissionless, decentralized network with no centralized sequencer. Anyone can sync a node, propose blocks, or generate proofs. Pick the role that fits what you want to do, and the docs will walk you through it. + +<RolePicker /> + +## Popular questions + +<PopularQuestions /> + +## What comes after picking a role + +- The **guided setup** is opinionated and personalized: paste your RPC URLs and keys once, every command on every page auto-fills with your values. +- The **advanced operations** section in the sidebar covers everything the playbook doesn't: high-availability topologies, deep keystore management, monitoring stack, governance signalling, provider tooling. +- The **reference** section is for when you know what you're looking for: CLI flags, env vars, JSON-RPC, the Ethereum-RPC requirements. + +## Using these docs with an AI assistant + +These docs are published in a machine-readable form, so you can hand them to an AI assistant instead of copying pages by hand. + +- `llms.txt` at [docs.aztec.network/llms.txt](https://docs.aztec.network/llms.txt) is an index of every page. `llms-full.txt` is the whole site as a single file. +- The **Copy page** button at the top of each page copies that page as markdown, ready to paste into a chat. +- Append `.md` to any docs URL to fetch the markdown source of that page. +- The [AI Tooling guide](/developers/ai_tooling) covers the Aztec MCP server and project instruction files for tools like Claude Code, Cursor, and Codex. + +Always verify commands, addresses, and version numbers against these docs before you run them on a live node. An assistant can read the docs, but it cannot see your setup. diff --git a/docs/lines/v6/docs-operate/operators/keystore/advanced-patterns.md b/docs/lines/v6/docs-operate/operators/keystore/advanced-patterns.md new file mode 100644 index 00000000000..db776d301f3 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/keystore/advanced-patterns.md @@ -0,0 +1,284 @@ +--- +title: Sample configuration patterns +description: Learn about advanced keystore patterns including multiple publishers, multiple sequencers, and infrastructure provider scenarios. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/node-keystore/src/schemas.ts", "labs/yarn-project/node-keystore/src/keystore_manager.ts"] +--- + +## Overview + +This guide covers advanced keystore configuration patterns for complex deployments, including multi-publisher setups, running multiple sequencers, and infrastructure provider scenarios. + +## Multiple publishers + +Multiple publisher accounts provide: +- **Load distribution**: Spread L1 transaction costs across accounts +- **Parallelization**: Submit multiple transactions simultaneously +- **Resilience**: Continue operating if one publisher runs out of gas + +**Array of publishers:** + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xATTESTER_ETH_PRIVATE_KEY", + "bls": "0xATTESTER_BLS_PRIVATE_KEY" + }, + "publisher": [ + "0xPUBLISHER_1_PRIVATE_KEY", + "0xPUBLISHER_2_PRIVATE_KEY", + "0xPUBLISHER_3_PRIVATE_KEY" + ], + "feeRecipient": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + ] +} +``` + +**Mixed storage methods:** + +```json +{ + "schemaVersion": 1, + "remoteSigner": "https://signer1.example.com:8080", + "validators": [ + { + "attester": { + "eth": "0xATTESTER_ETH_PRIVATE_KEY", + "bls": "0xATTESTER_BLS_PRIVATE_KEY" + }, + "publisher": [ + "0xLOCAL_PRIVATE_KEY", + "0xREMOTE_SIGNER_ADDRESS_1", + { + "address": "0xREMOTE_SIGNER_ADDRESS_2", + "remoteSignerUrl": "https://signer2.example.com:8080" + }, + { + "mnemonic": "test test test test test test test test test test test junk", + "addressCount": 2 + } + ], + "feeRecipient": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + ] +} +``` + +This creates 5 publishers: +1. Local private key +2. Address in default remote signer (signer1.example.com) +3. Address in alternative remote signer (signer2.example.com) +4. Two mnemonic-derived addresses + +:::warning Publisher Funding Required +All publisher accounts must be funded with ETH. Monitor balances to avoid missed proposals or proofs. +::: + +## Multiple sequencers + +Run multiple sequencer identities in a single node. This is useful when you operate multiple sequencers but want to consolidate infrastructure. + +:::info +This section covers running **multiple different sequencer identities** on a single node. +::: + +**When to use multiple sequencers per node:** +- You have multiple sequencer identities (different attester addresses) +- You want to consolidate infrastructure and reduce operational overhead +- You're running sequencers for multiple entities or clients +- You want to simplify management of several sequencers + +**Use two approaches:** + +**Option 1: Shared configuration** + +Multiple attesters sharing the same publisher, coinbase, and fee recipient: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": [ + { + "eth": "0xSEQUENCER_1_ETH_KEY", + "bls": "0xSEQUENCER_1_BLS_KEY" + }, + { + "eth": "0xSEQUENCER_2_ETH_KEY", + "bls": "0xSEQUENCER_2_BLS_KEY" + } + ], + "publisher": ["0xSHARED_PUBLISHER"], + "coinbase": "0xSHARED_COINBASE", + "feeRecipient": "0xSHARED_FEE_RECIPIENT" + } + ] +} +``` + +**Option 2: Separate configurations** + +Each sequencer with its own publisher, coinbase, and fee recipient: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xSEQUENCER_1_ETH_KEY", + "bls": "0xSEQUENCER_1_BLS_KEY" + }, + "publisher": ["0xPUBLISHER_1"], + "coinbase": "0xCOINBASE_1", + "feeRecipient": "0xFEE_RECIPIENT_1" + }, + { + "attester": { + "eth": "0xSEQUENCER_2_ETH_KEY", + "bls": "0xSEQUENCER_2_BLS_KEY" + }, + "publisher": ["0xPUBLISHER_2"], + "coinbase": "0xCOINBASE_2", + "feeRecipient": "0xFEE_RECIPIENT_2" + } + ] +} +``` + +## Infrastructure provider scenarios + +### Scenario 1: Multiple sequencers with isolation + +For sequencers requiring complete separation, use separate keystore files: + +**keystore-sequencer-a.json:** +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xSEQUENCER_A_ETH_KEY", + "bls": "0xSEQUENCER_A_BLS_KEY" + }, + "feeRecipient": "0xFEE_RECIPIENT_A" + } + ] +} +``` + +**keystore-sequencer-b.json:** +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xSEQUENCER_B_ETH_KEY", + "bls": "0xSEQUENCER_B_BLS_KEY" + }, + "feeRecipient": "0xFEE_RECIPIENT_B" + } + ] +} +``` + +Point `KEY_STORE_DIRECTORY` to the directory containing both files. + +### Scenario 2: Shared publisher infrastructure + +Multiple sequencers sharing a publisher pool for simplified gas management: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xSEQUENCER_1_ETH_KEY", + "bls": "0xSEQUENCER_1_BLS_KEY" + }, + "publisher": ["0xPUBLISHER_1", "0xPUBLISHER_2"], + "feeRecipient": "0xFEE_RECIPIENT_1" + }, + { + "attester": { + "eth": "0xSEQUENCER_2_ETH_KEY", + "bls": "0xSEQUENCER_2_BLS_KEY" + }, + "publisher": ["0xPUBLISHER_1", "0xPUBLISHER_2"], + "feeRecipient": "0xFEE_RECIPIENT_2" + } + ] +} +``` + +Both sequencers share publishers while maintaining separate identities and fee recipients. + +## Prover configurations + +**Simple prover** (uses same key for identity and publishing): + +```json +{ + "schemaVersion": 1, + "prover": "0xPROVER_PRIVATE_KEY" +} +``` + +**Prover with dedicated publishers:** + +```json +{ + "schemaVersion": 1, + "prover": { + "id": "0xPROVER_IDENTITY_ADDRESS", + "publisher": [ + "0xPUBLISHER_1_PRIVATE_KEY", + "0xPUBLISHER_2_PRIVATE_KEY" + ] + } +} +``` + +The `id` receives prover rewards while `publisher` accounts submit proofs. + +## Complete Configuration Examples + +### High Availability Sequencer Setup + +Creating keystores for running the same sequencer across multiple nodes: + +```bash +# Step 1: Generate a base keystore with your attester and multiple publishers +aztec validator-keys new \ + --fee-recipient [YOUR_FEE_RECIPIENT] \ + --mnemonic "your shared mnemonic..." \ + --address-index 0 \ + --publisher-count 3 \ + --data-dir ~/keys-temp + +# This generates ONE keystore with: +# - Attester keys (ETH and BLS) at derivation index 0 +# - Three publisher keys at indices 1, 2, and 3 +``` + +After generation, you'll have a keystore with one attester and multiple publishers. Create separate keystores for each node by copying the base keystore and editing each to use only one publisher: + +**Node 1** - Uses publisher at index 1 +**Node 2** - Uses publisher at index 2 +**Node 3** - Uses publisher at index 3 + +Each node's keystore will have the **same attester keys** (both ETH and BLS) but a **different publisher key**. + +## Next steps + +- See [Troubleshooting](./troubleshooting.md) for common issues +- Return to [Key Storage Methods](./storage-methods.md) for more options +- Start with basics at [Creating Keystores](./creating-keystores.md) diff --git a/docs/lines/v6/docs-operate/operators/keystore/creating-keystores.md b/docs/lines/v6/docs-operate/operators/keystore/creating-keystores.md new file mode 100644 index 00000000000..9c9bb7eb61d --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/keystore/creating-keystores.md @@ -0,0 +1,520 @@ +--- +id: creating_keystores +displayed_sidebar: operatorsSidebar +title: Creating Sequencer Keystores +description: Learn how to create sequencer keystores for running validators on the Aztec network using the Aztec CLI. +references: ["labs/yarn-project/cli/src/cmds/validator_keys/*", "labs/yarn-project/node-keystore/src/schemas.ts"] +--- + +## Overview + +Keystores are configuration files that store the cryptographic keys and addresses your sequencer node needs to operate on the Aztec network. This guide shows you how to create keystores using the Aztec CLI's `validator-keys` commands. + +A keystore contains: +- **Attester keys**: Your sequencer's identity (Ethereum and BLS keys for signing proposals and attestations) +- **Publisher keys**: Keys used to submit blocks to L1 (requires ETH for gas) +- **Fee recipient**: Aztec address for L2 transaction fees (currently not used) +- **Coinbase address**: Ethereum address receiving L1 block rewards (optional, defaults to attester address) + +## Prerequisites + +Before creating keystores, ensure you have: + +- Basic understanding of Ethereum addresses and private keys +- Access to an Ethereum L1 RPC endpoint +- Foundry toolkit installed (for creating publisher addresses) + +## Installing the Aztec CLI + +First, install the Aztec CLI using the official installer: + +```bash +VERSION=#release_version bash -i <(curl -sL https://install.aztec.network/#release_version) +``` + +Verify your CLI installation: + +```bash +aztec --version +``` + +## Recommended Setup: Multiple Validators with Shared Publisher + +This approach creates multiple sequencer identities (validators) that share a single publisher address for submitting transactions to L1. This is the recommended configuration for production deployments. + +### Step 1: Create Publisher Address and Set RPC Endpoint + +#if(testnet) +First, set your Ethereum Sepolia L1 RPC endpoint: +#else +First, set your Ethereum mainnet L1 RPC endpoint: +#endif + +```bash +export ETH_RPC=https://ethereum-rpc.publicnode.com +``` + +Or use your preferred Ethereum RPC provider (Infura, Alchemy, etc.). + +Then generate a separate address for publishing transactions to L1 using the Foundry toolkit: + +```bash +cast wallet new-mnemonic --words 24 +``` + +**Example output:** + +``` +Successfully generated a new mnemonic. +Phrase: +word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12 word13 word14 word15 word16 word17 word18 word19 word20 word21 word22 word23 word24 + +Accounts: +- Account 0: +Address: 0xE434A95e816991E66bF7052955FD699aEf8a286b +Private key: 0x7988a4a7...79f058a0 +``` + +:::warning Critical: Save Your Publisher Mnemonic +The 24-word mnemonic is the **only way** to recover your publisher private key. Store it securely offline (not on the server running the node). +::: + +**Save from the output:** +- ✅ The 24-word mnemonic (for recovery) +- ✅ The private key (you'll use this in the next step) +- ✅ The address (you'll fund this with ETH) + +### Step 2: Generate Your Keystores with Publisher + +Generate 5 validators with the publisher private key from Step 1: + +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ +#if(testnet) + --gse-address 0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac \ +#else + --gse-address 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f \ +#endif + --l1-rpc-urls $ETH_RPC \ + --count 5 \ + --publishers 0x7988a4a779f058a0 +``` + +Replace `0x7988a4a779f058a0` with your actual publisher private key from Step 1. + +**What this command does:** +- Generates a new mnemonic for your validator keys (save this securely!) +- Creates 5 sequencer identities (validators) with Ethereum and BLS keys +- Configures all validators to use the same publisher address for L1 submissions +- Generates public keystore data for the staking dashboard +- Saves files to `~/.aztec/keystore/` + +**Example output:** + +``` +No mnemonic provided, generating new one... +Using new mnemonic: + +absent city nephew garment million badge front text memory grape two lizard + +Wrote validator keystore to /Users/your-name/.aztec/keystore/key1.json +Wrote staker output for 5 validator(s) to /Users/your-name/.aztec/keystore/key1_staker_output.json + +acc1: + attester: + eth: 0x8E76a8B8D66E0A56E241F2768fD2ad4eba07E565 + bls: 0x29eaf46e4699e33a1abe7300258567c624a7304a2134e31aa2609437f281d81d + publisher: + - 0x7988a4a779f058a0 +acc2: + attester: + eth: 0x2037b472537a4246B1A7325f327028EF450ba0Ef + bls: 0x8d7eb7d9436ac6cb9b8f1c211673ea228c7f438882e6438b2caefca753df28e8 + publisher: + - 0x7988a4a779f058a0 +acc3: + attester: + eth: 0x0c14593f7465DeDbb86d68982374BB05F4C60386 + bls: 0xad1cccf512d2f180238af795831344445f7ac47e2d623f3dac854e93e5b1e76d + publisher: + - 0x7988a4a779f058a0 +acc4: + attester: + eth: 0x4D213928988f0123f6b3B4A377F856812F08E831 + bls: 0xa90f5889dddd4cd6bc5a28db5e0db60d3cbf5147eb6e82b313024b2d0634110e + publisher: + - 0x7988a4a779f058a0 +acc5: + attester: + eth: 0x29f147Da38d5F66bB84e791969b365c796829c92 + bls: 0x0d683001c2ce866e322f0c7509f087a909508787d125336931aa9168d2a1f95b + publisher: + - 0x7988a4a779f058a0 + +Note: The publisher value shown is the private key (truncated in this example). All validators share the same publisher private key. + +Staker outputs: +[ + { + "attester": "0x8E76a8B8D66E0A56E241F2768fD2ad4eba07E565", + "publicKeyG1": { "x": "0x...", "y": "0x..." }, + "publicKeyG2": { "x0": "0x...", "x1": "0x...", "y0": "0x...", "y1": "0x..." }, + "proofOfPossession": { "x": "0x...", "y": "0x..." } + }, + ... (4 more validators) +] +``` + +:::warning Critical: Save Both Mnemonics +You now have **two separate mnemonics** to secure: + +1. **Validator mnemonic** (shown above, 12 words) - Regenerates your attester keys +2. **Publisher mnemonic** (from Step 1, 24 words) - Regenerates your publisher key + +Both must be stored securely offline. Losing either mnemonic means losing access to those keys. +::: + +**Files created:** +- `~/.aztec/keystore/key1.json` - Private keystore with all 5 validators and publisher configured +- `~/.aztec/keystore/key1_staker_output.json` - Public keystore for staking dashboard + +### Step 3: Fund the Publisher Address + +Your publisher address needs ETH to pay for L1 gas when submitting proposals. + +**Funding requirement:** At least **0.3 ETH** for 5 validators (rule of thumb: 0.1 ETH per validator) + +Transfer ETH to the publisher address from Step 1. You can check the balance with: + +```bash +cast balance 0xE434A95e816991E66bF7052955FD699aEf8a286b --rpc-url $ETH_RPC +``` + +Replace the address with your actual publisher address. + +:::warning Monitor Publisher Balance +Set up monitoring to alert when the publisher balance falls below 0.5 ETH to prevent failed block publications. +::: + +### Step 4: Upload Keystore to Your Node + +Now you're ready to spin up your sequencer node! + +**Upload the private keystore to your server:** + +The `key1.json` file contains your private keys and must be uploaded to your sequencer node. + +**For standard server deployments:** +```bash +# Upload to your server's keystore directory +scp ~/.aztec/keystore/key1.json user@your-server:/path/to/aztec-sequencer/keys/keystore.json +``` + +**For dAppNode deployments:** +- Upload `key1.json` to the dAppNode keystore folder +- Rename it to `keystore.json` + +:::tip Keep the Public Keystore Local +Keep `key1_staker_output.json` on your local machine - you'll need it for registration on the staking dashboard. **Do not upload this to your server.** +::: + +### Step 5: Start Your Node + +Start your sequencer node following the [Sequencer Setup guide](../setup/sequencer-setup.md). + +When your node starts successfully, you'll see output similar to: + +``` +Started validator with addresses: 0x8E76a8B8D66E0A56E241F2768fD2ad4eba07E565, 0x2037b472537a4246B1A7325f327028EF450ba0Ef, 0x0c14593f7465DeDbb86d68982374BB05F4C60386, 0x4D213928988f0123f6b3B4A377F856812F08E831, 0x29f147Da38d5F66bB84e791969b365c796829c92 +``` + +These are your validator attester addresses - they match the addresses shown when you generated your keys. + +### Step 6: Register Your Validators + +Use the public keystore (`key1_staker_output.json`) to register your validators on the staking dashboard. See [Registering a Sequencer](../setup/registering-sequencer.md) for details. + +--- + +### Quick Setup Summary + +By following the recommended setup, you've accomplished: + +✅ **Generated a dedicated publisher address** with its own 24-word mnemonic +✅ **Created 5 validator identities** with a separate 12-word mnemonic +✅ **Configured all validators** to use the shared publisher for L1 transactions +✅ **Funded the publisher** with at least 0.3 ETH for gas costs +✅ **Uploaded the private keystore** (`key1.json`) to your sequencer node +✅ **Started your node** and verified validator addresses in the output +✅ **Ready to register** using the public keystore (`key1_staker_output.json`) + +**Two mnemonics to keep secure:** +1. **Publisher mnemonic** (24 words) - Recovers publisher private key +2. **Validator mnemonic** (12 words) - Recovers all 5 validator attester keys + +## Alternative: Single Validator Setup + +For testing or simpler setups, you can create a single validator that uses its attester key as the publisher. + +### Basic Single Validator + +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ +#if(testnet) + --gse-address 0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac \ +#else + --gse-address 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f \ +#endif + --l1-rpc-urls $ETH_RPC +``` + +This creates: +- One validator with attester keys +- No separate publisher (attester key used for publishing) +- Private keystore at `~/.aztec/keystore/keyN.json` +- Public keystore at `~/.aztec/keystore/keyN_staker_output.json` + +:::info When to Use Single Validator +Use single validator setup for: +- Testing and development +- Simple deployments with one sequencer identity +- When you don't need to isolate attester and publisher keys +::: + +## Understanding Keystore Structure + +### Private Keystore Format + +The private keystore (`key1.json`) contains sensitive private keys: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0x...", // Ethereum private key - sequencer identifier + "bls": "0x..." // BLS private key - signs proposals and attestations + }, + "publisher": ["0x..."], // Publisher private key(s) for L1 submissions + "feeRecipient": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x..." // Optional: custom address for L1 rewards + } + ] +} +``` + +**Field descriptions:** + +- **attester.eth**: Derives the address that serves as your sequencer's unique identifier +- **attester.bls**: Signs proposals and attestations, used for staking operations +- **publisher**: Array of private keys for submitting signed messages to L1 (pays gas) +- **feeRecipient**: L2 fee recipient (not currently used, set to all zeros) +- **coinbase**: L1 block reward recipient (optional, defaults to attester address) + +### Public Keystore Format + +The public keystore (`key1_staker_output.json`) contains only public information safe to share: + +```json +[ + { + "attester": "0xYOUR_ATTESTER_ADDRESS", + "publicKeyG1": { + "x": "0x...", + "y": "0x..." + }, + "publicKeyG2": { + "x0": "0x...", + "x1": "0x...", + "y0": "0x...", + "y1": "0x..." + }, + "proofOfPossession": { + "x": "0x...", + "y": "0x..." + } + } +] +``` + +This file is used for registration on the staking dashboard and contains no private keys. + +## Advanced Options + +### Providing Your Own Mnemonic + +For deterministic key generation or to recreate keys from an existing mnemonic: + +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ +#if(testnet) + --gse-address 0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac \ +#else + --gse-address 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f \ +#endif + --l1-rpc-urls $ETH_RPC \ + --mnemonic "your existing twelve word mnemonic phrase here" \ + --count 5 \ + --publishers 0x7988a4a779f058a0 +``` + +This regenerates the same validators if you've used this mnemonic before, or creates new ones at the next derivation indices. + +### BLS Keys and the Proof-of-Possession Gas Cap + +When a validator registers, L1 checks the proof of possession of its BLS key within a fixed gas cap. The cost of that +check depends on the key, and for a small fraction of keys it could exceed the cap, so those keys could never register. +`aztec validator-keys new`, `aztec validator-keys add` and `aztec generate-bls-keypair` estimate that cost for every BLS +key they derive, locally and without RPC calls. The v6 upgrade sets the GSE's `proofOfPossessionGasLimit` to 300,000 gas, +and the default budget is 270,000 gas, that cap minus a 10% margin. About 1 key in 170,000 estimates over the budget: + +- **No flag**: the command keeps the key at the usual derivation path. If its check could exceed the gas cap, the command + stops with an error before writing anything and suggests one of the flags below. If the command generated and printed + the mnemonic itself, re-running without `--mnemonic` generates a new one; pass the mnemonic back with `--mnemonic` to + keep it. +- **`--bls-key-derivation-v6`**: a key whose check could exceed the gas cap is replaced by the next candidate, derived from + the same mnemonic at `<path>/1`, `<path>/2`, and so on, until one fits. Keys that already fit are unchanged, and the + ETH attester and publisher keys never change. +- **`--bls-key-derivation-max-gas <gas>`**: a stricter budget of your choice, between 137,790 (the lowest estimate any key + can have) and 270,000. With `--bls-key-derivation-v6`, candidates are tried until one fits this budget; without it, a key + over the budget is an error. +- **`--skip-bls-key-gas-check`**: the command keeps the key at the usual path without checking it. Use this to + regenerate a key created by an earlier CLI version, for example one that is already registered. You cannot combine it + with `--bls-key-derivation-v6` or `--bls-key-derivation-max-gas`. + +When a key comes from a candidate other than 0, the summary shows its full derivation path, for example +`bls derivation path: m/12381/3600/0/0/384795/1 (candidate 1)`, followed by the max gas when +`--bls-key-derivation-max-gas` was set. With `--json`, the output lists these keys under `blsKeyCandidates`. Encrypted +BLS keystore files record the derivation path of each key the command derived in their `path` field. When +`aztec validator-keys add --password` encrypts plaintext BLS keys that were already in the keystore, it cannot know where +they came from, so it leaves their `path` empty. + +Governance can change `proofOfPossessionGasLimit` at any time. `aztec validator-keys staker` and +`aztec validator-keys new --staker-output` read the current value from the GSE and stop with an error naming every +attester whose BLS key is estimated to need more gas, before writing any output. To replace those BLS keys, regenerate +the validators with `aztec validator-keys new`, the same mnemonic or IKM and indices, and `--bls-key-derivation-v6`. If +the current limit minus 10% is below 270,000, also pass `--bls-key-derivation-max-gas` set to that value. The error +names the exact flags. + +:::warning Regenerating keys +To regenerate keys from your mnemonic, run the command with the same `--bls-key-derivation-v6` and +`--bls-key-derivation-max-gas` values you used to create them. A different max gas can select a different candidate. +Without `--bls-key-derivation-v6`, the command stops with an error for a key that came from a later candidate; with +`--skip-bls-key-gas-check`, it returns the original candidate 0 key instead. +::: + +### Custom Output Location + +Specify custom directory and filename: + +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ +#if(testnet) + --gse-address 0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac \ +#else + --gse-address 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f \ +#endif + --l1-rpc-urls $ETH_RPC \ + --count 5 \ + --publishers 0x7988a4a779f058a0 \ + --data-dir ~/my-sequencer/keys \ + --file sequencer1.json +``` + +This creates keystores at: +- `~/my-sequencer/keys/sequencer1.json` (private keystore) +- `~/my-sequencer/keys/sequencer1_staker_output.json` (public keystore) + +**Default behavior** (if you don't specify `--data-dir` or `--file`): +- **Directory**: `~/.aztec/keystore/` +- **Filename**: `key1.json`, `key2.json`, etc. (auto-increments) + +### Encrypted Keystore Passwords + +Use `--password`, `--password-file`, or `AZTEC_KEYSTORE_PASSWORD` to write encrypted ETH JSON V3 and BLS +EIP-2335 keystore files with a shared password. To use different passwords, provide role-specific sources such as +`--eth-password` and `--bls-password`. + +```bash +export AZTEC_KEYSTORE_PASSWORD='your-secure-password' +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --count 5 \ + --publishers 0x7988a4a779f058a0 \ + --encrypted-keystore-dir ~/.aztec/keystore/encrypted +``` + +For each key type, role-specific sources take precedence over shared sources: `--eth-password`, +`--eth-password-file`, and `AZTEC_ETH_KEYSTORE_PASSWORD` for ETH keystores; `--bls-password`, +`--bls-password-file`, and `AZTEC_BLS_KEYSTORE_PASSWORD` for BLS keystores. Shared sources use `--password`, +`--password-file`, then `AZTEC_KEYSTORE_PASSWORD`. Passwords cannot be empty. When the command generates a mnemonic +while writing encrypted keystores, it does not print the mnemonic to stdout; provide `--mnemonic` if you need deterministic +recovery. + +## Verifying Your Keystore + +Verify the keystore is valid JSON: + +```bash +cat ~/.aztec/keystore/key1.json | jq . +``` + +Check validator count: + +```bash +jq '.validators | length' ~/.aztec/keystore/key1.json +``` + +Verify BLS keys are present: + +```bash +jq '.validators[0].attester.bls' ~/.aztec/keystore/key1.json +``` + +Extract attester addresses: + +```bash +# Get attester ETH private key (to derive address) +jq -r '.validators[0].attester.eth' ~/.aztec/keystore/key1.json +``` + +## Troubleshooting + +If you encounter issues during keystore creation or management, see the **[Troubleshooting and Best Practices Guide](./troubleshooting.md)** for: + +- Keystore creation issues (RPC, permissions, invalid JSON, legacy BLS keys) +- Runtime and operational issues (node startup, remote signers, nonce conflicts) +- Comprehensive security best practices +- Complete CLI reference + +## Next Steps + +Now that you've created your keystores: + +### For Sequencer Operators + +1. **Fund publisher addresses** - At least 0.1 ETH per validator +2. **Set up your node** - See [Sequencer Management](../setup/sequencer-setup.md) +3. **Register validators** - Use the public keystore with the staking dashboard +4. **Monitor operations** - Track attestations and publisher balance + +### Advanced Configurations + +- **[Advanced Keystore Patterns](./advanced-patterns.md)** - Multiple validators, high availability, remote signers +- **[Key Storage Methods](./storage-methods.md)** - Encrypted keystores, HSMs, key management systems +- **[Troubleshooting and Best Practices](./troubleshooting.md)** - Common issues, security best practices, and CLI reference + +### Getting Help + +- Review the [Operator FAQ](../operator-faq.md) for common questions +- Join the [Aztec Discord](https://discord.gg/aztec) for operator support +- Check the [CLI reference](../reference/cli-reference.md) for all available commands diff --git a/docs/lines/v6/docs-operate/operators/keystore/examples.md b/docs/lines/v6/docs-operate/operators/keystore/examples.md new file mode 100644 index 00000000000..ae9aa92a0bc --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/keystore/examples.md @@ -0,0 +1,322 @@ +--- +draft: true +title: Complete examples +description: Real-world keystore configuration examples for common deployment scenarios. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/node-keystore/src/schemas.ts", "labs/yarn-project/node-keystore/src/types.ts"] +--- + +## Overview + +This page provides complete, ready-to-use keystore configurations for common deployment scenarios. + +## Development and testing + +**Simple local setup** - Single sequencer with inline keys: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xef17bcb86452f3f6a73678c01bee757e9d46d1cd0050f043c10cfc953b17bad2", + "bls": "0x20f2f5989b66462b39229900948c7846403768fec5b76d1c2937d64e04aac4b9" + }, + "feeRecipient": "0x0987654321098765432109876543210987654321098765432109876543210987" + } + ] +} +``` + +**Development with mnemonic** - Use test mnemonic for local development: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "mnemonic": "test test test test test test test test test test test junk", + "addressIndex": 0 + }, + "publisher": { + "mnemonic": "test test test test test test test test test test test junk", + "addressIndex": 1 + }, + "feeRecipient": "0x0987654321098765432109876543210987654321098765432109876543210987" + } + ] +} +``` + +## Production configurations + +**Small sequencer** - Remote signer with dedicated publisher: + +```json +{ + "schemaVersion": 1, + "remoteSigner": "https://signer.example.com:8080", + "validators": [ + { + "attester": { + "eth": "0x1234567890123456789012345678901234567890", + "bls": "0x2345678901234567890123456789012345678901" + }, + "publisher": "0x3456789012345678901234567890123456789012", + "coinbase": "0x4567890123456789012345678901234567890123", + "feeRecipient": "0x0987654321098765432109876543210987654321098765432109876543210987" + } + ] +} +``` + +**Medium sequencer** - Multiple publishers for resilience: + +```json +{ + "schemaVersion": 1, + "remoteSigner": "https://signer.example.com:8080", + "validators": [ + { + "attester": { + "eth": "0x1234567890123456789012345678901234567890", + "bls": "0x2345678901234567890123456789012345678901" + }, + "publisher": [ + "0x3456789012345678901234567890123456789012", + "0x4567890123456789012345678901234567890123", + "0x5678901234567890123456789012345678901234" + ], + "coinbase": "0x6789012345678901234567890123456789012345", + "feeRecipient": "0x0987654321098765432109876543210987654321098765432109876543210987" + } + ] +} +``` + +**Large sequencer** - Multiple sequencers with shared infrastructure: + +```json +{ + "schemaVersion": 1, + "remoteSigner": "https://signer.example.com:8080", + "validators": [ + { + "attester": [ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222", + "0x3333333333333333333333333333333333333333" + ], + "publisher": [ + "0x4444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555", + "0x6666666666666666666666666666666666666666" + ], + "coinbase": "0x7777777777777777777777777777777777777777", + "feeRecipient": "0x0987654321098765432109876543210987654321098765432109876543210987" + } + ] +} +``` + +## Prover configurations + +**Simple prover** - Single key for identity and publishing: + +```json +{ + "schemaVersion": 1, + "prover": "0x1234567890123456789012345678901234567890123456789012345678901234" +} +``` + +**Production prover** - Dedicated publishers: + +```json +{ + "schemaVersion": 1, + "remoteSigner": "https://signer.example.com:8080", + "prover": { + "id": "0x1234567890123456789012345678901234567890", + "publisher": [ + "0x2345678901234567890123456789012345678901", + "0x3456789012345678901234567890123456789012" + ] + } +} +``` + +**High-throughput prover** - Many publishers for parallel submission: + +```json +{ + "schemaVersion": 1, + "remoteSigner": "https://signer.example.com:8080", + "prover": { + "id": "0x1234567890123456789012345678901234567890", + "publisher": [ + "0x2345678901234567890123456789012345678901", + "0x3456789012345678901234567890123456789012", + "0x4567890123456789012345678901234567890123", + "0x5678901234567890123456789012345678901234", + "0x6789012345678901234567890123456789012345" + ] + } +} +``` + +## Infrastructure provider setups + +**Managed sequencers** - Separate keystores per client: + +Directory structure: +``` +/etc/aztec/keystores/ +├── client-a.json +├── client-b.json +└── client-c.json +``` + +**client-a.json:** +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xCLIENT_A_ATTESTER_ETH_KEY", + "bls": "0xCLIENT_A_ATTESTER_BLS_KEY" + }, + "publisher": ["0xPUBLISHER_1", "0xPUBLISHER_2"], + "coinbase": "0xCLIENT_A_COINBASE", + "feeRecipient": "0xCLIENT_A_FEE_RECIPIENT" + } + ] +} +``` + +**client-b.json:** +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xCLIENT_B_ATTESTER_ETH_KEY", + "bls": "0xCLIENT_B_ATTESTER_BLS_KEY" + }, + "publisher": ["0xPUBLISHER_1", "0xPUBLISHER_2"], + "coinbase": "0xCLIENT_B_COINBASE", + "feeRecipient": "0xCLIENT_B_FEE_RECIPIENT" + } + ] +} +``` + +Set `KEY_STORE_DIRECTORY=/etc/aztec/keystores/` to load all configurations. + +**Shared publisher pool:** + +```json +{ + "schemaVersion": 1, + "remoteSigner": "https://signer.example.com:8080", + "validators": [ + { + "attester": { + "eth": "0xSEQUENCER_1_ETH", + "bls": "0xSEQUENCER_1_BLS" + }, + "publisher": { + "mnemonic": "provider mnemonic phrase here...", + "addressCount": 10 + }, + "feeRecipient": "0xSEQUENCER_1_FEE_RECIPIENT" + }, + { + "attester": { + "eth": "0xSEQUENCER_2_ETH", + "bls": "0xSEQUENCER_2_BLS" + }, + "publisher": { + "mnemonic": "provider mnemonic phrase here...", + "addressCount": 10 + }, + "feeRecipient": "0xSEQUENCER_2_FEE_RECIPIENT" + } + ] +} +``` + +This derives 10 publisher addresses from a mnemonic, shared by all sequencers. + +## Mixed storage methods + +**Security-tiered setup** - Critical keys in remote signer, publishers local: + +```json +{ + "schemaVersion": 1, + "remoteSigner": "https://signer.example.com:8080", + "validators": [ + { + "attester": { + "eth": "0x1234567890123456789012345678901234567890", + "bls": "0x2345678901234567890123456789012345678901" + }, + "publisher": [ + "0x3456789012345678901234567890123456789012345678901234567890123456", + "0x4567890123456789012345678901234567890123456789012345678901234567" + ], + "coinbase": "0x5678901234567890123456789012345678901234", + "feeRecipient": "0x0987654321098765432109876543210987654321098765432109876543210987" + } + ] +} +``` + +Attester (critical) uses remote signer; publishers (operational) use inline keys. + +**Multi-region deployment** - Different remote signers per region: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "address": "0x1111111111111111111111111111111111111111", + "remoteSignerUrl": "https://us-signer.example.com:8080" + }, + "publisher": [ + { + "address": "0x2222222222222222222222222222222222222222", + "remoteSignerUrl": "https://us-signer.example.com:8080" + } + ], + "feeRecipient": "0x0987654321098765432109876543210987654321098765432109876543210987" + }, + { + "attester": { + "address": "0x3333333333333333333333333333333333333333", + "remoteSignerUrl": "https://eu-signer.example.com:8080" + }, + "publisher": [ + { + "address": "0x4444444444444444444444444444444444444444", + "remoteSignerUrl": "https://eu-signer.example.com:8080" + } + ], + "feeRecipient": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + ] +} +``` + +## Next steps + +- See [Troubleshooting](./troubleshooting.md) for common issues +- Return to [Advanced Configuration Patterns](./advanced-patterns.md) for more details +- Review [Key Storage Methods](./storage-methods.md) for storage options diff --git a/docs/lines/v6/docs-operate/operators/keystore/index.md b/docs/lines/v6/docs-operate/operators/keystore/index.md new file mode 100644 index 00000000000..09e5f12d845 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/keystore/index.md @@ -0,0 +1,90 @@ +--- +id: advanced_keystore_guide +displayed_sidebar: operatorsSidebar +title: Advanced Keystore Usage +description: Learn how to configure keystores with remote signers, mnemonics, JSON V3 keystores, and multiple publishers for enhanced security and flexibility. +references: ["labs/yarn-project/node-keystore/src/*", "labs/yarn-project/cli/src/cmds/validator_keys/*"] +--- + +## Overview + +The keystore manages private keys and addresses for your Aztec sequencer or prover. This guide covers advanced keystore configurations including secure key storage methods, multi-account setups, and production deployment patterns. + +## Prerequisites + +Before proceeding, you should: + +- Be familiar with running a sequencer or prover node +- Understand the basic keystore structure from the [sequencer setup guide](../setup/sequencer-setup.md) +- Have access to appropriate key management infrastructure (if using remote signers) + +## Understanding Keystore Roles + +The keystore manages different types of keys depending on your node type. Understanding these roles helps you configure the right keys for your needs. + +### Sequencer Keys + +When running a sequencer, you configure these keys and addresses: + +- **Attester** (required): Your sequencer's identity. This key signs block proposals and attestations. The corresponding Ethereum address uniquely identifies your sequencer on the network. +- **Publisher** (optional): Submits block proposals to L1. Defaults to using the attester key if not specified. Must be funded with at least 0.1 ETH. +- **Coinbase** (optional): Ethereum address that receives L2 block rewards on L1. Defaults to the attester address if not set. +- **Fee Recipient** (required): Aztec address that receives unburnt L2 transaction fees from blocks you produce. + +### Prover Keys + +Prover nodes use a simpler configuration: + +- **Prover ID**: Ethereum address identifying your prover and receiving rewards. +- **Publisher**: Submits proof transactions to L1. Must be funded with ETH for gas costs. + +### Slasher Keys + +If you're running a slasher to monitor the network: + +- **Slasher**: Key used to create slash payloads on L1 when detecting sequencer misbehavior. + +## What This Guide Covers + +This guide walks you through advanced keystore configurations in three parts: + +### 1. Key Storage Methods + +Learn about different ways to store and access private keys: + +- Inline private keys (for testing) +- Remote signers with Web3Signer (recommended for production Ethereum keys) +- JSON V3 encrypted keystores +- BIP44 mnemonic derivation + +See [Key Storage Methods](./storage-methods.md) for detailed instructions. + +### 2. Advanced Configuration Patterns + +Explore complex deployment scenarios: + +- Using multiple publisher accounts for load distribution +- Running multiple sequencers on a single node +- Infrastructure provider configurations +- High availability setups + +See [Advanced Configuration Patterns](./advanced-patterns.md) for examples. + +### 3. Troubleshooting + +Get help with common issues: + +- Keystore loading failures +- Key format validation +- Security best practices +- Permission problems + +See [Troubleshooting](./troubleshooting.md) for solutions. + +## Getting Started + +**First time creating a keystore?** Start with the [Creating Validator Keystores guide](./creating-keystores.md) to learn how to use the Aztec CLI to generate keystores for sequencers and provers. + +Once you have a basic keystore, explore the [Key Storage Methods](./storage-methods.md) guide to understand advanced options like remote signers and encrypted keystores. Then check out [Advanced Configuration Patterns](./advanced-patterns.md) for complex deployment scenarios. + +For production deployments, we strongly recommend using remote signers or encrypted keystores instead of inline private keys. diff --git a/docs/lines/v6/docs-operate/operators/keystore/storage-methods.md b/docs/lines/v6/docs-operate/operators/keystore/storage-methods.md new file mode 100644 index 00000000000..c88cec96a1c --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/keystore/storage-methods.md @@ -0,0 +1,227 @@ +--- +title: Key storage methods +description: Learn about different methods for storing and accessing private keys in Aztec keystores, including inline keys, remote signers, JSON V3 keystores, and mnemonics. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/node-keystore/src/schemas.ts", "labs/yarn-project/node-keystore/src/loader.ts", "labs/yarn-project/node-keystore/src/signer.ts", "labs/yarn-project/node-keystore/src/keystore_manager.ts"] +--- + +## Overview + +The keystore supports four methods for storing and accessing private keys. These methods can be mixed within a single configuration. + +## Private keys (inline) + +The simplest method is to include private keys directly in the keystore. The `validator-keys new` command generates keystores in this format by default: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xef17bcb86452f3f6a73678c01bee757e9d46d1cd0050f043c10cfc953b17bad2", + "bls": "0x20f2f5989b66462b39229900948c7846403768fec5b76d1c2937d64e04aac4b9" + }, + "feeRecipient": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ] +} +``` + +Note that the attester field now contains both Ethereum (`eth`) and BLS (`bls`) private keys. Both are required for sequencer operation. + +:::warning Not for Production Use +Inline private keys are convenient for testing but should be avoided in production. Use remote signers or encrypted keystores for production deployments. +::: + +## Remote signers (Web3Signer) + +Remote signers keep private keys in a separate, secure signing service. This is the recommended approach for production environments for Ethereum keys. + +The keystore supports [Web3Signer](https://docs.web3signer.consensys.io/) endpoints for Ethereum keys. The keystore automatically detects whether a value is a private key or an address based on string length: + +- **66 characters** (`0x` + 64 hex characters): Interpreted as a private key (stored inline) +- **42 characters** (`0x` + 40 hex characters): Interpreted as an address and uses the nearest `remoteSignerUrl` + +:::warning BLS Keys Do Not Support Remote Signers +BLS keys must always be stored as private keys directly in the keystore. The keystore does not check `remoteSignerUrl` for BLS keys. Web3Signer's BLS support is designed for Ethereum consensus layer operations and is not compatible with Aztec's BLS key requirements. +::: + +Remote signers can be configured at three levels: + +**Global level** (applies to all ETH keys): + +```json +{ + "schemaVersion": 1, + "remoteSigner": "https://signer.example.com:8080", + "validators": [ + { + "attester": { + "eth": "0x1234567890123456789012345678901234567890", + "bls": "0x20f2f5989b66462b39229900948c7846403768fec5b76d1c2937d64e04aac4b9" + }, + "feeRecipient": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + ] +} +``` + +In this example, the Ethereum attester address (42 characters) is managed by the remote signer, while the BLS key (66 characters) is a private key stored directly in the keystore. + +**Validator (sequencer) block level** (applies to all ETH keys in a sequencer configuration): + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0x1234567890123456789012345678901234567890", + "bls": "0x20f2f5989b66462b39229900948c7846403768fec5b76d1c2937d64e04aac4b9" + }, + "feeRecipient": "0x1234567890123456789012345678901234567890123456789012345678901234", + "remoteSigner": "https://signer.example.com:8080" + } + ] +} +``` + +**Account level** (applies to a specific ETH key): + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": { + "address": "0x1234567890123456789012345678901234567890", + "remoteSignerUrl": "https://signer.example.com:8080" + }, + "bls": "0x20f2f5989b66462b39229900948c7846403768fec5b76d1c2937d64e04aac4b9" + }, + "feeRecipient": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + ] +} +``` + +### Client certificate authentication + +For remote signers requiring client certificates: + +```json +{ + "schemaVersion": 1, + "remoteSigner": { + "remoteSignerUrl": "https://signer.example.com:8080", + "certPath": "/path/to/client-cert.p12", + "certPass": "certificate-password" + }, + "validators": [...] +} +``` + +## JSON V3 encrypted keystores + +JSON V3 keystores provide standard Ethereum-compatible encrypted key storage. + +**Single file:** + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "path": "/path/to/keystore.json", + "password": "keystore-password" + }, + "feeRecipient": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + ] +} +``` + +**Directory of keystores:** + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0x1234567890123456789012345678901234567890123456789012345678901234", + "bls": "0x2345678901234567890123456789012345678901234567890123456789012345" + }, + "publisher": { + "path": "/path/to/keystores/", + "password": "shared-password" + }, + "feeRecipient": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + ] +} +``` + +All `.json` files in the directory will be loaded using the provided password. + +## Mnemonics (BIP44 derivation) + +Mnemonics derive multiple keys from a single seed phrase using [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) paths. + +**Single key** (default path `m/44'/60'/0'/0/0`): + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0x1234567890123456789012345678901234567890123456789012345678901234", + "bls": "0x2345678901234567890123456789012345678901234567890123456789012345" + }, + "publisher": { + "mnemonic": "test test test test test test test test test test test junk" + }, + "feeRecipient": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + ] +} +``` + +**Multiple sequential keys:** + +```json +{ + "publisher": { + "mnemonic": "test test test test test test test test test test test junk", + "addressCount": 4 + } +} +``` + +Generates 4 keys at paths `m/44'/60'/0'/0/0` through `m/44'/60'/0'/0/3`. + +**Custom derivation paths:** + +```json +{ + "publisher": { + "mnemonic": "test test test test test test test test test test test junk", + "accountIndex": 5, + "addressIndex": 3, + "addressCount": 2 + } +} +``` + +:::warning Not for Production Use +Mnemonics are convenient for testing but should be avoided in production. Use remote signers or encrypted keystores for production deployments. +::: + +## Next steps + +- Learn about [Advanced Configuration Patterns](./advanced-patterns.md) +- See [Troubleshooting](./troubleshooting.md) if you encounter issues diff --git a/docs/lines/v6/docs-operate/operators/keystore/troubleshooting.md b/docs/lines/v6/docs-operate/operators/keystore/troubleshooting.md new file mode 100644 index 00000000000..3d9b322b3b1 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/keystore/troubleshooting.md @@ -0,0 +1,464 @@ +--- +title: Troubleshooting and Best Practices +description: Common issues, troubleshooting steps, security best practices, and CLI reference for keystore configuration and operation on the Aztec network. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/cli/src/cmds/validator_keys/*", "labs/yarn-project/node-keystore/src/*"] +--- + +## Keystore Creation Issues + +### Missing fee-recipient Flag + +**Error message:** +``` +error: required option '--fee-recipient <address>' not specified +``` + +**Solution:** The CLI requires the `--fee-recipient` flag. Use the zero address: + +```bash +--fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 +``` + +### RPC Connection Issues + +**Error message:** +``` +Error: HTTP request failed +``` + +**Solutions:** +- Verify `$ETH_RPC` is set correctly: `echo $ETH_RPC` +- Test RPC connectivity: `cast block-number --rpc-url $ETH_RPC` +- Try a different RPC provider if the current one is rate-limited + +### Permission Denied + +**Error message:** +``` +Error: permission denied +``` + +**Solution:** Ensure you have write permissions for the target directory: + +```bash +mkdir -p ~/.aztec/keystore +chmod 755 ~/.aztec/keystore +``` + +### Invalid Keystore JSON + +**Error:** Node fails to load keystore or CLI rejects keystore file + +**Solutions:** +- Validate JSON syntax: `jq . ~/.aztec/keystore/key1.json` +- Ensure all required fields are present +- Check that publisher is an array: `["0x..."]` not `"0x..."` +- Verify private keys are 64-character hex strings (with or without `0x` prefix) + +### BLS Key Proof-of-Possession Check Over the Gas Budget + +**Error:** `The proof-of-possession check of the BLS key derived at m/12381/3600/0/0/<n> is estimated at <gas> gas, above the 270000 budget of BLS key derivation v6, so it could exceed the L1 gas cap.` With `--bls-key-derivation-max-gas`, the error names that budget instead. + +**Cause:** The proof-of-possession check of this BLS key could cost more gas than the L1 cap allows, so its registration +could fail. + +**Solutions:** +- For a new key, re-run with `--bls-key-derivation-v6`. The command derives a replacement BLS key from the same mnemonic + for that validator and prints its derivation path. Keep using `--bls-key-derivation-v6` (and the same + `--bls-key-derivation-max-gas`, if you set one) whenever you regenerate these keys. +- To regenerate a key you already registered with an earlier CLI version, re-run with `--skip-bls-key-gas-check`. It + keeps the original key without checking it. +- If you did not pass `--mnemonic`, the command generated a new mnemonic, and re-running generates another one. If the + command printed it, pass it back with `--mnemonic` to keep it. + +See [BLS Keys and the Proof-of-Possession Gas Cap](./creating-keystores.md#bls-keys-and-the-proof-of-possession-gas-cap). + +### BLS Key Over the GSE Proof-of-Possession Gas Limit + +**Error:** `Registering these validators could fail: the GSE at <address> gives each proof-of-possession check <limit> gas, and the BLS keys of these attesters are estimated to need more: ...` + +**Cause:** `aztec validator-keys staker` or `aztec validator-keys new --staker-output` read the GSE's current +`proofOfPossessionGasLimit`, and some BLS keys are estimated to need more gas than that, so their registration could +fail. This happens when governance set a limit below the one the keys were checked against, or for keys created with +`--skip-bls-key-gas-check`. + +**Solution:** Regenerate the listed validators with `aztec validator-keys new`, the same mnemonic or IKM and indices, +and the flags the error names: `--bls-key-derivation-v6`, plus `--bls-key-derivation-max-gas` at 10% below the limit when +that is below 270,000. This replaces their BLS keys and keeps their ETH keys. `aztec validator-keys add` appends new +validators instead, so it does not replace these keys. + +### Legacy BLS Key Derivation (2.1.4 Users) + +**Issue:** Need to regenerate keys that were created with CLI version 2.1.4 or earlier + +Version 2.1.5 changed the BLS key derivation path, which means keys generated from the same mnemonic produce different results. This affects users who: +- Generated keys with version 2.1.4 using `--count` parameter +- Used `--account-index` explicitly in version 2.1.4 +- Need to regenerate keys from mnemonic that are already registered in the GSE contract + +**The derivation path change:** +- **2.1.4**: `m/12381/3600/0/0/0`, `m/12381/3600/1/0/0`, `m/12381/3600/2/0/0` +- **2.1.5+**: `m/12381/3600/0/0/0`, `m/12381/3600/0/0/1`, `m/12381/3600/0/0/2` + +**Solution: Use the --legacy flag** + +If you generated keys with version 2.1.4 and need to regenerate them from your mnemonic, use the `--legacy` flag: + +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ +#if(testnet) + --gse-address 0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac \ +#else + --gse-address 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f \ +#endif + --l1-rpc-urls $ETH_RPC \ + --mnemonic "your twelve word mnemonic phrase here" \ + --count 5 \ + --legacy +``` + +The `--legacy` flag uses the 2.1.4 derivation path to reproduce your original keys. + +:::warning When NOT to Use --legacy +Do NOT use the `--legacy` flag if: +- You're generating keys for the first time +- You generated keys with version 2.1.5 or later +- You didn't use `--count` or `--account-index` in version 2.1.4 + +Using `--legacy` unnecessarily will create keys with the old derivation path that won't match your newer registrations. +::: + +:::info Why This Matters +BLS keys are registered in the GSE (Governance Staking Escrow) contract and cannot be easily updated. If you regenerate keys with a different derivation path, they won't match what's registered on chain, and your sequencer won't be able to attest properly. +::: + +## Runtime and Operational Issues + +### "No validators found in keystore" + +**Symptoms**: Node fails to start with no sequencer configurations loaded + +**Causes**: +- Keystore file not found at specified path +- Invalid JSON syntax +- Missing required fields +- File permissions prevent reading + +**Solutions**: + +1. Verify keystore path: +```bash +ls -la $KEY_STORE_DIRECTORY +``` + +2. Validate JSON syntax: +```bash +cat keystore.json | jq . +``` + +3. Check required fields: +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "REQUIRED - Ethereum private key", + "bls": "REQUIRED - BLS private key" + }, + "feeRecipient": "REQUIRED - Aztec address" + } + ] +} +``` + +4. Fix file permissions: +```bash +chmod 600 keystore.json +chown aztec:aztec keystore.json +``` + +### "Failed to connect to remote signer" + +**Symptoms**: Node cannot reach Web3Signer endpoint + +**Causes**: +- Incorrect URL or port +- Network connectivity issues +- Certificate validation failures +- Remote signer not running + +**Solutions**: + +1. Test connectivity: +```bash +curl https://signer.example.com:8080/upcheck +``` + +2. Verify certificate: +```bash +openssl s_client -connect signer.example.com:8080 -showcerts +``` + +3. Check remote signer logs for authentication errors + +4. For self-signed certificates, ensure proper certificate configuration in keystore + +### "Insufficient funds for gas" + +**Symptoms**: Transactions fail with insufficient balance errors + +**Causes**: +- Publisher accounts not funded +- ETH balance depleted + +**Solutions**: + +1. Check publisher balances: +```bash +cast balance 0xPUBLISHER_ADDRESS --rpc-url $ETHEREUM_HOST +``` + +2. Fund publisher accounts with ETH + +3. Set up automated balance monitoring and alerts + +### "Nonce too low" or "Replacement transaction underpriced" + +**Symptoms**: Transaction submission failures related to nonces + +**Causes**: +- Multiple nodes using same publisher key +- Publisher key reused across keystores +- Transaction pool issues + +**Solutions**: + +1. **Never share publisher keys across multiple running nodes** + +2. If you must use the same key, ensure only one node is active at a time + +3. Clear pending transactions if safe to do so + +### "Keystore file not loaded" + +**Symptoms**: Only some keystores load from a directory + +**Causes**: +- Invalid JSON in some files +- Incorrect file extensions +- Schema version mismatch + +**Solutions**: + +1. Check all files in directory: +```bash +for file in /path/to/keystores/*.json; do + echo "Checking $file" + jq . "$file" || echo "Invalid JSON in $file" +done +``` + +2. Ensure all files use `.json` extension + +3. Verify `schemaVersion: 1` in all keystores + +### "Cannot decrypt JSON V3 keystore" + +**Symptoms**: Failed to load encrypted keystore files + +**Causes**: +- Incorrect password +- Corrupted keystore file +- Unsupported encryption algorithm + +**Solutions**: + +1. Verify password is correct + +2. Test decryption manually: +```bash +# Using ethereumjs-wallet or similar tool +``` + +3. Re-generate keystore if corrupted + +4. Ensure keystore was generated using standard tools (geth, web3.py, ethers.js) + +## Security Best Practices + +### Protecting Private Keys + +1. **Never commit keystores to version control** + - Add `keystore.json` to `.gitignore` + - Store keystores outside your project directory + +2. **Backup your mnemonic securely** + - Write it down offline + - Store in a secure location (not on the server) + - Consider using a hardware wallet or password manager + +3. **Limit keystore access** + ```bash + chmod 600 ~/.aztec/keystore/key1.json + ``` + +4. **Separate publisher from attester** + - Use dedicated publisher keys + - Keep attester keys offline when possible + - Use remote signers for production + +### Key Storage + +**DO:** +- Use remote signers (Web3Signer) for production deployments +- Store keystores in encrypted volumes +- Use JSON V3 keystores with strong passwords +- Restrict file permissions to 600 (owner read/write only) +- Keep backups of keystores in secure, encrypted locations + +**DON'T:** +- Commit keystores or private keys to version control +- Store unencrypted private keys on disk +- Share private keys between nodes +- Use the same keys across test and production environments +- Log private keys or keystore passwords + +### Publisher Key Management + +**DO:** +- Use separate publisher keys for each sequencer if possible +- Monitor publisher account balances with alerting +- Rotate publisher keys periodically +- Maintain multiple funded publishers for resilience +- Keep publisher keys separate from attester keys + +**DON'T:** +- Reuse publisher keys across multiple nodes +- Run out of gas in publisher accounts +- Use sequencer attester keys as publishers if avoidable +- Share publisher keys between sequencers + +### Remote Signer Security + +**DO:** +- Use TLS/HTTPS for all remote signer connections +- Implement client certificate authentication +- Run remote signers on isolated networks +- Monitor remote signer access logs +- Use firewall rules to restrict access + +**DON'T:** +- Use unencrypted HTTP connections +- Expose remote signers to the public internet +- Share remote signer endpoints between untrusted parties +- Disable certificate verification + +### Operational Security + +**DO:** +- Implement principle of least privilege for file access +- Use hardware security modules (HSMs) for high-value sequencers +- Maintain audit logs of key access and usage +- Test keystore configurations in non-production environments first +- Document your key management procedures + +**DON'T:** +- Run nodes as root user +- Store passwords in shell history or scripts +- Share attester keys between sequencers +- Neglect monitoring and alerting + +### Production Deployments + +For production, consider: +- **Hardware Security Modules (HSMs)** for key storage +- **Remote signers** to keep keys off the node +- **Encrypted keystores** with password protection +- **Key management systems** (HashiCorp Vault, AWS Secrets Manager) + +See [Key Storage Methods](./storage-methods.md) for advanced security patterns. + +## CLI Reference + +### validator-keys new + +Create a new keystore with validators: + +```bash +aztec validator-keys new [options] +``` + +**Common Options:** + +| Option | Description | Default | +|--------|-------------|---------| +| `--fee-recipient <address>` | L2 fee recipient (required) | None | +| `--mnemonic <phrase>` | 12 or 24 word mnemonic | Auto-generated | +| `--count <number>` | Number of validators to create | `1` | +| `--publisher-count <number>` | Publishers per validator | `0` | +| `--staker-output` | Generate public keystore for staking | `false` | +| `--gse-address <address>` | GSE contract address (required with --staker-output) | None | +| `--l1-rpc-urls <urls>` | L1 RPC endpoints (required with --staker-output) | None | +| `--legacy` | Use 2.1.4 BLS derivation path (only for regenerating old keys) | `false` | +| `--bls-key-derivation-v6` | Replace a BLS key whose proof-of-possession check could exceed the L1 gas cap with the next deterministic candidate | Keep the key, fail if over the budget | +| `--bls-key-derivation-max-gas <gas>` | Stricter budget for the estimated proof-of-possession gas (137,790 to 270,000; the L1 cap is 300,000 from the v6 upgrade) | 270,000 | +| `--skip-bls-key-gas-check` | Keep the first derived BLS key without checking its proof-of-possession gas | `false` | +| `--password <str>` | Shared password for encrypted ETH and BLS keystore files | `AZTEC_KEYSTORE_PASSWORD` if set | +| `--password-file <path>` | File containing the shared encrypted-keystore password | None | +| `--eth-password <str>` | Password for encrypted ETH keystore files | `AZTEC_ETH_KEYSTORE_PASSWORD`, then shared password | +| `--eth-password-file <path>` | File containing the ETH encrypted-keystore password | None | +| `--bls-password <str>` | Password for encrypted BLS keystore files | `AZTEC_BLS_KEYSTORE_PASSWORD`, then shared password | +| `--bls-password-file <path>` | File containing the BLS encrypted-keystore password | None | +| `--encrypted-keystore-dir <path>` | Directory for encrypted ETH and BLS keystore files | Keystore output directory | +| `--data-dir <path>` | Output directory | `~/.aztec/keystore` | +| `--file <name>` | Keystore filename | `key1.json` | + +For the complete list: + +```bash +aztec validator-keys new --help +``` + +### validator-keys add + +Add validators to an existing keystore: + +```bash +aztec validator-keys add <keystore-path> [options] +``` + +### validator-keys staker + +Generate staker output from an existing keystore: + +```bash +aztec validator-keys staker \ + --from <keystore-path> \ + --gse-address <address> \ + --l1-rpc-urls <url> \ + --output <output-file> +``` + +## Getting Help + +If you encounter issues not covered here: + +- Review the [Operator FAQ](../operator-faq.md) for common questions +- Join the [Aztec Discord](https://discord.gg/aztec) for operator support +- Check the [CLI reference](../reference/cli-reference.md) for all available commands +- Review node logs for specific error messages (redact private keys!) +- When asking for help, provide: + - Error messages (with private keys redacted) + - Keystore structure (anonymized) + - Node version and deployment environment + +## Related Documentation + +- **[Creating Keystores](./creating-keystores.md)** - Main guide for generating keystores +- **[Advanced Keystore Patterns](./advanced-patterns.md)** - Multiple validators, high availability, remote signers +- **[Key Storage Methods](./storage-methods.md)** - Encrypted keystores, HSMs, key management systems +- **[Sequencer Management](../setup/sequencer-setup.md)** - Operational guidance for running sequencers diff --git a/docs/lines/v6/docs-operate/operators/monitoring/grafana-setup.md b/docs/lines/v6/docs-operate/operators/monitoring/grafana-setup.md new file mode 100644 index 00000000000..c37a3f532a4 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/monitoring/grafana-setup.md @@ -0,0 +1,171 @@ +--- +title: Grafana Setup +description: Configure Grafana to visualize Aztec node metrics and set up alerts for monitoring your node's health. +references: ["labs/yarn-project/telemetry-client/src/metrics.ts"] +--- + +## Overview + +Grafana provides visualization and alerting for your metrics, allowing you to create custom dashboards and receive notifications when issues arise. + +## Prerequisites + +- Completed [Prometheus Setup](./prometheus-setup.md) +- Prometheus running and accessible at `http://prometheus:9090` + +## Setup Steps + +### Step 1: Add Grafana to Docker Compose + +Add Grafana to your `docker-compose.yml`: + +```yaml +services: + # ... existing services (otel-collector, prometheus, etc.) ... + grafana: + image: grafana/grafana:latest + container_name: aztec-grafana + ports: + - 3000:3000 + volumes: + - grafana-data:/var/lib/grafana + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + networks: + - aztec + restart: always + +volumes: + # ... existing volumes ... + grafana-data: + +networks: + aztec: + name: aztec +``` + +:::warning Admin Password Security +Change the default admin password (`GF_SECURITY_ADMIN_PASSWORD`) to a secure value for production deployments. +::: + +### Step 2: Start Grafana + +```bash +docker compose up -d grafana +``` + +### Step 3: Access Grafana + +1. Navigate to `http://localhost:3000` +2. Login with username `admin` and the password you set (default: `admin`) +3. You'll be prompted to change the password on first login + +### Step 4: Add Prometheus Data Source + +1. In the left sidebar, click **Connections** → **Data sources** +2. Click **Add data source** +3. Search for and select **Prometheus** +4. Configure: + - **Name**: Aztec Prometheus + - **URL**: `http://prometheus:9090` +5. Click **Save & Test** + +You should see a green success message confirming Grafana can connect to Prometheus. + +## Creating Dashboards + +### Option 1: Create a Basic Dashboard + +1. In the left sidebar, click **Dashboards** +2. Click **New** → **New Dashboard** +3. Click **Add visualization** +4. Select your **Aztec Prometheus** data source +5. In the query editor, enter a metric (explore available metrics using the autocomplete) +6. Customize the visualization type and settings +7. Click **Apply** +8. Click **Save dashboard** icon (top right) +9. Give your dashboard a name and click **Save** + +### Option 2: Import a Pre-built Dashboard + +If the Aztec community has created shared dashboards: + +1. Click **+** → **Import** +2. Enter dashboard ID or upload JSON file +3. Select **Aztec Prometheus** as the data source +4. Click **Import** + +### Recommended Dashboard Panels + +Example panels you can create (adjust metric names based on what's actually available): + +1. **Block Height Over Time**: Line graph tracking block sync progress +2. **Sync Rate**: Line graph showing blocks synced over time window (use `increase()` function) +3. **Peer Count**: Gauge showing P2P connections +4. **Memory Usage**: Line graph of `process_resident_memory_bytes` +5. **CPU Usage**: Line graph of `rate(process_cpu_seconds_total[5m])` + +## Setting Up Alerts + +Configure alerts to notify you of issues: + +### Step 1: Create an Alert Rule + +1. In the left sidebar, click **Alerting** (bell icon) +2. Click **Alert rules** → **New alert rule** +3. Configure your alert: + - **Query**: Select your Prometheus data source and metric (e.g., `aztec_archiver_block_height`) + - **Condition**: Define the threshold (e.g., `increase(aztec_archiver_block_height[15m]) == 0` to alert if no blocks in 15 minutes) + - **Evaluation interval**: How often to check (e.g., 1m) +4. Click **Save** + +### Step 2: Configure Contact Points + +1. Under **Alerting**, click **Contact points** +2. Click **Add contact point** +3. Choose your notification method: + - **Email**: Configure SMTP settings + - **Slack**: Add webhook URL + - **PagerDuty**: Add integration key + - **Webhook**: Custom HTTP endpoint +4. Click **Save** + +### Step 3: Create Notification Policies + +1. Under **Alerting**, click **Notification policies** +2. Click **New notification policy** +3. Define routing rules to send alerts to specific contact points +4. Click **Save** + +## Example Alert Rules + +### Node Sync Alert + +Alert if the node stops syncing blocks: + +- **Query**: `increase(aztec_archiver_block_height[15m])` +- **Condition**: `== 0` +- **Description**: Node has not synced any blocks in the last 15 minutes + +### High Memory Usage Alert + +Alert if memory usage exceeds threshold: + +- **Query**: `process_resident_memory_bytes` +- **Condition**: `> 8000000000` (8GB) +- **Description**: Node memory usage exceeds 8GB + +### Peer Connection Alert + +Alert if peer count drops too low: + +- **Query**: `aztec_peer_manager_peer_count_peers` +- **Condition**: `< 5` +- **Description**: Node has fewer than 5 peer connections + +## Next Steps + +- Explore the [Monitoring Overview](./) for troubleshooting and metrics reference +- Join the [Aztec Discord](https://discord.gg/aztec) to share dashboards with the community +- Configure additional notification channels for your alerts diff --git a/docs/lines/v6/docs-operate/operators/monitoring/index.md b/docs/lines/v6/docs-operate/operators/monitoring/index.md new file mode 100644 index 00000000000..f41f2be11d4 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/monitoring/index.md @@ -0,0 +1,62 @@ +--- +displayed_sidebar: operatorsSidebar +id: monitoring +title: Monitoring and Observability +description: Learn how to monitor your Aztec node with metrics, OpenTelemetry, Prometheus, and Grafana. +references: ["labs/yarn-project/telemetry-client/src/metrics.ts", "labs/yarn-project/telemetry-client/src/config.ts"] +--- + +## Overview + +This guide shows you how to set up monitoring and observability for your Aztec node using OpenTelemetry, Prometheus, and Grafana. Monitoring helps you maintain healthy node operations, diagnose issues quickly, and track performance over time. + +:::info Docker Compose Setup +This monitoring setup is designed to work with Docker Compose deployments of Aztec nodes. +::: + +## Architecture + +The monitoring stack uses three components working together: + +- **OpenTelemetry Collector**: Receives metrics from your Aztec node via OTLP protocol +- **Prometheus**: Stores and queries time-series metrics data +- **Grafana**: Visualizes metrics with dashboards and alerts + +Your Aztec node exports metrics to the OpenTelemetry Collector, which processes and exposes them in a format Prometheus can scrape. Prometheus stores the metrics as time-series data, and Grafana queries Prometheus to create visualizations and alerts. + +## Getting Started + +Follow these guides in order to set up your complete monitoring stack: + +1. [OpenTelemetry Collector Setup](./otel-setup.md) - Configure OTEL to receive metrics from your node +2. [Prometheus Setup](./prometheus-setup.md) - Set up Prometheus to store and query metrics +3. [Grafana Setup](./grafana-setup.md) - Configure Grafana for visualization and alerting +4. [Key Metrics Reference](./metrics-reference.md) - Understand the metrics your node exposes and create custom dashboards +5. [Complete Example and Troubleshooting](./troubleshooting.md) - Full Docker Compose configuration and troubleshooting help + +## Available Metrics Overview + +Your Aztec node exposes metrics through OpenTelemetry to help you monitor performance and health. The metrics available depend on your node type (full node, sequencer, or prover) and version. + +### Metric Categories + +Your node exposes metrics in these categories: + +- **Node Metrics**: Block height, sync status, peer count, and transaction processing +- **Sequencer Metrics**: Attestation activity, block proposals, and committee participation (sequencer nodes only) +- **Prover Metrics**: Job queue, proof generation, and agent utilization (prover nodes only) +- **System Metrics**: CPU, memory, disk I/O, and network bandwidth + +For detailed information about each metric, PromQL queries, and dashboard creation, see the [Key Metrics Reference](./metrics-reference.md). + +## Next Steps + +Once your monitoring stack is running: + +- Review the [Key Metrics Reference](./metrics-reference.md) to understand available metrics and PromQL queries +- Set up alerting rules in Prometheus for critical conditions +- Create custom dashboards tailored to your operational needs +- Configure notification channels (Slack, PagerDuty, email) in Grafana +- Join the [Aztec Discord](https://discord.gg/aztec) to share dashboards with the community + +For troubleshooting common monitoring issues, see the [Troubleshooting](./troubleshooting.md) guide. diff --git a/docs/lines/v6/docs-operate/operators/monitoring/metrics-reference.md b/docs/lines/v6/docs-operate/operators/monitoring/metrics-reference.md new file mode 100644 index 00000000000..5886b241233 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/monitoring/metrics-reference.md @@ -0,0 +1,546 @@ +--- +title: Key Metrics Reference +description: Comprehensive guide to understanding and using the metrics exposed by your Aztec node for monitoring and observability. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/telemetry-client/src/metrics.ts", "labs/yarn-project/telemetry-client/src/attributes.ts"] +--- + +## Overview + +Your Aztec node exposes metrics through OpenTelemetry to help you monitor performance, health, and operational status. This guide covers key metrics across node types and how to use them effectively. + +:::tip Discovering Metrics +Once your monitoring stack is running, you can discover available metrics in the Prometheus UI at `http://localhost:9090/graph`. Start typing in the query box to see autocomplete suggestions for metrics exposed by your node. +::: + +## Prerequisites + +- Complete monitoring stack setup following the [Monitoring Overview](./) +- Ensure Prometheus is running and scraping metrics from your OTEL collector +- Verify access to Prometheus UI at `http://localhost:9090` + +:::warning Metric Names May Vary +The exact metric names and labels in this guide depend on your node type, version, and configuration. Always verify the actual metrics exposed by your node using the Prometheus UI metrics explorer at `http://localhost:9090/graph`. Common prefixes: `aztec_archiver_*`, `aztec_sequencer_*`, `aztec_prover_*`, `process_*`. +::: + +:::info How metric names are formed +The node defines metrics in OpenTelemetry style with dotted names like `aztec.archiver.block_height`. When Prometheus scrapes them, two transformations apply, so the name you query is not the name in the source: + +1. Dots become underscores: `aztec.archiver.block_height` becomes `aztec_archiver_block_height`. +2. The unit is appended as a suffix. A metric with unit `eth` gains `_eth` (`aztec.l1_publisher.balance` becomes `aztec_l1_publisher_balance_eth`); unit `peers` gains `_peers` (`aztec.peer_manager.peer_count` becomes `aztec_peer_manager_peer_count_peers`). + +If a metric name in this guide returns nothing, type its prefix into the Prometheus explorer and read the exact exported name, including any unit suffix. +::: + +## Querying with PromQL + +Use Prometheus Query Language (PromQL) to query and analyze your metrics. Understanding these basics will help you read the alert rules throughout this guide. + +### Basic Queries + +```promql +# Instant vector - current value +aztec_archiver_block_height + +# Range vector - values over time +aztec_archiver_block_height[5m] +``` + +### Rate and Increase + +```promql +# Rate of change per second (for counters) +rate(aztec_sequencer_block_proposal_failed_count[5m]) + +# Blocks synced over time window (for gauges) +increase(aztec_archiver_block_height[1h]) + +# Derivative - per-second change rate of gauges +deriv(process_memory_usage[30m]) +``` + +### Arithmetic Operations + +Calculate derived metrics using basic math operators: + +```promql +# Calculate percentage (block proposal failure rate) +(increase(aztec_sequencer_slot_total_count[15m]) - increase(aztec_sequencer_slot_filled_count[15m])) +/ increase(aztec_sequencer_slot_total_count[15m]) + +# Convert a 0..1 ratio to a percentage scale +process_cpu_utilization * 100 +``` + +### Comparison Operators + +Filter and alert based on thresholds: + +```promql +# Greater than (70% of available CPU) +process_cpu_utilization > 0.7 + +# Less than +aztec_peer_manager_peer_count_peers < 5 + +# Equal to +increase(aztec_archiver_block_height[15m]) == 0 + +# Not equal to (no new blocks proposed in the window) +increase(aztec_sequencer_block_count[15m]) == 0 +``` + +### Time Windows + +Choose time windows based on metric behavior and alert sensitivity: + +- **Short windows** (`[5m]`, `[10m]`) - Detect immediate issues, sensitive to spikes +- **Medium windows** (`[15m]`, `[30m]`) - Balance between responsiveness and stability, recommended for most alerts +- **Long windows** (`[1h]`, `[2h]`) - Trend analysis, capacity planning, smooth out temporary fluctuations + +Example: `increase(aztec_archiver_block_height[15m])` checks if blocks were processed in the last 15 minutes - long enough to avoid false alarms from brief delays, short enough to catch real problems quickly. + +## Core Node Metrics + +Your node exposes these foundational metrics for monitoring blockchain synchronization and network health. Configure immediate alerting for these metrics in all deployments. + +### L2 Block Height Progress + +Track whether your node is actively processing new L2 blocks: + +- **Metric**: `aztec_archiver_block_height` +- **Description**: Current L2 block number the node has synced to + +**Alert rule**: +```yaml +- alert: L2BlockHeightNotIncreasing + expr: increase(aztec_archiver_block_height{aztec_status=""}[15m]) == 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Aztec node not processing L2 blocks" + description: "No L2 blocks processed in the last 15 minutes. Node may be stuck or out of sync." +``` + +### Peer Connectivity + +Track the number of active P2P peers connected to your node: + +- **Metric**: `aztec_peer_manager_peer_count_peers` +- **Description**: Number of outbound peers currently connected to the node + +**Alert rule**: +```yaml +- alert: LowPeerCount + expr: aztec_peer_manager_peer_count_peers < 5 + for: 10m + labels: + severity: warning + annotations: + summary: "Low peer count detected" + description: "Node has only {{ $value }} peers connected. Risk of network isolation." +``` + +### L1 Block Height Progress + +Monitor whether your node is seeing new L1 blocks: + +- **Metric**: `aztec_archiver_l1_block_height` +- **Description**: Latest L1 (Ethereum) block number seen by the node + +**Alert rule**: +```yaml +- alert: L1BlockHeightNotIncreasing + expr: increase(aztec_archiver_l1_block_height[15m]) == 0 + for: 10m + labels: + severity: warning + annotations: + summary: "Node not seeing new L1 blocks" + description: "No L1 block updates in 15 minutes. Check L1 RPC connection." +``` + +## Sequencer Metrics + +If you're running a sequencer node, monitor these metrics for consensus participation, block production, and L1 publishing. Configure alerting for critical operations. + +### L1 publisher ETH balance + +The node reads the balance of every publisher once a minute, whether or not it has sent anything, and prices a reference L1 transaction at current fees. Sequencers and prover nodes export: + +- **`aztec_l1_publisher_balance_eth`**: ETH balance of each publisher, labelled by `aztec_address_sender` and `aztec_l1_tx_scope` (`sequencer` or `prover`). +- **`aztec_l1_publisher_sends_affordable`**: how many reference transactions each publisher's balance covers at current L1 fees, with the same labels. It reads `0` while a publisher is skipped after an L1 rejection for insufficient funds. +- **`aztec_l1_publisher_required_balance_eth`**: ETH one reference transaction needs at current L1 fees, labelled by `aztec_l1_tx_scope`. +- **`aztec_l1_publisher_funder_balance_eth`**: ETH balance of the funding account, if you set up [automatic top-ups](../solo-sequencer/fund-accounts#optional-automatic-top-ups). + +For a sequencer, the reference transaction is the first checkpoint proposal of an epoch without an invalidation (1.75M gas and 6 blobs). Most proposals cost less, and a proposal that also invalidates a checkpoint costs about 1M gas more, so treat `sends_affordable` as an estimate of the proposals left rather than an exact count. For a prover node, it is an epoch proof submission (3.5M gas). Fisherman nodes and prover nodes with proof publishing disabled never send these transactions, so they export balances only. + +**Alert rules**: +```yaml +- alert: NoAffordablePublisher + expr: max by (service_instance_id, aztec_l1_tx_scope) (aztec_l1_publisher_sends_affordable) < 1 + labels: + severity: critical + annotations: + summary: "No L1 publisher can afford a transaction" + description: "No {{ $labels.aztec_l1_tx_scope }} publisher can afford its next L1 transaction at current fees. Proposals or proof submissions are being skipped. Fund a publisher now." + +- alert: LowL1PublisherRunway + expr: | + predict_linear(aztec_l1_publisher_balance_eth[1d], 3 * 86400) + < on (service_instance_id, aztec_l1_tx_scope) group_left + aztec_l1_publisher_required_balance_eth + for: 15m + labels: + severity: warning + annotations: + summary: "L1 publisher will run out of ETH within 3 days" + description: "At its burn rate over the last day, publisher {{ $labels.aztec_address_sender }} will not afford its next L1 transaction within 3 days. Top it up." + +- alert: LowL1PublisherBalance + expr: aztec_l1_publisher_balance_eth < 0.5 + for: 5m + labels: + severity: critical + annotations: + summary: "L1 publisher ETH balance critically low" + description: "Publisher balance is {{ $value }} ETH. Refill immediately to avoid transaction failures." +``` + +`NoAffordablePublisher` has no `for` delay: below 1, the node skips its proposals or proof submissions right away, so waiting only adds missed slots. + +`LowL1PublisherRunway` projects each publisher's balance 3 days ahead from its spending over the last day and compares it with the cost of one transaction at current fees. It warns based on time left, which a fixed balance or transaction count can't do, since the same balance can last an hour or a week depending on how often your node publishes and on L1 fees. Set the projection to however long your top-ups take. It needs a day of history, and a top-up in the last day flattens the projection, so it stays quiet just after a restart or a refill. + +Keep `LowL1PublisherBalance` as a backstop for those cases. It doesn't follow L1 fees, so size it against the worst-case gas price you expect. + +### Sequencer Liveness + +There is no single gauge that reports a sequencer "healthy/unhealthy" state. Observe liveness through the work the sequencer does: a running sequencer keeps building blocks and keeps its archiver advancing. Alert when block production stalls. + +- **Metric**: `aztec_sequencer_block_count` +- **Description**: Count of blocks this sequencer has built. A flat count over a full proposal window means the sequencer is not producing. + +**Alert rule**: +```yaml +- alert: SequencerNotProducing + expr: increase(aztec_sequencer_block_count[30m]) == 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Sequencer is not building blocks" + description: "No blocks built in the last 30 minutes. Check sequencer logs and confirm the node is in the committee." +``` + +:::note +A sequencer only builds blocks during slots it is assigned, so size the window to span several of your expected slots before alerting. If you want a node-level liveness signal that is independent of slot assignment, alert on `aztec_archiver_block_height` not advancing instead (see [L2 Block Height Progress](#l2-block-height-progress)). +::: + +### Block Proposal Failures + +Track failed block proposals by comparing slots to filled slots: + +- **Metrics**: `aztec_sequencer_slot_total_count` and `aztec_sequencer_slot_filled_count` +- **Description**: Tracks slots assigned to your sequencer versus slots successfully filled. Alert triggers when the failure rate exceeds 5% over 15 minutes. + +**Alert rule**: +```yaml +- alert: HighBlockProposalFailureRate + expr: | + (increase(aztec_sequencer_slot_total_count[15m]) - increase(aztec_sequencer_slot_filled_count[15m])) + / increase(aztec_sequencer_slot_total_count[15m]) > 0.05 + for: 5m + labels: + severity: warning + annotations: + summary: "High block proposal failure rate" + description: "{{ $value | humanizePercentage }} of block proposals are failing in the last 15 minutes." +``` + +### Blob Publishing Failures + +Track failures when publishing blobs to L1: + +- **Metric**: `aztec_l1_publisher_blob_tx_failure` +- **Description**: Number of failed blob transaction submissions to L1 + +**Alert rule**: +```yaml +- alert: BlobPublishingFailures + expr: increase(aztec_l1_publisher_blob_tx_failure[15m]) > 0 + for: 5m + labels: + severity: warning + annotations: + summary: "Blob publishing failures detected" + description: "{{ $value }} blob transaction failures in the last 15 minutes. Check L1 gas prices and publisher balance." +``` + +### Attestation Activity + +Track your sequencer's participation in the consensus protocol: + +- **Metrics**: Attestations submitted, attestation success rate, attestation timing +- **Use cases**: + - Verify your sequencer is actively participating + - Monitor attestation success rate + - Detect missed attestation opportunities + +### Block Proposals + +Monitor block proposal activity and success: + +- **Metrics**: Blocks proposed, proposal success rate, proposal timing +- **Use cases**: + - Track block production performance + - Identify proposal failures and causes + - Monitor proposal timing relative to slot schedule + +### Committee Participation + +Track your sequencer's involvement in consensus committees: + +- **Metrics**: Committee assignments, participation rate, duty execution +- **Use cases**: + - Verify your sequencer is assigned to committees + - Monitor duty execution completion rate + - Track committee participation over time + +### Performance Metrics + +Measure block production efficiency: + +- **Metrics**: Block production time, validation latency, processing throughput +- **Use cases**: + - Optimize block production pipeline + - Identify performance bottlenecks + - Compare performance against network averages + +### Own-validator slashing votes + +Detect the network voting to slash one of your own validators while there is still time to react. New in v5.2.0. + +- **Metric**: `aztec_slasher_own_validator_current_round_votes_max` +- **Description**: highest number of votes cast against any committee position held by your own validators in the current slashing round. Resets when a new round starts. +- **Compare against**: `aztec_slasher_quorum_size`, the number of votes a validator must receive in a round to be slashed. + +**Alert rule**: + +```yaml +- alert: OwnValidatorNearSlashQuorum + expr: aztec_slasher_own_validator_current_round_votes_max >= 0.5 * aztec_slasher_quorum_size + for: 1m + labels: + severity: critical + annotations: + summary: "Own validator approaching slash quorum" + description: "{{ $value }} votes cast against one of your validators this round. Check node logs for 'targeted by slashing vote'." +``` + +Related counters: `aztec_slasher_own_validator_targeted_count` (votes naming one of your validators), `aztec_slasher_own_validator_slashed_count` and `aztec_slasher_own_validator_slashed_amount` (executed slashes). Votes cast while your node is offline are not counted, so treat these as a floor. + +## Prover Metrics + +If you're running a prover node, track these metrics for proof generation workload and resource utilization. + +### Job Queue + +Monitor pending proof generation work: + +- **Metrics**: Queue depth, queue wait time, job age +- **Use cases**: + - Detect proof generation backlogs + - Capacity planning for prover resources + - Monitor job distribution across agents + +### Proof Generation + +Track proof completion metrics: + +- **Metrics**: Proofs completed, completion time, success rate, failure reasons +- **Use cases**: + - Monitor proof generation throughput + - Identify failing proof types + - Track generation time trends + +### Agent Utilization + +Monitor resource usage per proof agent: + +- **Metrics**: CPU usage per agent, memory allocation, GPU utilization (if applicable) +- **Use cases**: + - Optimize agent allocation + - Detect resource constraints + - Load balancing across agents + +### Throughput + +Measure proof generation capacity: + +- **Metrics**: Jobs completed per time period, proofs per second, utilization rate +- **Use cases**: + - Capacity planning + - Performance optimization + - SLA monitoring + +## JSON-RPC server metrics + +Every Aztec JSON-RPC server (node, prover broker, prover agent) reports request-level metrics. New in v5.2.0. Use these to spot a saturated or rejecting RPC endpoint. + +- `aztec_json_rpc_server_request_count`: completed JSON-RPC server requests +- `aztec_json_rpc_server_request_duration_ms`: request handler duration +- `aztec_json_rpc_server_request_validation_duration_ms`: request parameter validation duration +- `aztec_json_rpc_server_rejected_request_count`: requests rejected before handler dispatch (batch-size or body-size limits, bad params) +- `aztec_json_rpc_server_batch_count`: JSON-RPC batches received +- `aztec_json_rpc_server_batch_duration_ms`: batch processing duration +- `aztec_json_rpc_server_batch_size_requests`: number of requests per batch + +A rising `rejected_request_count` on a prover broker or agent usually means `RPC_MAX_BODY_SIZE` or `RPC_MAX_BATCH_SIZE` is too low for your payloads. + +## System Metrics + +Your node exposes standard infrastructure metrics through OpenTelemetry and the runtime environment. + +### CPU Usage + +Monitor process and system CPU utilization: + +- **Metric**: `process_cpu_utilization` +- **Description**: Fraction of available CPU the process is using, already normalized across all cores. A value of `1.0` means every available core is fully busy; `0.7` means 70% of total CPU capacity. This is a gauge, so query it directly without `rate()`. + +**Alert rules**: +```yaml +# process_cpu_utilization is a 0..1 ratio across all cores, so the threshold is +# a fraction of total capacity, not a core count. 0.7 = 70% of available CPU. +- alert: HighCPUUsage + expr: process_cpu_utilization > 0.7 + for: 10m + labels: + severity: warning + annotations: + summary: "High CPU usage detected" + description: "Node using {{ $value | humanizePercentage }} of available CPU. Consider scaling resources." +``` + +### Memory Usage + +Track RAM consumption: + +- **Metric**: `process_memory_usage` +- **Description**: Resident (physical) memory the process is using, in bytes + +**Alert rules**: +```yaml +- alert: HighMemoryUsage + expr: process_memory_usage > 8000000000 + for: 5m + labels: + severity: warning + annotations: + summary: "High memory usage detected" + description: "Memory usage is {{ $value | humanize1024 }}B. Consider increasing available RAM or investigating memory leaks." +``` + +**Additional monitoring**: +- Track memory growth rate to detect leaks +- Monitor garbage collection metrics for runtime efficiency + +### Disk I/O + +Monitor storage operations: + +- **Metrics**: Disk read/write rates, I/O latency, disk utilization +- **Use cases**: + - Identify I/O bottlenecks + - Plan storage upgrades + - Detect disk performance degradation + +### Network Bandwidth + +Track network throughput: + +- **Metrics**: Bytes sent/received, packet rates, connection counts +- **Use cases**: + - Monitor P2P bandwidth usage + - Capacity planning for network resources + - Detect unusual traffic patterns + +## Creating Dashboards in Grafana + +Organize your Grafana dashboards by operational focus to make monitoring efficient and actionable. For specific panel configurations and queries, see the [Grafana Setup](./grafana-setup.md) guide. + +### Dashboard Organization Strategy + +**Overview Dashboard** - At-a-glance health check +- L2 and L1 block height progression +- Peer connectivity status +- Critical alerts summary +- Resource utilization (CPU, memory) +- Use stat panels and gauges for current values +- Include time-series graphs for trends + +**Performance Dashboard** - Deep-dive into operational metrics +- Block processing rates and latencies +- Transaction throughput +- Network bandwidth utilization +- Query response times +- Use percentile graphs (p50, p95, p99) for latency metrics +- Compare current performance against historical baselines + +**Resource Dashboard** - Infrastructure monitoring +- CPU usage per core +- Memory allocation and garbage collection +- Disk I/O rates and latency +- Network packet rates +- Set threshold warning lines at 70-80% utilization +- Include growth trend projections + +**Role-Specific Dashboards** - Specialized metrics by node type +- **Sequencer Dashboard**: Block proposals, attestations, committee participation, L1 publisher balance +- **Prover Dashboard**: Job queue depth, proof generation rates, agent utilization, success rates +- Focus on metrics unique to the role's responsibilities +- Include SLA tracking and performance benchmarks + +## Best Practices + +### Metric Collection + +1. **Appropriate Scrape Intervals**: Balance data granularity against storage costs + - Standard: 15s for most metrics + - High-frequency: 5s for critical real-time metrics + - Low-frequency: 60s for slow-changing metrics + +2. **Retention Policy**: Configure based on operational needs + - Short-term: 7-15 days for detailed troubleshooting + - Long-term: 30-90 days for trend analysis + - Archive: Consider downsampling for longer retention + +3. **Label Cardinality**: Avoid high-cardinality labels that explode metric storage + - Good: `instance`, `node_type`, `region` + - Avoid: `user_id`, `transaction_hash`, `timestamp` + +### Monitoring Strategy + +1. **Layered Monitoring**: Monitor at multiple levels + - Infrastructure: CPU, memory, disk, network + - Application: Block height, peers, throughput + - Business: Transaction success rate, user activity + +2. **Proactive Alerts**: Set alerts before problems become critical + - Use warning and critical thresholds + - Alert on trends, not just absolute values + - Reduce alert fatigue with proper tuning + +3. **Dashboard Discipline**: Keep dashboards focused and actionable + - Separate dashboards by role and concern + - Include relevant context in panel titles + - Add threshold lines and annotations + +## Next Steps + +- Explore advanced PromQL queries in the [Prometheus documentation](https://prometheus.io/docs/prometheus/latest/querying/basics/) +- Set up alerting rules following the [Prometheus alerting guide](https://prometheus.io/docs/alerting/latest/overview/) +- Configure notification channels in [Grafana](./grafana-setup.md) +- Return to [Monitoring Overview](./) +- Join the [Aztec Discord](https://discord.gg/aztec) to share dashboards with the community diff --git a/docs/lines/v6/docs-operate/operators/monitoring/otel-setup.md b/docs/lines/v6/docs-operate/operators/monitoring/otel-setup.md new file mode 100644 index 00000000000..4928b99a753 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/monitoring/otel-setup.md @@ -0,0 +1,127 @@ +--- +title: OpenTelemetry Collector Setup +description: Configure OpenTelemetry Collector to receive metrics from your Aztec node and export them to Prometheus. +references: ["labs/yarn-project/telemetry-client/src/config.ts", "labs/yarn-project/telemetry-client/src/otel.ts"] +--- + +## Overview + +The OpenTelemetry Collector receives metrics from your Aztec node and exports them to Prometheus for storage and analysis. + +## Prerequisites + +- A running Aztec node with Docker Compose +- Basic understanding of Docker networking + +## Setup Steps + +### Step 1: Create Configuration File + +Create an `otel-collector-config.yml` file in the same directory as your existing `docker-compose.yml`: + +```yaml +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + grpc: + endpoint: 0.0.0.0:4317 + +exporters: + prometheus: + endpoint: "0.0.0.0:8889" + metric_expiration: 5m + +processors: + batch: + +service: + pipelines: + metrics: + receivers: [otlp] + exporters: + - prometheus +``` + +This configuration: +- Receives metrics via OTLP (OpenTelemetry Protocol) on ports 4317 (gRPC) and 4318 (HTTP) +- Exports metrics to Prometheus format on port 8889 +- Uses batch processing for efficiency + +### Step 2: Add OTEL Collector to Docker Compose + +Add the following to your existing `docker-compose.yml` file: + +```yaml +services: + # ... existing services ... + otel-collector: + image: otel/opentelemetry-collector + container_name: aztec-otel + ports: + - 8888:8888 # OTEL collector metrics endpoint + - 8889:8889 # Prometheus exporter endpoint + - 4317:4317 # OTLP gRPC receiver + - 4318:4318 # OTLP HTTP receiver + volumes: + - ./otel-collector-config.yml:/etc/otel-collector-config.yml + command: >- + --config=/etc/otel-collector-config.yml + networks: + - aztec + restart: always +``` + +### Step 3: Configure Your Node to Export Metrics + +Configure your Aztec node to export metrics to the OTEL collector. + +**Step 3a: Add to .env file** + +Add these variables to your `.env` file: + +```bash +OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://otel-collector:4318/v1/metrics +``` + +**Step 3b: Update docker-compose.yml** + +Add these environment variables to your node's service in `docker-compose.yml`: + +```yaml +services: + aztec-node: # or aztec-sequencer, prover-node, etc. + # ... existing configuration ... + environment: + # ... existing environment variables ... + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: ${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT} +``` + +**Network configuration:** Since your node and OTEL collector share the same Docker Compose file and `aztec` network, use the service name `otel-collector` in the endpoint URL as shown above. + +### Step 4: Start Services + +```bash +# Start or restart all services +docker compose up -d +``` + +### Step 5: Verify Metrics Collection + +Check that metrics are being collected: + +```bash +# View OTEL collector logs +docker compose logs -f otel-collector + +# Query Prometheus endpoint +curl http://localhost:8889/metrics +``` + +You should see metrics in Prometheus format. + +## Next Steps + +- Proceed to [Prometheus Setup](./prometheus-setup.md) to configure metric storage and querying +- Return to [Monitoring Overview](./) diff --git a/docs/lines/v6/docs-operate/operators/monitoring/prometheus-setup.md b/docs/lines/v6/docs-operate/operators/monitoring/prometheus-setup.md new file mode 100644 index 00000000000..7c80db73699 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/monitoring/prometheus-setup.md @@ -0,0 +1,107 @@ +--- +title: Prometheus Setup +description: Configure Prometheus to scrape and store metrics from your Aztec node's OpenTelemetry Collector. +references: ["labs/yarn-project/telemetry-client/src/metrics.ts", "labs/yarn-project/telemetry-client/src/config.ts"] +--- + +## Overview + +Prometheus scrapes and stores the metrics exposed by the OTEL collector, providing a time-series database for querying and analysis. + +## Prerequisites + +- Completed [OpenTelemetry Collector Setup](./otel-setup.md) +- OTEL collector running and exposing metrics on port 8889 + +## Setup Steps + +### Step 1: Create Prometheus Configuration + +Create a `prometheus.yml` file: + +```yaml +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'aztec-node' + static_configs: + - targets: ['otel-collector:8889'] + labels: + instance: 'aztec-node-1' +``` + +If you're running multiple nodes, adjust the `instance` label to uniquely identify each node. + +### Step 2: Add Prometheus to Docker Compose + +Add Prometheus to your `docker-compose.yml`: + +```yaml +services: + # ... existing services (otel-collector, etc.) ... + + prometheus: + image: prom/prometheus:latest + container_name: aztec-prometheus + ports: + - 9090:9090 + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus-data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--storage.tsdb.retention.time=30d' + networks: + - aztec + restart: always + +volumes: + prometheus-data: +``` + +### Step 3: Start Prometheus + +```bash +docker compose up -d +``` + +### Step 4: Verify Prometheus + +Access Prometheus UI at `http://localhost:9090` and verify: +1. Go to **Status → Targets** to check that the `aztec-node` target is up +2. Go to **Graph** and query a metric (e.g., `aztec_archiver_block_height`) + +## Using Prometheus + +### Query Metrics + +Use the Prometheus UI to explore and query metrics: + +1. Navigate to `http://localhost:9090/graph` +2. Enter a metric name in the query box (use autocomplete to discover available metrics) +3. Click **Execute** to see the results +4. Switch between **Table** and **Graph** views + +### Example Queries + +```promql +# Current block height +aztec_archiver_block_height + +# Blocks synced over time window +increase(aztec_archiver_block_height[5m]) + +# Memory usage +process_resident_memory_bytes + +# CPU usage rate +rate(process_cpu_seconds_total[5m]) +``` + +## Next Steps + +- Proceed to [Grafana Setup](./grafana-setup.md) to configure visualization and alerting +- Return to [Monitoring Overview](./) diff --git a/docs/lines/v6/docs-operate/operators/monitoring/troubleshooting.md b/docs/lines/v6/docs-operate/operators/monitoring/troubleshooting.md new file mode 100644 index 00000000000..7cab7a93b62 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/monitoring/troubleshooting.md @@ -0,0 +1,153 @@ +--- +displayed_sidebar: operatorsSidebar +title: Complete Example and Troubleshooting +description: Complete Docker Compose example with all monitoring components and troubleshooting guide for common monitoring issues. +references: ["labs/yarn-project/telemetry-client/src/config.ts", "labs/yarn-project/telemetry-client/src/otel.ts", "labs/yarn-project/telemetry-client/src/metrics.ts"] +--- + +## Complete Docker Compose Example + +Here's a complete example with all monitoring components integrated with your Aztec node: + +```yaml +services: + # Your Aztec node (example for full node) + aztec-node: + image: "azteclabs/aztec:#release_version" + container_name: "aztec-node" + ports: + - ${AZTEC_PORT}:${AZTEC_PORT} + - ${P2P_PORT}:${P2P_PORT} + - ${P2P_PORT}:${P2P_PORT}/udp + volumes: + - ${DATA_DIRECTORY}:/var/lib/data + environment: + DATA_DIRECTORY: /var/lib/data + LOG_LEVEL: ${LOG_LEVEL} + ETHEREUM_HOSTS: ${ETHEREUM_HOSTS} + L1_CONSENSUS_HOST_URLS: ${L1_CONSENSUS_HOST_URLS} + P2P_IP: ${P2P_IP} + P2P_PORT: ${P2P_PORT} + AZTEC_PORT: ${AZTEC_PORT} + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: http://otel-collector:4318/v1/metrics + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --node + --network #release_network + networks: + - aztec + restart: always + + # OpenTelemetry Collector + otel-collector: + image: otel/opentelemetry-collector + container_name: aztec-otel + ports: + - 8888:8888 + - 8889:8889 + - 4317:4317 + - 4318:4318 + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml + command: >- + --config=/etc/otel-collector-config.yaml + networks: + - aztec + restart: always + + # Prometheus + prometheus: + image: prom/prometheus:latest + container_name: aztec-prometheus + ports: + - 9090:9090 + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus-data:/prometheus + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--storage.tsdb.retention.time=30d" + networks: + - aztec + restart: always + + # Grafana + grafana: + image: grafana/grafana:latest + container_name: aztec-grafana + ports: + - 3000:3000 + volumes: + - grafana-data:/var/lib/grafana + environment: + - GF_SECURITY_ADMIN_PASSWORD=your-secure-password + - GF_USERS_ALLOW_SIGN_UP=false + networks: + - aztec + restart: always + +volumes: + prometheus-data: + grafana-data: + +networks: + aztec: + name: aztec +``` + +This configuration includes: + +- Your Aztec node configured to export metrics to the OTEL collector +- OpenTelemetry Collector to receive and process metrics +- Prometheus to store time-series data with 30-day retention +- Grafana for visualization and alerting +- Persistent volumes for Prometheus and Grafana data +- All services on the same Docker network for easy communication + +## Troubleshooting + +### Metrics not appearing + +**Issue**: No metrics showing in Prometheus or Grafana. + +**Solutions**: + +- Verify OTEL collector is running: `docker compose ps otel-collector` +- Check OTEL collector logs: `docker compose logs otel-collector` +- Verify node is configured with correct OTEL endpoints +- Test OTEL collector endpoint: `curl http://localhost:8889/metrics` +- Ensure all containers are on the same Docker network + +### Prometheus target down + +**Issue**: Prometheus shows target as "down" in Status → Targets. + +**Solutions**: + +- Verify OTEL collector is running and exposing port 8889 +- Check Prometheus configuration in `prometheus.yml` +- Ensure target address is correct (use service name if in same Docker network) +- Review Prometheus logs: `docker compose logs prometheus` + +### Grafana cannot connect to Prometheus + +**Issue**: Grafana shows "Bad Gateway" or cannot query Prometheus. + +**Solutions**: + +- Verify Prometheus is running: `docker compose ps prometheus` +- Check data source URL in Grafana (should be `http://prometheus:9090`) +- Test Prometheus endpoint: `curl http://localhost:9090/api/v1/query?query=up` +- Ensure Grafana and Prometheus are on the same Docker network + +## Next Steps + +- Set up alerting rules in Prometheus for critical conditions +- Create custom dashboards for your specific monitoring needs +- Configure notification channels (Slack, PagerDuty, email) in Grafana +- Explore advanced PromQL queries for deeper insights +- Join the [Aztec Discord](https://discord.gg/aztec) to share dashboards with the community diff --git a/docs/lines/v6/docs-operate/operators/operator-faq.md b/docs/lines/v6/docs-operate/operators/operator-faq.md new file mode 100644 index 00000000000..5a62ad74b9b --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/operator-faq.md @@ -0,0 +1,465 @@ +--- +displayed_sidebar: operatorsSidebar +title: FAQs & Common Issues +description: Troubleshooting guide for common Aztec node operator issues including sync errors, RPC limits, and update procedures. +keywords: [aztec, sequencer, node, validator, setup] +tags: + - sequencer + - node + - tutorial + - infrastructure +references: ["labs/yarn-project/ethereum/src/config.ts", "labs/yarn-project/p2p/src/config.ts", "labs/yarn-project/node-keystore/src/schemas.ts", "labs/yarn-project/node-lib/src/config/index.ts"] +--- + +## Overview + +This guide addresses common issues node operators encounter when running Aztec nodes. Each entry includes the issue symptoms, possible causes, and step-by-step solutions. + +If your issue isn't listed here, visit the [Aztec Discord](https://discord.gg/aztec) in the `#operator-faq` channel for community support. + +## Node Sync Issues + +### SYNC_BLOCK Failed Error + +**Symptom**: You see this error in your node logs: + +``` +ERROR: world-state:database Call SYNC_BLOCK failed: Error: Can't synch block: block state does not match world state +``` + +**Cause**: Your local database state is corrupted or out of sync with the network. + +**Solution**: + +1. Stop your node: + + ```bash + docker compose down + ``` + +2. Remove the archiver data directory: + + ```bash + rm -rf ~/.aztec/v#release_version/data/archiver + ``` + +3. Restart your node: + + ```bash + docker compose up -d + ``` + +:::warning Data Loss and Resync +This process removes local state and requires full resynchronization. Consider using snapshot sync mode (`SYNC_MODE=snapshot`) to speed up recovery. See the [syncing best practices guide](./setup/syncing_best_practices) for more information. +::: + +### Error Getting Slot Number + +**Symptom**: Your logs show "Error getting slot number" related to beacon or execution endpoints. + +**Cause**: + +- **Beacon-related errors**: Failed to connect to your L1 consensus (beacon) RPC endpoint +- **Execution-related errors**: Failed to connect to your L1 execution RPC endpoint or reporting routine issue + +**Solutions**: + +1. **Verify L1 endpoint configuration**: + + - Check your `L1_CONSENSUS_HOST_URLS` setting points to your beacon node + - Check your `ETHEREUM_HOSTS` setting points to your execution client + - Ensure URLs are formatted correctly (e.g., `http://localhost:5052` for beacon) + +2. **Test endpoint connectivity**: + + ```bash + # Test beacon endpoint + curl [YOUR_BEACON_ENDPOINT]/eth/v1/beacon/headers + + # Test execution endpoint + curl -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + [YOUR_EXECUTION_ENDPOINT] + ``` + +3. **Verify L1 clients are synced**: + + - Check that your beacon node is fully synced + - Check that your execution client is fully synced + - Use `docker compose logs` or check L1 client logs for sync status + +4. **Check for rate limiting** (if using third-party RPC): + - See the "RPC and Rate Limiting" section below + - Consider using your own L1 node for better reliability + +## RPC and Rate Limiting + +### RPC Rate Limit or Quota Exceeded + +**Symptom**: Your logs show errors like: + +``` +Error: quota limit exceeded +Error: rate limit exceeded +Error: too many requests +``` + +**Cause**: Your RPC provider is throttling requests due to rate limits or quota restrictions. + +**Solutions**: + +1. **Register for an API key with your RPC provider**: + + - Most providers (Infura, Alchemy, QuickNode) offer higher limits with authenticated requests + - Update your configuration to include the API key in your RPC URL + - Example: `https://mainnet.infura.io/v3/YOUR_API_KEY` + +2. **Use your own L1 node** (recommended for sequencers): + + - Running your own Ethereum node eliminates rate limits entirely + - Provides better performance, reliability, and privacy + - See [Eth Docker's guide](https://ethdocker.com/Usage/QuickStart) for setup instructions + - Ensure you're running both execution and consensus clients + +3. **Configure multiple RPC endpoints for failover**: + - Aztec nodes support comma-separated RPC URLs + - Example: `ETHEREUM_HOSTS=https://rpc1.example.com,https://rpc2.example.com` + - The node will automatically fail over if one endpoint is unavailable + +:::tip Run Your Own L1 Infrastructure +Sequencer operators should always run their own L1 infrastructure to ensure reliability, avoid rate limits, and maintain optimal performance. Third-party RPC providers are suitable for testing but not recommended for production sequencer operations. +::: + +### Blob Retrieval Errors + +**Symptom**: Your logs show errors like: + +``` +Error: No blob bodies found +Error: Unable to get blob sidecar, Gateway Time-out (504) +``` + +**Cause**: Your beacon node endpoint is slow, overloaded, rate-limited, or not synced properly. + +**Solutions**: + +1. **Verify beacon endpoint configuration**: + + ```bash + # Check L1_CONSENSUS_HOST_URLS in your configuration + # Should point to your beacon node's API endpoint + ``` + +2. **Test beacon endpoint health**: + + ```bash + # Check if beacon node is responding + curl [YOUR_BEACON_ENDPOINT]/eth/v1/node/health + + # Check sync status + curl [YOUR_BEACON_ENDPOINT]/eth/v1/node/syncing + ``` + +3. **Ensure beacon node is fully synced**: + + - Check your beacon client logs + - Verify the sync status shows as synced + - A regular beacon serves blob sidecars only for recent blocks; a node that does not serve them at all needs supernode or semi-supernode mode (see [L1 RPC requirements](/operate/operators/concepts/l1-rpc)) + +4. **Run your own beacon node** (recommended): + - Using a third-party beacon endpoint may have rate limits + - Running your own provides better reliability and eliminates timeouts + - See the [prerequisites guide](./prerequisites.md) for L1 infrastructure setup + +## L1 Node Requirements + +### Do I Need an L1 Archive Node? + +No. You do not need an L1 archive node to run an Aztec node. Snapshot sync is the recommended approach and works with standard L1 full nodes. + +To use snapshot sync, set `SYNC_MODE=snapshot` in your configuration. + +### Consensus RPC and blob availability + +Your L1 consensus (beacon) RPC must serve **blob sidecars**. After Ethereum's Fusaka upgrade (PeerDAS), a regular beacon node no longer serves the blob sidecar endpoint, so an Aztec node pointed at one cannot sync. You need a beacon configured as a **supernode** (holds all data columns) or a **semi-supernode** (holds enough columns to reconstruct blobs, sufficient for an Aztec node). + +For the version and flag requirements, the load a node puts on an RPC, and the self-host versus hosted trade-off, see [L1 RPC requirements](/operate/operators/concepts/l1-rpc). + +## Funding and Resources + +### Insufficient L1 Funds + +**Symptom**: Your sequencer cannot publish blocks, and logs show: + +``` +Error: Insufficient L1 funds +Error: insufficient funds for gas * price + value +``` + +#if(testnet) +**Cause**: Your publisher address has run out of Sepolia ETH to pay for L1 gas. Publisher balance decays slowly, so this usually means you started small and let it drift to zero. + +**Solutions**: + +1. **Get Sepolia ETH from a faucet**: + + - [Sepolia Faucet](https://sepoliafaucet.com/) + - [Alchemy Sepolia Faucet](https://www.alchemy.com/faucets/ethereum-sepolia) + - [Infura Sepolia Faucet](https://www.infura.io/faucet/sepolia) + + For Aztec testnet tokens (TST), use the **[Aztec Testnet Faucet](https://testnet.aztec.network/)**. + +2. **Top up the publisher account**: +#else +**Cause**: Your publisher address has run out of ETH to pay for L1 gas. + +**Solutions**: + +1. **Top up the publisher account**: +#endif + + - The [funding calculator](./solo-sequencer/fund-accounts#fund-the-publisher) in Fund accounts converts a top-up amount into a runway estimate at current and elevated gas prices once you have scanned the network with your RPC + +2. **Check your publisher balance**: + + ```bash + # Check your publisher balance + cast balance [YOUR_PUBLISHER_ADDRESS] --rpc-url [YOUR_RPC_URL] + ``` + +3. **Check the node logs**: + - `No publisher can afford a proposal` (or `an epoch proof` on a prover node) and `Skipping checkpoint proposal` name the publisher to fund and how much ETH it is missing + - The `Publisher balances` line logged at startup lists every publisher's balance + +4. **Set a refill threshold and an alert**: + - Use `aztec_l1_publisher_balance_eth` and `aztec_l1_publisher_sends_affordable` from your Prometheus stack (see [Metrics reference](./monitoring/metrics-reference#l1-publisher-eth-balance) for example alert rules) + - The threshold should leave enough headroom to refill before the publisher dries up at the worst-case gas price you expect + - Or let the node refill it with [automatic top-ups](./solo-sequencer/fund-accounts#optional-automatic-top-ups) + +:::warning Slashing risk +A publisher that runs out of ETH cannot submit proposals. A single missed proposal is not slashable, but staying out of ETH long enough for the attester to miss almost all its duties over a full epoch hits the inactivity threshold, which is slashable. Set a refill threshold that leaves time to act under realistic L1 gas conditions. +::: + +## Updates and Maintenance + +#### Version-Specific Updates: + +To update to a specific version: + +```yaml +# Change the image tag from: +image: "azteclabs/aztec:latest" +# To: +image: "azteclabs/aztec:#release_version" +``` + +Then run: + +```bash +docker compose pull +docker compose down +docker compose up -d +``` + +:::tip Stay Informed About Updates +Join the [Aztec Discord](https://discord.gg/aztec) and follow the announcements channel to stay informed about new releases and required updates. +::: + +## Network and Connectivity + +### Port Forwarding Not Working + +**Symptom**: Your node cannot discover peers or shows "0 peers connected" in logs. + +**Cause**: Firewall rules or router configuration are blocking P2P connections. + +**Solutions**: + +1. **Verify your external IP address**: + + ```bash + curl ipv4.icanhazip.com + ``` + + Confirm this matches your `P2P_IP` configuration. + +2. **Test port connectivity**: + + ```bash + # From another machine, test if your P2P port is accessible + nc -zv [YOUR_EXTERNAL_IP] 40400 + ``` + +3. **Configure router port forwarding**: + + - Log into your router's admin interface + - Forward port 40400 (TCP and UDP) to your node's local IP address + - Save and restart router if needed + +4. **Check local firewall rules**: + + ```bash + # Linux: Allow P2P port through firewall + sudo ufw allow 40400/tcp + sudo ufw allow 40400/udp + + # Verify rules + sudo ufw status + ``` + +5. **Verify Docker network settings**: + - Ensure ports are properly mapped in docker-compose.yml + - Check that `P2P_PORT` environment variable matches the exposed ports + +## Other Common Issues + +### CodeError: Stream Reset + +**Symptom**: You occasionally see this error in logs: + +``` +CodeError: stream reset +``` + +**Cause**: Temporary P2P connection disruption. This is normal network behavior and occurs when peer connections are interrupted. + +**Impact**: This is safe to ignore. Your node automatically reconnects to peers and maintains network connectivity. + +**Action Required**: None. This is expected behavior in P2P networks. + +### Keystore Not Loading + +**Symptom**: Your sequencer fails to start with errors about invalid keys or missing keystore. + +**Cause**: Keystore file is improperly formatted, missing, or has incorrect permissions. + +**Solutions**: + +1. **Verify keystore.json format**: + + ```json + { + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xYOUR_ETH_PRIVATE_KEY_HERE", + "bls": "0xYOUR_BLS_PRIVATE_KEY_HERE" + }, + "publisher": ["0xYOUR_PUBLISHER_KEY_HERE"], + "coinbase": "0xYOUR_COINBASE_ADDRESS", + "feeRecipient": "0xYOUR_AZTEC_ADDRESS" + } + ] + } + ``` + +2. **Validate private key format**: + + - Keys should start with `0x` + - Keys should be 64 hexadecimal characters (plus the `0x` prefix) + - No spaces or extra characters + - The attester must contain both `eth` and `bls` keys + +3. **Check file permissions**: + + ```bash + # Ensure keystore is readable + chmod 600 ~/.aztec/keys/keystore.json + + # Verify ownership + ls -la ~/.aztec/keys/ + ``` + +4. **Verify keystore directory path**: + - Ensure `KEY_STORE_DIRECTORY` environment variable is set in your `.env` file + - Verify the volume mount in `docker-compose.yml` points to the correct directory + +For more information on keystore configuration and creation, see the [Creating Validator Keystores guide](./keystore/creating-keystores.md) and the [Advanced Keystore Usage guide](./keystore/index.md). + +### Docker Container Won't Start + +**Symptom**: Docker container crashes immediately after starting or won't start at all. + +**Cause**: Various issues including configuration errors, insufficient resources, or port conflicts. + +**Solutions**: + +1. **Check container logs**: + + ```bash + docker compose logs aztec-sequencer + ``` + + Look for specific error messages that indicate the problem. + +2. **Verify Docker resources**: + + - Ensure sufficient disk space: `df -h` + - Check Docker has adequate memory allocated (16GB+ recommended) + - Verify CPU resources are available + +3. **Check environment file format**: + + ```bash + # Verify .env file exists and is properly formatted + cat .env + + # No spaces around = signs + # No quotes around values (unless necessary) + ``` + +4. **Verify port availability**: + + ```bash + # Check if ports are already in use + lsof -i :8080 + lsof -i :40400 + ``` + +5. **Update Docker and Docker Compose**: + + ```bash + # Check versions + docker --version + docker compose version + + # Update if needed + sudo apt-get update && sudo apt-get upgrade docker-ce docker-compose-plugin + ``` + +6. **Try a clean restart**: + ```bash + docker compose down + docker compose pull + docker compose up -d + ``` + +## Getting Additional Help + +If you've tried the solutions above and are still experiencing issues: + +1. **Gather diagnostic information**: + + - Recent log output from your node + - Your configuration (remove private keys!) + - Aztec version you're running + - Operating system and hardware specs + +2. **Check existing issues**: + + - Browse the [Aztec GitHub issues](https://github.com/AztecProtocol/aztec-packages/issues) + - Search for similar problems and solutions + +3. **Ask for help**: + - Join the [Aztec Discord](https://discord.gg/aztec) + - Post in the `#operator-faq` or `#operator-support` channel + - Include your diagnostic information + - Be specific about what you've already tried + +## Next Steps + +- See [Reading your logs](./reference/reading-logs.md) for a catalogue of common log messages and which are safe to ignore +- Review [monitoring setup](./monitoring/index.md) to catch issues early with metrics and alerts +- Check the [CLI reference](./reference/cli-reference.md) for all configuration options +- Join the [Aztec Discord](https://discord.gg/aztec) for real-time operator support diff --git a/docs/lines/v6/docs-operate/operators/prerequisites.md b/docs/lines/v6/docs-operate/operators/prerequisites.md new file mode 100644 index 00000000000..5a220613a5a --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/prerequisites.md @@ -0,0 +1,144 @@ +--- +title: Prerequisites +description: Common prerequisites and requirements for running nodes on the Aztec network, including hardware, software, and network configuration. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/ethereum/src/config.ts", "labs/yarn-project/p2p/src/config.ts", "labs/yarn-project/cli/src/cmds/validator_keys/*"] +--- + +## Overview + +This guide covers the prerequisites and setup requirements for running nodes on the Aztec network. + +## Common Prerequisites + +The following prerequisites apply to all node types. + +### Operating System + +The node software can be run on any Unix system released after 2020. + +- Linux (common flavors) +- MacOS (ARM and intel) + +### Docker and Docker Compose + +Docker and Docker Compose are required for all node types. All Aztec nodes run in Docker containers managed by Docker Compose. + +**On Linux:** Install Docker Engine and Docker Compose separately: + +1. Install Docker: + +```bash +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh +``` + +2. Add your user to the docker group so `sudo` is not needed: + +```bash +sudo groupadd docker +sudo usermod -aG docker $USER +newgrp docker +# Test without sudo +docker run hello-world +``` + +3. Install Docker Compose by following the [Docker Compose installation guide](https://docs.docker.com/compose/install/). + +**On macOS:** Install [Docker Desktop](https://docs.docker.com/desktop/install/mac-install/), which includes both Docker and Docker Compose. + +### Aztec Toolchain + +The Aztec toolchain provides CLI utilities for key generation, validator registration, and other operational tasks. While not required for running nodes (which use Docker Compose), it is needed for: + +- Generating validator keystores and creating staking registration data (`aztec validator-keys`) +- Registering sequencers on L1 (`aztec add-l1-validator`) + +The toolchain requires Node.js 20.10 or later. Nodes themselves do not need Node.js on the host, since they run in Docker containers. Install Node.js using [nvm](https://github.com/nvm-sh/nvm): + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash +source ~/.nvm/nvm.sh +nvm install --lts +nvm alias default node +``` + +Install the Aztec toolchain using the official installer: + +```bash +VERSION=#release_version bash -i <(curl -sL https://install.aztec.network/#release_version) +``` + +:::note macOS users +macOS ships with an outdated version of Bash (v3.2) that is known to cause issues with the installer. Install a modern version with [Homebrew](https://brew.sh/): + +```bash +brew install bash +``` + +Even if you use zsh as your default shell, the installer explicitly invokes `bash`. If the installer still picks up the old version, add the Homebrew `bash` to your `$PATH` or [set it as your default shell](https://support.apple.com/en-gb/guide/terminal/trml113/mac). +::: + +### L1 Ethereum Node Access + +All Aztec nodes require access to Ethereum L1 node endpoints: + +- **Execution client endpoint** (e.g., Geth, Nethermind, Besu, Erigon) +- **Consensus client endpoint** (e.g., Prysm, Lighthouse, Teku, Nimbus) + +**Options:** +1. **Run your own L1 node** (recommended for best performance): + - Better performance and lower latency + - No rate limiting or request throttling + - Greater reliability and uptime control + - Enhanced privacy for your node operations + - See [Eth Docker's guide](https://ethdocker.com/Usage/QuickStart) for setup instructions + +2. **Use a third-party RPC provider**: + - Easier to set up initially + - May have rate limits and throttling + - Ensure the provider supports beacon apis + +:::tip High Throughput Required +Your L1 endpoints must support high throughput to avoid degraded node performance. +::: + +### Port Forwarding and Connectivity + +For nodes participating in the P2P network (full nodes, sequencers, provers), proper port configuration is essential: + +**Required steps:** +1. Configure your router to forward both UDP and TCP traffic on your P2P port (default: 40400) to your node's local IP address +2. Ensure your firewall allows traffic on the required ports: + - P2P port: 40400 (default, both TCP and UDP) + - HTTP API port: 8080 (default) +3. Set the `P2P_IP` environment variable to your external IP address +4. Verify the P2P port is accessible from the internet + +**Find your public IP address:** + +```bash +curl ipv4.icanhazip.com +``` + +**Verify port connectivity:** + +```bash +# For TCP traffic on port 40400 +nc -zv [YOUR_EXTERNAL_IP] 40400 + +# For UDP traffic on port 40400 +nc -zuv [YOUR_EXTERNAL_IP] 40400 +``` + +:::tip Port Forwarding Required +If port forwarding isn't properly configured, your node may not be able to participate in P2P duties. +::: + +## Next Steps + +Once you have met the prerequisites, proceed to set up your desired node type: + +- [Run a Full Node →](./setup/running-a-node.md) +- [Run a Sequencer Node →](./setup/sequencer-setup.md) +- [Run a Prover Node →](./setup/running-a-prover.md) diff --git a/docs/lines/v6/docs-operate/operators/prover/claiming-rewards.mdx b/docs/lines/v6/docs-operate/operators/prover/claiming-rewards.mdx new file mode 100644 index 00000000000..96edcc43e80 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/prover/claiming-rewards.mdx @@ -0,0 +1,72 @@ +--- +id: claiming-rewards +title: Claiming prover rewards +description: How a prover claims epoch rewards from the Rollup contract, which address receives them, and how to claim without exposing your prover key. +displayed_sidebar: operatorsSidebar +references: ["docs/src/components/OperatorConfig/ProverRewardClaimer.js"] +--- + +Prover rewards accrue per epoch on the Rollup contract and are claimed with a different function than sequencer rewards. This page covers how to find what you are owed and how to claim it. + +## How prover rewards accrue + +When your prover submits the epoch proof, your `PROVER_ID` address is recorded as having a share of that epoch's prover reward pool. Your reward for an epoch is your share of that pool: `yourShares × epochRewards / totalShares`. Rewards become claimable only **after the epoch's proof submission deadline has passed**. + +`PROVER_ID` is the address you set in your prover node config (by default, the address derived from `PROVER_PUBLISHER_PRIVATE_KEY`). Rewards for an epoch are always paid **to that address**. + +## Find your claimable epochs + +Enter your RPC, Rollup address, and `PROVER_ID` below. This runs read-only calls against the Rollup to find every recent epoch where you have an unclaimed reward, and assembles the exact claim command with the epoch list filled in. Nothing is signed here and no keys are entered; you run the generated command yourself. + +<ProverRewardClaimer /> + +`claimProverRewards` takes an **array of epochs**, so one transaction claims them all. It skips epochs you already claimed, and reverts if any listed epoch has not yet passed its proof deadline. + +### You do not need your prover key to claim + +The address that submits the claim (and pays the gas) does **not** have to be your `PROVER_ID`. The contract reads your rewards by the `PROVER_ID` argument and pays them to that address regardless of who sent the transaction. So you can claim from any funded wallet (for example a separate gas-paying account) and the rewards still land at your `PROVER_ID` address. Your prover key only needs to sign later, if and when you move the claimed funds. + +### Claiming manually + +The helper above generates the claim command for you. To run the calls by hand, or to check epochs older than the helper's scan window, the functions on the Rollup contract are (set `$RPC_URL`, `$ROLLUP`, and `$PROVER_ID` to your RPC, the [Rollup address](#rollup-contract-addresses), and your prover address): + +```bash +# Read-only: pending reward for one epoch (0 if nothing, or already claimed) +cast call $ROLLUP "getSpecificProverRewardsForEpoch(uint256,address)(uint256)" [EPOCH] $PROVER_ID --rpc-url $RPC_URL + +# Claim one or more epochs (pays your PROVER_ID address) +cast send $ROLLUP "claimProverRewards(address,uint256[])" $PROVER_ID "[EPOCH_1,EPOCH_2]" --rpc-url $RPC_URL --private-key [GAS_PAYER_PRIVATE_KEY] +``` + +## Rewards after a rollup upgrade + +Rewards you earned before a rollup upgrade stay on the prior Rollup contract. To claim them, call `claimProverRewards` against that contract's address as well as the current one. + +### Rollup contract addresses + +**Mainnet:** + +| Rollup | Address | +|---|---| +| Current canonical (v6) | `0xE37A2663Cc9c119BA41572C40fAb0bbb23a5402f` | +| Previous (v5) | `0x91fF8bbD8Ebb07893010D50A48A1609e5EBd8E34` | +| Previous (v4) | `0xAe2001f7e21d5EcABf6234E9FDd1E76F50F74962` | + +**Testnet (Sepolia):** + +| Rollup | Address | +|---|---| +| Current canonical (v6) | `0x1f435a7F2dA358caF887A470FAeEE7d5D5f4eFBA` | +| Previous (v6 release candidate) | `0xEd1eB2eB670b42E2fB5A478c076A264b9604089d` | +| Previous (v6 release candidate) | `0x8c2fb2A68A3d362ab1DE99E06F83f8903160BbD9` | +| Previous (v5) | `0xD73A91bdcF6891C7642F3e460036e1ef2CC23178` | +| Previous (v4) | `0xE4394F118B115De2bDad88Ee1abd599cf5d25c70` | + +The current canonical address is always whatever `getCanonicalRollup()` returns on the Aztec Registry: `0x35b22e09ee0390539439e24f06da43d83f90e298` on mainnet, `0xa0bfb1b494fb49041e5c6e8c2c1be09cd171c6ba` on testnet. A testnet relaunch deploys a new Rollup, so confirm against the Registry if you are unsure which is current. + +Reward claiming is enabled on both mainnet and testnet, so accrued rewards can be claimed. + +## See also + +- [Prover setup](./setup): generating the prover key and setting `PROVER_ID` +- [Claiming rewards](/operate/operators/concepts/claiming-rewards): the sequencer and provider reward paths diff --git a/docs/lines/v6/docs-operate/operators/prover/overview.md b/docs/lines/v6/docs-operate/operators/prover/overview.md new file mode 100644 index 00000000000..859e10348a4 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/prover/overview.md @@ -0,0 +1,58 @@ +--- +id: overview +title: Prover overview +description: What an Aztec prover does, the broker / agent architecture, and minimum hardware requirements. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/prover-node/src/*", "labs/yarn-project/prover-client/src/proving_broker/*"] +--- + +## Overview + +This guide covers the steps required to run a prover on the Aztec network. Operating a prover is a resource-intensive role typically undertaken by experienced engineers due to its technical complexity and hardware requirements. + +Aztec provers are critical infrastructure components. They generate cryptographic proofs attesting to transaction correctness, ultimately producing a single rollup proof submitted to Ethereum. + +:::tip Prerequisites +Before proceeding, ensure you've reviewed and completed the [prerequisites](../prerequisites.md). +::: + +## Prover Architecture + +The prover consists of three main components: + +1. **Prover node**: Polls L1 for unproven epochs, creates prover jobs, distributes them to the broker, and submits the final rollup proof to the rollup contract. + +2. **Prover broker**: Manages the job queue, distributing work to agents and collecting results. + +3. **Prover agent(s)**: Executes proof generation jobs in a stateless manner. + +## Minimum Requirements + +### Prover Node + +- 16 core / 32 vCPU (Skylake or newer microarchitecture) +- 16 GB RAM +- 1 TB NVMe SSD +- 25 Mbps network connection + +### Prover Broker + +- 8 core / 16 vCPU (Skylake or newer microarchitecture) +- 16 GB RAM +- 10 GB SSD + +### Prover Agents + +**For each agent:** +- 32 core / 64 vCPU (Skylake or newer microarchitecture) +- 128 GB RAM +- 10 GB SSD + +These requirements are subject to change as the network throughput increases. Prover agents require high-performance hardware, typically data center-grade infrastructure. + +:::tip Running Multiple Agents +You can run multiple prover agents on a single machine by adjusting `PROVER_AGENT_COUNT`. Hardware requirements scale approximately linearly: +- **2 agents**: 64 cores, 256 GB RAM +- **3 agents**: 96 cores, 384 GB RAM +- **4 agents**: 128 cores, 512 GB RAM +::: diff --git a/docs/lines/v6/docs-operate/operators/prover/setup.md b/docs/lines/v6/docs-operate/operators/prover/setup.md new file mode 100644 index 00000000000..99d1e8cc5c1 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/prover/setup.md @@ -0,0 +1,243 @@ +--- +id: setup +title: Prover setup +description: Generate keys, then deploy the prover node, broker, and agents. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/prover-node/src/config.ts", "labs/yarn-project/prover-client/src/proving_broker/config.ts", "labs/yarn-project/aztec/src/cli/aztec_start_options.ts"] +--- + +## Generating Keys + +Before setting up your prover, you need to generate the required Ethereum private key for the prover publisher. + +### Prover Publisher Private Key + +The prover publisher key is used to submit proofs to L1. This account needs ETH funding to pay for L1 gas. + +Generate an Ethereum private key using Foundry's `cast` tool: + +```bash +# Generate a new wallet with a 24-word mnemonic +cast wallet new-mnemonic --words 24 + +# This outputs a mnemonic phrase, a derived address, and private key +# Save these securely - you'll need the private key for PROVER_PUBLISHER_PRIVATE_KEY +# and the address for PROVER_ID +``` + +**Important notes:** +- Save both the private key and the derived address securely +- The private key will be used for `PROVER_PUBLISHER_PRIVATE_KEY` +- The derived Ethereum address will be used for `PROVER_ID` + +:::warning Account Funding Required +The publisher account needs to be funded with ETH to post proofs to L1. Ensure the account holds sufficient ETH for gas costs during operation. +::: + +:::tip +If you don't have Foundry installed, follow the installation guide at [getfoundry.sh](https://getfoundry.sh/). +::: + +## Setup + +The prover components are distributed across multiple machines for better performance and resource utilization. This setup runs multiple prover agents on separate high-performance machines, isolates the broker for better job queue management, and separates network-facing components (prover node) from compute-intensive components (agents). + +### Architecture + +- **Prover Node**: Runs on a machine with network access and L1 connectivity +- **Prover Broker**: Can run on the same machine as the prover node or separately (must be accessible from prover agents) +- **Prover Agents**: Run on separate high-performance machines (32+ cores each, scalable with `PROVER_AGENT_COUNT`) + +:::warning Network Requirements +Prover agents must communicate with the prover broker over the network. Ensure that: + +- The broker machine's port 8080 is accessible from all agent machines +- Firewall rules allow traffic between agents and broker +- Network connectivity is stable and low-latency between components +::: + +### Prover Node and Broker Setup + +On the machine that will run the prover node and broker: + +#### Step 1: Set Up Directory Structure + +```bash +mkdir -p aztec-prover-node/prover-node-data aztec-prover-node/prover-broker-data +cd aztec-prover-node +touch .env +``` + +#### Step 2: Configure Environment Variables + +Add to your `.env` file: + +```bash +# Prover Node Configuration +DATA_DIRECTORY=./prover-node-data +P2P_IP=[your external IP address] +P2P_PORT=40400 +ETHEREUM_HOSTS=[your L1 execution endpoint] +L1_CONSENSUS_HOST_URLS=[your L1 consensus endpoint] +LOG_LEVEL=info +PROVER_BROKER_HOST=http://prover-broker:8080 +PROVER_PUBLISHER_PRIVATE_KEY=[your prover publisher private key, see prerequisites] +AZTEC_PORT=8080 +AZTEC_ADMIN_PORT=8880 + +# Prover Broker Configuration +PROVER_BROKER_DATA_DIRECTORY=./prover-broker-data +PROVER_BROKER_PORT=8080 +``` + +#### Step 3: Create Docker Compose File + +Create `docker-compose.yml`: + +```yaml +name: aztec-prover-node +services: + prover-node: + image: azteclabs/aztec:#release_version + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --prover-node + --network #release_network + depends_on: + prover-broker: + condition: service_started + required: true + environment: + DATA_DIRECTORY: /var/lib/data + ETHEREUM_HOSTS: ${ETHEREUM_HOSTS} + L1_CONSENSUS_HOST_URLS: ${L1_CONSENSUS_HOST_URLS} + LOG_LEVEL: ${LOG_LEVEL} + PROVER_BROKER_HOST: ${PROVER_BROKER_HOST} + PROVER_PUBLISHER_PRIVATE_KEY: ${PROVER_PUBLISHER_PRIVATE_KEY} + P2P_IP: ${P2P_IP} + P2P_PORT: ${P2P_PORT} + AZTEC_PORT: ${AZTEC_PORT} + AZTEC_ADMIN_PORT: ${AZTEC_ADMIN_PORT} + ports: + - ${AZTEC_PORT}:${AZTEC_PORT} + - ${P2P_PORT}:${P2P_PORT} + - ${P2P_PORT}:${P2P_PORT}/udp + volumes: + - ${DATA_DIRECTORY}:/var/lib/data + restart: unless-stopped + + prover-broker: + image: azteclabs/aztec:#release_version + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --prover-broker + --network #release_network + environment: + DATA_DIRECTORY: /var/lib/data + ETHEREUM_HOSTS: ${ETHEREUM_HOSTS} + P2P_IP: ${P2P_IP} + LOG_LEVEL: ${LOG_LEVEL} + ports: + - ${PROVER_BROKER_PORT}:8080 + volumes: + - ${PROVER_BROKER_DATA_DIRECTORY}:/var/lib/data + restart: unless-stopped +``` + +:::warning Security: Admin Port Not Exposed +The admin port (8880) is intentionally **not exposed** to the host machine for security reasons. The admin API provides sensitive operations like configuration changes and database rollbacks that should never be accessible from outside the container. + +If you need to access admin endpoints, use `docker exec`: +```bash +docker exec -it prover-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' +``` + +The admin API requires an API key by default. The node generates one at first startup and prints it once to the logs; pass it with the `x-api-key` header (or `Authorization: Bearer [ADMIN_API_KEY]`). See [admin API key authentication](../reference/changelog/v4.md#admin-api-key-authentication). +::: + +**Important:** The broker exposes port 8080 via `ports: - ${PROVER_BROKER_PORT}:8080`, making it accessible to external prover agents. Ensure this port is reachable from your agent machines. + +This configuration includes only essential settings. The `--network #release_network` flag applies network-specific defaults—see the [CLI reference](../reference/cli-reference.md) for all available configuration options. + +#### Step 4: Start Node and Broker + +```bash +docker compose up -d +``` + +### Prover Agent Setup + +On each machine that will run prover agents: + +#### Step 1: Set Up Directory + +```bash +mkdir aztec-prover-agent +cd aztec-prover-agent +touch .env +``` + +#### Step 2: Configure Environment Variables + +Add to your `.env` file: + +```bash +PROVER_AGENT_COUNT=1 +PROVER_AGENT_POLL_INTERVAL_MS=10000 +PROVER_BROKER_HOST=http://[BROKER_MACHINE_IP]:8080 +PROVER_ID=[address corresponding to PROVER_PUBLISHER_PRIVATE_KEY] +``` + +Replace `[BROKER_MACHINE_IP]` with the IP address of the machine running the prover broker. + +**Agent configuration tips:** + +- Set `PROVER_AGENT_COUNT` based on your machine's hardware (e.g., 64 cores/256 GB RAM = 2 agents, 96 cores/384 GB RAM = 3 agents, 128 cores/512 GB RAM = 4 agents) +- Test connectivity before starting: `curl http://[BROKER_MACHINE_IP]:8080` +- If the curl test fails, check your network configuration, firewall rules, and ensure the broker is running + +#### Step 3: Create Docker Compose File + +Create `docker-compose.yml`: + +```yaml +name: aztec-prover-agent +services: + prover-agent: + image: azteclabs/aztec:#release_version + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --prover-agent + --network #release_network + environment: + PROVER_AGENT_COUNT: ${PROVER_AGENT_COUNT} + PROVER_AGENT_POLL_INTERVAL_MS: ${PROVER_AGENT_POLL_INTERVAL_MS} + PROVER_BROKER_HOST: ${PROVER_BROKER_HOST} + PROVER_ID: ${PROVER_ID} + restart: unless-stopped +``` + +#### Step 4: Start Agent + +```bash +docker compose up -d +``` + +**Scaling your prover capacity:** + +- **Horizontal scaling**: Add more agent machines by repeating the agent setup on additional high-performance machines +- **Vertical scaling**: Increase `PROVER_AGENT_COUNT` on existing machines (ensure adequate hardware) + +All agents, regardless of which machine they're on, must be able to communicate with the broker at the configured `PROVER_BROKER_HOST`. diff --git a/docs/lines/v6/docs-operate/operators/prover/troubleshooting.md b/docs/lines/v6/docs-operate/operators/prover/troubleshooting.md new file mode 100644 index 00000000000..df45f4cf8f0 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/prover/troubleshooting.md @@ -0,0 +1,65 @@ +--- +id: troubleshooting +title: Prover troubleshooting +description: Common issues prover operators hit and how to fix them. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/prover-client/src/proving_broker/*", "labs/yarn-project/prover-node/src/config.ts"] +--- + +## Troubleshooting + +### Components not communicating + +**Issue**: Prover agent cannot connect to broker. + +**Solutions**: +- Verify the broker IP address in `PROVER_BROKER_HOST` is correct +- Ensure port 8080 on the broker machine is accessible from agent machines +- Check firewall rules between machines allow traffic on port 8080 +- Test connectivity from agent machine: `curl http://[BROKER_IP]:8080` +- Verify the broker container is running: `docker compose ps` +- Check if the broker port is exposed in docker-compose.yml +- Review broker logs for connection attempts: `docker compose logs prover-broker` + +### Insufficient resources + +**Issue**: Prover agent crashes or performs poorly. + +**Solutions**: +- Verify your hardware meets the minimum requirements (32 cores per agent, 128 GB RAM per agent) +- Check system resource usage: `docker stats` +- Reduce `PROVER_AGENT_COUNT` if running multiple agents per machine +- Ensure no other resource-intensive processes are running +- Monitor CPU and memory usage to verify resources match your configured agent count + +### Agent not picking up jobs + +**Issue**: Agent logs show no job activity. + +**Solutions**: +- Verify the broker is receiving jobs from the prover node +- Check broker logs for errors +- Confirm `PROVER_ID` matches your publisher address +- Ensure agent can reach the broker endpoint +- Test broker connectivity: `curl http://[BROKER_IP]:8080` + +### Docker issues + +**Issue**: Containers won't start or crash repeatedly. + +**Solutions**: +- Ensure Docker and Docker Compose are up to date +- Check disk space availability on all machines +- Verify `.env` files are properly formatted +- Review logs for specific error messages + +### Common Issues + +See the [Operator FAQ](../operator-faq.md) for additional common issues and resolutions. + +## Next Steps + +- Monitor your prover's performance and proof submission rate +- Consider adding more prover agents for increased capacity (either by increasing `PROVER_AGENT_COUNT` or adding more machines) +- Join the [Aztec Discord](https://discord.gg/aztec) for operator support +- Review [governance participation](../sequencer-management/governance-participation.md) for participating in governance diff --git a/docs/lines/v6/docs-operate/operators/prover/verification.md b/docs/lines/v6/docs-operate/operators/prover/verification.md new file mode 100644 index 00000000000..77918fbeb3e --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/prover/verification.md @@ -0,0 +1,113 @@ +--- +id: verification +title: Verifying your prover +description: Confirm the prover node, broker, and agents are running, connected, and proving epochs. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/prover-client/src/proving_broker/proving_broker.ts", "labs/yarn-project/prover-client/src/proving_broker/proving_agent.ts", "labs/yarn-project/prover-node/src/prover-node.ts"] +--- + +## Verification + +A prover is working when four things line up: every container is up, the broker is +receiving jobs from the prover node, the agents are picking those jobs up, and jobs are +completing. Check them in that order. + +### Containers are up + +On the machine running the prover node and broker: + +```bash +docker compose ps +``` + +Both `prover-node` and `prover-broker` should show `Up`. On each agent machine: + +```bash +docker compose ps +``` + +Each `prover-agent` should show `Up`. A container that is restarting or has exited points to a +configuration or resource problem; see [Prover troubleshooting](./troubleshooting.md). + +### The broker is receiving jobs from the prover node + +The prover node discovers epochs to prove and submits them to the broker. Watch the broker +log: + +```bash +docker compose logs -f prover-broker +``` + +On startup the broker logs `Proving Broker started`. Once the prover node begins submitting +work, the broker logs a line per job: + +``` +New proving job id=... epochNumber=... +``` + +If you never see `New proving job` lines, the prover node is not submitting work. Confirm the +node is synced and connected to the broker before looking at the agents. + +### Agents are picking up jobs + +On an agent machine: + +```bash +docker compose logs -f prover-agent +``` + +A connected, working agent logs each job it takes and finishes: + +``` +Starting job id=... type=... inputsUri=... +Job id=... type=... completed outputUri=... +``` + +If the broker shows `New proving job` lines but the agents log nothing, the agents cannot +reach the broker. Confirm `PROVER_BROKER_HOST` on the agent points at the broker machine and +that port 8080 is reachable: + +```bash +curl http://[BROKER_IP]:8080 +``` + +See [Components not communicating](./troubleshooting.md#components-not-communicating). + +### Jobs are completing + +Back on the broker log, each finished job produces: + +``` +Proving job complete id=... type=... totalAttempts=... +``` + +Steady `New proving job` then `Proving job complete` pairs mean the full pipeline is working: +prover node to broker to agent and back. Repeated `failed` lines in the agent log, or jobs that +the broker logs as `Retrying` and then `Marking proving job as failed`, indicate agents that are +crashing or under-resourced; see [Insufficient resources](./troubleshooting.md#insufficient-resources). + +### Optional: check queue depth from metrics + +If you export metrics (see [Monitoring](../concepts/monitoring.md)), the broker publishes the +current queue as `aztec.proving_queue.size` and the jobs in progress as +`aztec.proving_queue.active_jobs_count`. A queue that only grows while the active-jobs count +stays at zero is the same "agents not picking up jobs" condition as the step above. + +### Optional: query the node admin API + +The prover node exposes an admin API on port 8880 inside the container, which is not published +to the host. Use `docker exec` to read its configuration: + +```bash +docker exec -it prover-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' +``` + +The admin API requires an API key by default. The node generates one at first startup and prints it once to the logs; pass it with the `x-api-key` header (or `Authorization: Bearer [ADMIN_API_KEY]`). See [admin API key authentication](../reference/changelog/v4.md#admin-api-key-authentication). + +## Next steps + +- Keep an eye on the prover through [Monitoring](../concepts/monitoring.md). +- If any check above fails, work through [Prover troubleshooting](./troubleshooting.md). diff --git a/docs/lines/v6/docs-operate/operators/provider/configure-environment.mdx b/docs/lines/v6/docs-operate/operators/provider/configure-environment.mdx new file mode 100644 index 00000000000..21a00910676 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/configure-environment.mdx @@ -0,0 +1,138 @@ +--- +title: "Configure environment" +description: "Create the project directory and a single .env file that holds every variable your Aztec sequencer needs." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/foundation/src/config/env_var.ts", "labs/yarn-project/node-keystore/src/*", "labs/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts", "labs/yarn-project/aztec-node/src/aztec-node/server.ts", "labs/yarn-project/p2p/src/config.ts"] +--- + +# Configure environment + +**Time:** ~5 minutes. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +<OperatorConfig /> + +## 1. Create the directory + +```bash +mkdir -p aztec-sequencer/data && cd aztec-sequencer +``` + +## 2. Copy your keystore into the project + +Copy only the node keystore files (`key1.json`, `key2.json`, ...) into the directory the node loads. Do **not** copy the `*_staker_output.json` files. + +```bash +mkdir -p ./keys +cp ~/.aztec/keystore/key*.json ./keys +rm -f ./keys/*_staker_output.json +``` + +:::warning Keep staker output files out of `KEY_STORE_DIRECTORY` +The node loads every `.json` file in `KEY_STORE_DIRECTORY` and expects each one to be a node keystore. The `key*_staker_output.json` files generated by `aztec validator-keys new --staker-output` are a different format (used for registration, not for running the node), so leaving one in this directory makes the node fail to start. The `rm` above removes them after the copy. +::: + +## 3. Create the `.env` file + +<ConfigCode language="bash" title=".env">{`# ── Aztec Sequencer Configuration ── + +# Data & Keys +DATA_DIRECTORY=./data +KEY_STORE_DIRECTORY=./keys +LOG_LEVEL=info + +# Ethereum L1 endpoints +ETHEREUM_HOSTS={{ETH_RPC}} +L1_CONSENSUS_HOST_URLS={{CONSENSUS_RPC}} +ETHEREUM_DEBUG_HOSTS={{DEBUG_RPC}} + +# P2P networking +P2P_IP={{P2P_IP}} +P2P_PORT=40400 + +# Aztec ports +AZTEC_PORT=8080 +AZTEC_ADMIN_PORT=8880`}</ConfigCode> + +:::tip Don't know your external IP? +Run `curl ipv4.icanhazip.com`. +::: + +:::warning Never expose port 8880 +Port 8880 is the **admin port**: it reads and changes node configuration. It requires an API key by default, and it must still never be exposed. It is intentionally not mapped in the Docker Compose file in the next step. +::: + +## After delegation: set coinbase to the Split contract + +When a delegator stakes to your provider, the protocol creates a **Splits-V2 Split contract** that holds the commission/delegator distribution rules for that delegation. In the default flow, you update the corresponding attester's keystore so its `coinbase` points at that Split. Without this step, rewards still accumulate but the Split can't disperse them according to your agreed commission. For the opt-in alternative, see [EOA coinbase with the payout script](#alternative-eoa-coinbase-with-the-payout-script) below. + +This is a **per-delegation, manual** task. The protocol does not notify you when a new delegation arrives. + +**The procedure for each new delegation:** + +1. Open the [staking dashboard](https://stake.aztec.network) and find your provider page. +2. Expand the **Sequencer Registered (x)** dropdown. The table shows each of your active attester addresses paired with its Split contract address. +3. Edit the keystore for that attester. Set `coinbase` to the Split contract address. Example: + + ```json + { + "schemaVersion": 1, + "validators": [{ + "attester": { "eth": "0x...", "bls": "0x..." }, + "publisher": ["0x..."], + "coinbase": "0x<SPLIT_CONTRACT_ADDRESS>", + "feeRecipient": "0x000...000" + }] + } + ``` + +4. Tell the running node to reload the keystore from disk. This applies the new coinbase without a restart: + + ```bash + docker exec -it aztec-sequencer curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_reloadKeystore","params":[],"id":1}' + ``` + + The admin API requires an API key by default. The node generates one at first startup and prints it once to the logs; it is reused across restarts because you set `DATA_DIRECTORY`. Pass it with the `x-api-key` header (or `Authorization: Bearer [ADMIN_API_KEY]`). See [admin API key authentication](../reference/changelog/v4.md#admin-api-key-authentication) for the key lifecycle and the opt-out flag. + + `aztecAdmin_reloadKeystore` re-reads the keystore directory and applies each validator's attester key, coinbase, and fee recipient. It does not reload L1 publisher signers, prover keys, or HA signer database connections; those still require a restart. A new validator must reuse a publisher key that was already configured at startup, or the reload is rejected. See the [node API reference](../reference/node-api-reference.md#admin-api). + + Restarting the node also loads the new keystore, if you prefer: + + ```bash + docker compose restart aztec-sequencer + ``` + +:::warning Monitor delegations actively +The system does not push notifications. Set up a daily (or hourly during active recruiting) check on the dashboard, and update keystores promptly when new delegations arrive. Maintain a local mapping of attester → Split address so you don't lose track. +::: + +### Alternative: EOA coinbase with the payout script + +Instead of pointing each delegated attester's coinbase at its Split contract, a provider can set the coinbase to an address they control and handle delegator payouts with the [aztec-staking-payout](https://github.com/AztecProtocol/aztec-staking-payout) script: + +- Run the script on a regular cadence (weekly, for example). It calculates each delegator's share at the commission rate you publish. +- It produces a Multicall3 batch of ERC20 transfers ready to sign, plus auditable JSON artifacts that delegators can verify against onchain data. + +This is fully opt-in and nothing changes at the protocol level: existing Split contracts keep working, and providers who stay on the Split flow take no action. The difference is the trust model, which moves offchain. Delegators rely on you running the script at the commission and cadence you published, so publish both and keep to them. Background and discussion: [Operator commission adjustments](https://forum.aztec.network/t/operator-commission-adjustments/8588) on the forum. + +If you hit empty queue, no delegations can activate. See [Updates, alerts, rewards](./day-2-essentials) for the queue-monitoring snippet. + +<details open> +<summary>What does each variable do?</summary> + +- **`DATA_DIRECTORY`** is where the node stores blockchain data. The directory grows over time. +- **`KEY_STORE_DIRECTORY`** is the path to your validator keystore (the `key1.json` file). +- **`ETHEREUM_HOSTS`** is your L1 execution client. The node reads rollup contract state and submits proofs here. +- **`L1_CONSENSUS_HOST_URLS`** is your L1 consensus client (Beacon API). Needed for blob data access. +- **`ETHEREUM_DEBUG_HOSTS`** is a trace-capable endpoint (`debug_traceTransaction`). It is optional: if you leave it unset, the node uses `ETHEREUM_HOSTS` for trace calls, so set it separately only if your trace-capable endpoint differs from your main execution endpoint. Whichever endpoint the node traces with must serve trace methods: the node refuses to start otherwise, unless you set `ETHEREUM_ALLOW_NO_DEBUG_HOSTS=true`. +- **`P2P_IP`** is your server's public IP so other nodes can connect to you. +- **`P2P_PORT 40400`** is the port for peer-to-peer communication, both TCP and UDP. It must be forwarded through your firewall. + +</details> + diff --git a/docs/lines/v6/docs-operate/operators/provider/day-2-essentials.mdx b/docs/lines/v6/docs-operate/operators/provider/day-2-essentials.mdx new file mode 100644 index 00000000000..5818fd8970f --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/day-2-essentials.mdx @@ -0,0 +1,106 @@ +--- +title: "Updates, alerts, rewards" +description: "Set up version upgrades, monitoring alerts, reward distribution, and queue monitoring for your live provider, plus reference commands and links." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/telemetry-client/src/*"] +--- + +# Updates, alerts, rewards + +**Time:** ~5 minutes. Your sequencers are running. Set up four things before you call it done: version upgrades, monitoring alerts, reward distribution, and queue monitoring. The rest of this page is reference material to keep handy. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +<OperatorConfig /> + +## 1. Stay current on upgrades + +When a new version is released: + +```bash +# Pull new image and restart +docker compose pull && docker compose up -d + +# Verify version +docker compose logs aztec-sequencer | head -20 +``` + +:::warning Watch for upgrade announcements +Some upgrades require action within a specific timeframe; a node left on an old version eventually stops attesting. Watch the channels in [Where to stay informed](#where-to-stay-informed) below, especially Discord, where time-sensitive upgrade notices are posted first. +::: + +## 2. Set up monitoring and alerts + +Aztec exports metrics via **OpenTelemetry**. The full monitoring stack: + +- **OpenTelemetry Collector** receives metrics from the node via OTLP. +- **Prometheus** stores time-series metrics. +- **Grafana** renders dashboards and alerts. + +Key metrics to watch: + +- Attestation rate (target: 99%+) +- Block proposals +- Committee participation +- Peer count +- Disk usage + +:::tip Quick start +The [monitoring installer](/operate/operators/concepts/monitoring#set-up-monitoring-with-the-installer) sets up the whole stack (dashboard, metrics, and Telegram alerts) from one script. +::: + +See [Monitoring and metrics](/operate/operators/concepts/monitoring) for the installer walkthrough and what to alert on. + +## 3. Claim rewards + +Two paths: + +- **[Aztec staking dashboard](https://stake.aztec.network/my-position)** — aggregates pending rewards across every sequencer you operate, claims in one wallet flow. Easiest for most operators. +- **CLI claim** — see the [Claiming Rewards guide](/operate/operators/sequencer-management/claiming-rewards) for the full reference (pending-rewards check, `claimSequencerRewards` cast send, keystore-file and hardware-wallet variants, gas-limit troubleshooting). + +In the default flow, your `coinbase` for each delegated attester is the per-delegation Split contract (set in [Configure environment](./configure-environment#after-delegation-set-coinbase-to-the-split-contract)). Each delegation's Split is a Splits-V2 pull split, so getting rewards out to the final addresses is two steps. First, anyone calls `distribute()` on the Split contract, which moves the accumulated balance into the shared Splits warehouse and credits each recipient by their share. Second, each recipient calls `withdraw()` on the warehouse to move their credited balance to their own address: your commission share to the rewards-recipient address you set at registration, and the delegator's share to their vault. Both steps are permissionless and need no manual settlement script; just monitor that delegations are being distributed and withdrawn. + +:::info Alternative: EOA coinbase with the payout script +The Split flow is the default, not the only option. A provider can set the coinbase to an address they control and run the [payout script](https://github.com/AztecProtocol/aztec-staking-payout) to calculate and distribute delegator rewards at a published commission and cadence. See [the configuration step](/operate/operators/provider/configure-environment#alternative-eoa-coinbase-with-the-payout-script) and the [forum announcement](https://forum.aztec.network/t/operator-commission-adjustments/8588). +::: + +## 4. Keep your provider queue stocked + +Every delegation pulls one keystore off your queue. Empty queue = no new delegations can activate. Check queue length: + +<ConfigCode language="bash">{`cast call {{STAKING_REGISTRY_ADDR}} \\ + "getProviderQueueLength(uint256)(uint256)" \\ + {{PROVIDER_ID}} \\ + --rpc-url {{ETH_RPC}}`}</ConfigCode> + +For a cron-driven alert that pings you below a threshold, see the [Monitoring Keystore Availability section](/operate/operators/setup/become_a_staking_provider#monitoring-keystore-availability) — copy that script verbatim, edit the placeholders, schedule with `crontab -e`. + +When you need to add more keystores, follow the same flow you used in [register as staking provider](./register-sequencer) (Step 2). + +For per-delegation Split-coinbase updates, provider config changes, queue recovery (`dripProviderQueue`), and admin rotation, see the dedicated [provider operations](./provider-operations) page. + +## Slashing awareness + +Slashing is tiered. Inactivity (failing to attest or propose when assigned) is slashed 2,000 AZTEC, and so is submitting a proposal with invalid attestations. Duplicate proposals and duplicate attestations are slashed 5,000 AZTEC. There is a 190,000 AZTEC ejection threshold below which the sequencer is ejected. See [Slashing](../concepts/slashing) for the full offense set and thresholds. + +## Where to stay informed + +- [Discord](https://discord.com/invite/aztec) — upgrade announcements and operator discussion. This is where version updates and time-sensitive notices are posted first. +- [Forum](https://forum.aztec.network) — longer-form proposals, governance, and operator announcements. +- [GitHub](https://github.com/AztecProtocol/aztec-packages) — source, issues, and [releases](https://github.com/AztecProtocol/aztec-packages/releases). +- [Changelog](/operate/operators/reference/changelog) — per-version operator-facing changes and migration notes. + +## Key links + +- [Operator overview](/operate/operators) (the rest of this section) +- [Network info and contract addresses](https://docs.aztec.network/networks) +- [Dashtec — sequencer analytics](https://dashtec.xyz) +- [AztecScan — block explorer](https://aztecscan.xyz) +- [CLI reference (1000+ flags)](/operate/operators/reference/cli-reference) +- [Node API reference (JSON-RPC)](/operate/operators/reference/node_api_reference) + +## You finished the guide + +Your sequencers are live. Focus on keeping the publisher funded, the L1 endpoints reliable, keeping your nodes on the latest recommended version, and keeping the queue stocked. diff --git a/docs/lines/v6/docs-operate/operators/provider/docker-compose.mdx b/docs/lines/v6/docs-operate/operators/provider/docker-compose.mdx new file mode 100644 index 00000000000..f64de7adcd2 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/docker-compose.mdx @@ -0,0 +1,163 @@ +--- +title: "Docker Compose" +description: "Create the docker-compose.yml that runs the Aztec node and sequencer. HA mode runs the same attester across two nodes coordinating via shared PostgreSQL." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/aztec/src/cli/*", "labs/yarn-project/validator-ha-signer/src/*", "labs/yarn-project/stdlib/src/interfaces/aztec-node.ts"] +--- + +# Docker Compose + +**Time:** ~3 minutes. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +<IfChoice axis="topology" is="single"> + +<Heading as="h2" id="single-server-compose">Single-server compose</Heading> + +Create `docker-compose.yml` in your `aztec-sequencer/` directory: + +<ConfigCode language="yaml" title="docker-compose.yml">{`services: + aztec-sequencer: + image: "azteclabs/aztec:{{IMAGE_TAG}}" + container_name: "aztec-sequencer" + ports: + - "\${AZTEC_PORT}:\${AZTEC_PORT}" + - "\${P2P_PORT}:\${P2P_PORT}" + - "\${P2P_PORT}:\${P2P_PORT}/udp" + volumes: + - \${DATA_DIRECTORY}:/var/lib/data + - \${KEY_STORE_DIRECTORY}:/var/lib/keys:ro + environment: + DATA_DIRECTORY: /var/lib/data + KEY_STORE_DIRECTORY: /var/lib/keys + LOG_LEVEL: \${LOG_LEVEL} + ETHEREUM_HOSTS: \${ETHEREUM_HOSTS} + L1_CONSENSUS_HOST_URLS: \${L1_CONSENSUS_HOST_URLS} + ETHEREUM_DEBUG_HOSTS: \${ETHEREUM_DEBUG_HOSTS} + P2P_IP: \${P2P_IP} + P2P_PORT: \${P2P_PORT} + AZTEC_PORT: \${AZTEC_PORT} + AZTEC_ADMIN_PORT: \${AZTEC_ADMIN_PORT} + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --node + --sequencer + --network {{NETWORK_NAME}} + networks: + - aztec + restart: always + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:'+process.env.AZTEC_PORT,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method:'aztec_getChainTips',params:[],id:1})}).then(r=>r.json()).then(j=>{var b=j&&j.result&&j.result.proven&&j.result.proven.block;var n=b&&b.number;if(typeof n!=='number'||n<=0)process.exit(1);return fetch('http://localhost:'+process.env.AZTEC_PORT,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method:'aztec_getSyncedL1Timestamp',params:[],id:1})}).then(r=>r.json()).then(j2=>{var ts=Number(j2&&j2.result);if(!ts||(Math.floor(Date.now()/1000)-ts)>300){process.exit(1)}process.exit(0)})}).catch(()=>process.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10m + +networks: + aztec: + name: aztec`}</ConfigCode> + +</IfChoice> + +<IfChoice axis="topology" is="ha" label="HA topology"> + +<Heading as="h2" id="ha-compose-same-attester-two-nodes-shared-postgresql">HA compose: same attester, two nodes, shared PostgreSQL</Heading> + +In HA mode you run **two near-identical sequencer nodes**, both holding the same attester key. They coordinate via a shared PostgreSQL database that ensures only one node signs each duty (block proposal, attestation, governance vote). If one node dies, the other keeps attesting without missing a duty and without risking double-signing. + +You'll need: + +1. A PostgreSQL database reachable by both nodes. Use a managed multi-zone service with automatic failover; a database on a single machine takes both nodes down with it. See [the HA database setup](/operate/operators/setup/high_availability_sequencers#step-6-set-up-the-ha-database) for provider examples. +2. The HA database schema initialized once before starting either node: + +<ConfigCode language="bash">{`aztec migrate-ha-db up --database-url postgresql://user:password@host:5432/validator_ha`}</ConfigCode> + +3. The compose file below, deployed to **both servers** with **different `VALIDATOR_HA_NODE_ID` values** (and different `P2P_IP` values per server's public IP). Everything else stays identical. + +<ConfigCode language="yaml" title="docker-compose.yml (deploy on both servers)">{`services: + aztec-sequencer: + image: "azteclabs/aztec:{{IMAGE_TAG}}" + container_name: "aztec-sequencer" + ports: + - "\${AZTEC_PORT}:\${AZTEC_PORT}" + - "\${P2P_PORT}:\${P2P_PORT}" + - "\${P2P_PORT}:\${P2P_PORT}/udp" + volumes: + - \${DATA_DIRECTORY}:/var/lib/data + - \${KEY_STORE_DIRECTORY}:/var/lib/keys:ro + environment: + DATA_DIRECTORY: /var/lib/data + KEY_STORE_DIRECTORY: /var/lib/keys + LOG_LEVEL: \${LOG_LEVEL} + ETHEREUM_HOSTS: \${ETHEREUM_HOSTS} + L1_CONSENSUS_HOST_URLS: \${L1_CONSENSUS_HOST_URLS} + ETHEREUM_DEBUG_HOSTS: \${ETHEREUM_DEBUG_HOSTS} + P2P_IP: \${P2P_IP} + P2P_PORT: \${P2P_PORT} + AZTEC_PORT: \${AZTEC_PORT} + AZTEC_ADMIN_PORT: \${AZTEC_ADMIN_PORT} + # HA coordination + VALIDATOR_HA_SIGNING_ENABLED: "true" + VALIDATOR_HA_DATABASE_URL: \${VALIDATOR_HA_DATABASE_URL} + VALIDATOR_HA_NODE_ID: \${VALIDATOR_HA_NODE_ID} + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --node + --sequencer + --network {{NETWORK_NAME}} + networks: + - aztec + restart: always + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:'+process.env.AZTEC_PORT,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method:'aztec_getChainTips',params:[],id:1})}).then(r=>r.json()).then(j=>{var b=j&&j.result&&j.result.proven&&j.result.proven.block;var n=b&&b.number;if(typeof n!=='number'||n<=0)process.exit(1);return fetch('http://localhost:'+process.env.AZTEC_PORT,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method:'aztec_getSyncedL1Timestamp',params:[],id:1})}).then(r=>r.json()).then(j2=>{var ts=Number(j2&&j2.result);if(!ts||(Math.floor(Date.now()/1000)-ts)>300){process.exit(1)}process.exit(0)})}).catch(()=>process.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10m + +networks: + aztec: + name: aztec`}</ConfigCode> + +In each server's `.env`, add: + +```bash +VALIDATOR_HA_DATABASE_URL=postgresql://user:password@host:5432/validator_ha +VALIDATOR_HA_NODE_ID=validator-node-1 # use validator-node-2 on the second server +``` + +:::warning Same keystore on both nodes +Both nodes hold the same attester key. The shared Postgres lock is what stops them from both signing the same duty. Do not run HA without the database, or you risk double-signing and slashing. +::: + +:::info What HA does and doesn't do +HA gives you **redundancy for one attester identity**. If you have N validator identities and want to spread them across multiple nodes for load (rather than redundancy), that's a different setup, not what this section covers. See [High availability sequencers](/operate/operators/setup/high_availability_sequencers) for the canonical reference, including remote-signer patterns and database tuning. +::: + +</IfChoice> + +:::info Health check +The `healthcheck` block probes the node's own HTTP API every 30 seconds. The container reports: + +- **`starting`** during initial sync. The node answers its API but does not yet have a proven block, so the check holds off. The `start_period` of 10 minutes covers the snapshot download and catch-up before any failure counts. If your node typically takes longer to reach a proven tip, raise `start_period` to match. +- **`healthy`** once the node has a proven tip greater than 0 and the chain time it has synced to is within 5 minutes of now, meaning it is keeping up with network state. +- **`unhealthy`** if the API stops answering, the node falls more than 5 minutes behind the chain, or the proven tip drops back to 0 for three checks in a row. + +The status shows in `docker compose ps` and can be scraped by monitoring. The check **reports** health; it does not restart the container on its own. For automatic recovery on `unhealthy`, pair it with a tool such as `autoheal`, or alert on the status from your monitoring stack. + +The check requires both a proven L2 tip greater than 0 and a recent synced chain time (within 5 minutes), so it catches a node that synced once and then stalled. Adjust the `300` (seconds) in the probe to change that staleness window. +::: + +:::warning Admin port stays internal +Port 8880 is the admin port and is intentionally not mapped to the host. Reach it only via `docker exec` on the box itself. +::: + diff --git a/docs/lines/v6/docs-operate/operators/provider/fund-accounts.mdx b/docs/lines/v6/docs-operate/operators/provider/fund-accounts.mdx new file mode 100644 index 00000000000..9515a0692f6 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/fund-accounts.mdx @@ -0,0 +1,123 @@ +--- +title: "Fund accounts" +description: "Fund the publisher with ETH. Delegators provide the AZTEC stake." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/ethereum/src/l1_tx_utils/config.ts", "labs/yarn-project/ethereum/src/publisher_manager.ts", "labs/yarn-project/sequencer-client/src/publisher/*"] +--- + +# Fund accounts + +**Time:** ~5 minutes. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +<OperatorConfig /> + +## Fund the publisher + +The publisher account pays a small amount of L1 gas every time your node publishes a checkpoint. At current mainnet gas levels a comfortable opening top-up is around **0.01 ETH per sequencer**. + +Paste your L1 RPC URL into the calculator below to scan the last 30 days of network activity and get a runway estimate. Without an RPC the calculator still shows the per-call cost at each scenario, but cannot compute runway, because the propose-rate depends on the live validator-set size. + +<PublisherFundingCalculator /> + +<IfChoice axis="topology" is="ha" label="HA topology"> + +In HA mode the publisher runs on a separate server, but the publisher *address* you funded is the same. The account just needs to be reachable by whichever node is currently the active publisher. + +</IfChoice> + +:::warning Monitor publisher balance +If the publisher balance runs dry, your sequencer cannot publish blocks, and an inactive sequencer is a slashable offense. Set a refill threshold and an alert using `aztec_l1_publisher_balance_eth` and `aztec_l1_publisher_sends_affordable` from your Prometheus stack (see [Metrics reference](../monitoring/metrics-reference#l1-publisher-eth-balance)), leaving enough headroom to refill before the publisher dries up at the worst-case gas price you expect, or set up [automatic top-ups](#optional-automatic-top-ups). +::: + + +## Optional: automatic top-ups + +Your node can refill its publishers from a separate funding account, so a publisher does not run dry between manual top-ups. Once a minute the node reads every publisher's balance, and sends `PUBLISHER_FUNDING_AMOUNT` to each publisher below `PUBLISHER_FUNDING_THRESHOLD`, in a single transaction from the funding account. + +To set it up: + +1. Add a funding account to your keystore. It goes in the top-level `fundingAccount` field, next to `validators`, and takes a private key, or an address held by a remote signer. You can set it with the CLI: + + ```bash + aztec validator-keys set-funding-account [KEYSTORE_PATH] [FUNDING_PRIVATE_KEY_OR_ADDRESS] + ``` + + Pass `--remote-signer [URL]` for an address held by a remote signer, or `--password [PASSWORD]` to store the key encrypted. + +2. Set both environment variables on the node. Both are in ETH (for example `0.05`), and top-ups stay off unless both are set. + + | Env var | What it does | + |---|---| + | `PUBLISHER_FUNDING_THRESHOLD` | A publisher whose balance falls below this amount gets topped up. | + | `PUBLISHER_FUNDING_AMOUNT` | ETH sent to the publisher in each top-up. | + +3. Fund the funding account, and restart the node. + +Keep in mind: + +- The funding account must not also be one of your publishers. If it is, the node logs an error and disables top-ups. +- Set the threshold above the low-balance warning level, so publishers are refilled before the node warns that they are running low. The warning level is 4 times the cost of a proposal at current L1 fees by default (`PUBLISHER_LOW_BALANCE_WARNING_MULTIPLIER`). The node warns if the threshold is below it. +- The node warns when the funding account holds less than 10 top-ups, and logs an error when it cannot pay for any. Watch `aztec_l1_publisher_funder_balance_eth` to refill it in time. + +## Optional: tuning publisher gas settings + +The publisher uses sensible defaults, so most operators do not need to touch these. The settings below let you control what the publisher is willing to pay for L1 inclusion. All are environment variables set on the node. + +:::warning A gas-price cap does not stop the broadcast +`L1_GAS_PRICE_MAX` (gwei) caps the L1 gas price your publisher will pay, to avoid runaway costs during fee spikes. The trap: **the cap does not stop the transaction from being broadcast.** When L1 gas is above your cap, the publisher still sends the proposal at your capped max fee. It loses the inclusion race against higher-paying transactions and does not get mined, but the broadcast still consumes fees, so a cap set too low costs money without producing a block. It also silently misses proposals during every fee spike even on a healthy node (one operator capped near 1 gwei missed roughly 20% of proposals while attesting at 100%). Missed proposals are forgone block rewards now, and skip-block slashing is slated to return in a later upgrade. Size the cap against real L1 conditions using the gas prices the [funding calculator](#fund-the-publisher) reports, not a low round number. +::: + +| Setting | Env var | What it does | +|---|---|---| +| Max gas price | `L1_GAS_PRICE_MAX` | Ceiling on the L1 gas price (gwei) the publisher will pay. See the warning above. | +| Max blob fee | `L1_BLOB_FEE_PER_GAS_MAX` | Ceiling on the blob fee per gas (gwei). The same broadcast caveat applies. | +| Priority fee bump | `L1_PRIORITY_FEE_BUMP_PERCENTAGE` | How much to raise the priority fee on each send, to win inclusion. | +| Priority fee retry bump | `L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE` | Extra priority-fee increase on each retry of a stuck transaction. | +| Minimum priority fee | `L1_MINIMUM_PRIORITY_FEE_PER_GAS_GWEI` | Floor on the computed priority fee (gwei). | +| Gas-limit buffer | `L1_GAS_LIMIT_BUFFER_PERCENTAGE` | Headroom added to the estimated gas limit, to avoid underestimates. | + +If your proposals are frequently not getting included, the usual cause is a `L1_GAS_PRICE_MAX` set too low or priority fees too low for current L1 competition. Raise the cap and the priority-fee settings rather than lowering them. + +## Verify the balance + +<ConfigCode language="bash">{`cast balance {{PUBLISHER_ADDR}} --rpc-url {{ETH_RPC}} --ether`}</ConfigCode> + +## Stake / delegated stake + +You don't supply the AZTEC stake yourself. Delegators provide it. For <Count /> validator slot<IfCount gt={1}>s</IfCount>, you only need: + +| Item | Cost (for <Count /> sequencer<IfCount gt={1}>s</IfCount>) | +|---|---| +| AZTEC stake | **0** (delegators provide) | +| Opening publisher top-up | **<EthTotal /> ETH** (see the calculator above for runway) | +| Provider registration gas | ~0.02 ETH (one-time, regardless of count) | + +Provider rewards: the protocol pays your sequencer rewards into the Rollup contract; you keep your **commission** and forward the rest to delegators. + +## Set your reward and withdrawer addresses + +Provider mode handles attester-level addresses differently: + +- **Coinbase (per attester, per delegation):** when a delegator stakes to your provider, the protocol creates a [Splits-V2 Split contract](https://docs.splits.org/core/split) for that delegation. In the default flow, you update **that specific attester's keystore** to set `coinbase = <Split contract address>` and reload the keystore. See [Configure environment](./configure-environment#after-delegation-set-coinbase-to-the-split-contract) for the per-delegation procedure and the payout-script alternative. +- **Withdrawer (per attester):** set by the delegated-stake flow, not by you. You don't need to fill the **Withdrawer Address** field above for delegated attesters. + +What you *do* configure at registration time (one-time, applies to the whole provider entry, lives on the StakingRegistry): + +- **Provider admin** — the L1 address authorized to manage your provider entry (`addKeysToProvider`, `updateProviderTakeRate`, etc.). Use a multisig or hardware wallet you control. +- **Rewards recipient** — the L1 address where your **commission share** lands when each Split distributes. Long-lived multisig recommended. + +Both are passed to `registerProvider(...)` in step 8. They're stored on the StakingRegistry, not the keystore. + +For the role map (provider admin vs attester withdrawer vs coinbase Split vs rewards recipient), see the [keys-and-addresses deep-dive](./generate-keystore#the-keys-the-addresses-and-who-calls-what) and the [Become a staking provider](/operate/operators/setup/become_a_staking_provider) guide. + +<details open> +<summary>What just happened?</summary> + +You confirmed the publisher account has enough ETH for at least a few weeks of attestation gas. The stake side comes from delegators: registering as a provider creates a queue in the Governance Staking Escrow, delegators stake to it, and each delegated stake spawns one attester position you operate. + +</details> + diff --git a/docs/lines/v6/docs-operate/operators/provider/generate-keystore.mdx b/docs/lines/v6/docs-operate/operators/provider/generate-keystore.mdx new file mode 100644 index 00000000000..5105a1f994e --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/generate-keystore.mdx @@ -0,0 +1,194 @@ +--- +title: "Generate keystore" +description: "Create the publisher mnemonic and validator keystore. Action steps first, theory at the bottom." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/cli/src/cmds/validator_keys/*", "labs/yarn-project/node-keystore/src/schemas.ts"] +--- + +# Generate keystore + +**Time:** ~5 minutes. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +## 1. Generate a publisher mnemonic (24 words) + +```bash +cast wallet new-mnemonic --words 24 +``` + +:::danger Back up the mnemonic NOW, before anything else +**Write it down on paper, or store it on a hardware device. Do this before you continue.** + +This 24-word phrase is the **only** way to regenerate your publisher private key if you lose the server. **There is no recovery if you skip this step.** Lose the mnemonic, lose the funds in the publisher account, lose the ability to publish, get slashed for inactivity, lose your stake. + +Do **not**: +- Store it on the server running the node. +- Email it to yourself. +- Save it in a password manager that syncs to the cloud without a hardware-key second factor. +- Take a screenshot. +::: + +## 2. Derive the publisher private key + +```bash +cast wallet derive-key "your 24 word mnemonic phrase here" +``` + +Copy the returned private key into the **Publisher Private Key** field in the configuration panel. + +<OperatorConfig /> + +## 3. Generate the validator keystore + +The configuration panel says you'll register **<Count />** sequencer identit<IfCount eq={1}>y</IfCount><IfCount gt={1}>ies</IfCount>. Adjust the **Sequencer count** field above if that's wrong before running the command. + +<ConfigCode language="bash">{`aztec validator-keys new \\ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \\ + --staker-output \\ + --gse-address {{GSE_ADDR}} \\ + --l1-rpc-urls {{ETH_RPC}} \\ + --l1-chain-id {{L1_CHAIN_ID}} \\ + --publishers {{PUBLISHER_KEY}} \\ + --count {{COUNT}}`}</ConfigCode> + +:::warning Provider coinbase is set per delegation, not here +A provider's coinbase is **not** a single wallet you choose now. Each time a delegator stakes to you, the protocol creates a **Splits-V2 Split contract** for that delegation and, in the default flow, you update that specific attester's `coinbase` field to point at the new Split contract address, then reload the keystore. (Providers using the [payout-script flow](./configure-environment#alternative-eoa-coinbase-with-the-payout-script) point coinbase at an address they control instead.) The procedure for updating coinbase after a delegation arrives lives in [Configure environment](./configure-environment#after-delegation-set-coinbase-to-the-split-contract). For now, omit `--coinbase` so it defaults to the attester address, so the keystore is functional and you'll update it per attester later. Failing to update coinbase after a delegation means rewards route to your hot attester key instead of the Split, breaking your commission accounting and the delegator's claim flow. +::: + +<IfCount gt={1}> + +All <Count /> identities are derived from the same mnemonic using sequential derivation paths and end up in the same two files (`key1.json` and `key1_staker_output.json`). The public file is a JSON array with <Count /> objects, one per validator. + +</IfCount> + +This writes two files in `~/.aztec/keystore/`: + +- **`key1.json`** is the private keystore. Never share it, never commit it to git. +- **`key1_staker_output.json`** is the public keystore. Safe to share. You'll upload it to the staking dashboard when you [register as sequencer](./register-sequencer). + +:::tip Securing the keystore in production +This command stores private keys inline in `key1.json`. That's the simplest setup, but means anyone with read access to the file gets your keys. At a minimum, restrict file permissions (`chmod 600 ~/.aztec/keystore/key1.json`), use full-disk encryption, and limit who can SSH into the server. For more advanced storage (encrypted JSON V3 keystores, remote signers like Web3Signer), see [Key storage methods](/operate/operators/keystore/storage-methods). +::: + +:::danger Make sure you have the mnemonic saved offline +The mnemonic from step 1 is your **single backup**. Both the publisher ECDSA key and the attester BLS key are derived deterministically from it, so re-running the same `aztec validator-keys new` command with the same mnemonic reproduces `key1.json` byte-for-byte. If the server's disk fails and you have the mnemonic, you can rebuild the node from scratch. + +Without the mnemonic and without a copy of `key1.json`, a disk loss means a forced exit and re-registration, including the withdrawal delay of about 10 days. + +Optional, for faster recovery: keep an encrypted copy of `key1.json` and `key1_staker_output.json` somewhere off this server. Restoring from a copy is faster than re-deriving from the mnemonic. The command below uses GPG symmetric encryption (most Linux distros ship `gpg`; macOS users can `brew install gnupg`). It prompts for a passphrase, asks you to confirm it, and writes the encrypted archive: + +```bash +tar czf - ~/.aztec/keystore/key1.json ~/.aztec/keystore/key1_staker_output.json \ + | gpg --symmetric --cipher-algo AES256 --output aztec-keystore-backup.tar.gz.gpg +``` + +Move `aztec-keystore-backup.tar.gz.gpg` off this server (hardware drive, encrypted cloud bucket, air-gapped machine). To restore later, on a fresh server, run: + +```bash +gpg --decrypt aztec-keystore-backup.tar.gz.gpg | tar xzf - -C / +``` + +GPG prompts for the same passphrase. The keystore lands back in `~/.aztec/keystore/key1.json`. +::: + +## 4. Extract your addresses + +<IfCount eq={1}> + +```bash +# Attester address (your sequencer's identifier) +jq -r '.[0].attester' ~/.aztec/keystore/key1_staker_output.json + +# Publisher address (the account that pays L1 gas) +jq -r '.validators[0].publisher[0]' ~/.aztec/keystore/key1.json | xargs cast wallet address --private-key +``` + +Paste both into the configuration panel above. + +</IfCount> + +<IfCount gt={1}> + +You generated <Count /> validators. List all of them at once: + +```bash +# All attester addresses (one per line) +jq -r '.[].attester' ~/.aztec/keystore/key1_staker_output.json + +# All publisher addresses (one per line) +jq -r '.validators[].publisher[0]' ~/.aztec/keystore/key1.json | \ + xargs -n1 cast wallet address --private-key +``` + +Paste **the first attester** and **the first publisher address** into the configuration panel. The configuration panel holds one address each because the Playbook uses the first validator as the canonical example; downstream multi-validator commands extract all <Count /> from the keystore directly. + +</IfCount> + +## 5. Verify the keystore loaded correctly + +```bash +cat ~/.aztec/keystore/key1.json | jq . +``` + +Expected structure: + +```json +{ + "schemaVersion": 1, + "validators": [{ + "attester": { "eth": "0x...", "bls": "0x..." }, + "publisher": ["0x..."], + "feeRecipient": "0x000...000", + "coinbase": "0x..." + }] +} +``` + +## Before you continue + +- **Mnemonic written down on paper or stored on a hardware device.** Not on this server. Not in a synced password manager. Not in a screenshot. +- `key1.json` and `key1_staker_output.json` exist in `~/.aztec/keystore/`. +- Attester address and Publisher address filled in the configuration panel. + +If you cannot honestly check off the first item, **stop now and do it**. The next steps fund accounts with real ETH and AZTEC; losing the mnemonic after that point costs money. + +--- + +<details id="the-keys-the-addresses-and-who-calls-what"> +<summary>The keys, the addresses, and who calls what</summary> + +A sequencer involves a small cast of distinct identities. They can be the same wallet for a casual setup, or fully separated for a production setup. + +**Cryptographic keys (live in `key1.json`):** + +- **Attester ETH key (ECDSA)** identifies your sequencer on the network. The address derived from this key is what gets registered in the Rollup contract and what other operators see. It can also start an exit of its position (an [attester-initiated exit](/operate/operators/sequencer-management/exiting-a-sequencer)). The stake still goes to the withdrawer, but anyone holding this key can take the sequencer out of the active set, so protect it like the node itself. +- **Attester BLS key (BLS12-381)** signs block attestations. BLS signatures aggregate, so when a 32-member committee attests to a block, all 32 signatures combine into one short signature on L1. That saves gas. +- **Publisher key (ECDSA)** is a separate Ethereum account that pays L1 gas when your node submits proofs and attestations. Keeping it separate means you can rotate the funded account without rotating your sequencer identity. + +**Operational addresses (passed in at registration time, stored onchain):** + +- **Caller** is whoever calls `Rollup.deposit(...)` to register the sequencer. This is the address whose 200K AZTEC gets pulled in as stake (verified against `StakingLib.deposit`: `safeTransferFrom(msg.sender, ...)`). Typically a treasury or operator multisig that holds the AZTEC. +- **Withdrawer** is the address that controls the stake: it can start an exit with `initiateWithdraw`, names the recipient for every exit (including attester-initiated ones), signs governance delegations ([governance docs](/operate/operators/sequencer-management/creating_and_voting_on_proposals)), and receives residual stake on slash-driven ejection ([slashing docs](/operate/operators/sequencer-management/slashing_and_offenses)). Use a cold wallet or multisig you control. It does **not** need to be the same as the caller. +- **Coinbase** is an L1 Ethereum address that accumulates sequencer rewards on the Rollup contract. You claim them via `claimSequencerRewards(coinbase)`. +- **Fee recipient** is an L2 Aztec address that would collect L2 transaction fees. L2 fees are not enabled today, so this is a placeholder set to `0x000...000`. + +**Casual setup** uses one operator wallet for caller + withdrawer + coinbase, plus a hot publisher key on the box, plus the in-keystore attester. **Production setup** keeps caller and withdrawer on a multisig, attester hot on the node, publisher funded separately, and coinbase on a treasury wallet. Four (or five) distinct identities, each rotatable on its own. + +`--gse-address` in the keystore-generation command points at the **Governance Staking Escrow** contract that manages validator registration and stake. It is a different address on testnet vs mainnet; the configuration panel handles that for you. + +For the deeper schema, advanced patterns (multi-validator, hardware-wallet publisher), and recovery procedures, see the [Keystore Management section](/operate/operators/keystore). + +</details> + +<details open> +<summary>Something wrong?</summary> + +- **"Error: could not connect to RPC"** — Your L1 RPC URL may be wrong or rate-limited. Test with `cast block-number --rpc-url YOUR_URL`. +- **`jq` shows an empty validators array** — Re-run the command. Ensure `--publishers` has a valid private key with the `0x` prefix. + +</details> + +**Stuck?** Ask in [Discord](https://discord.com/invite/aztec). diff --git a/docs/lines/v6/docs-operate/operators/provider/index.mdx b/docs/lines/v6/docs-operate/operators/provider/index.mdx new file mode 100644 index 00000000000..bfd46e27e73 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/index.mdx @@ -0,0 +1,47 @@ +--- +title: "Staking provider: zero to attesting" +description: "An opinionated, end-to-end walkthrough that takes a new staking provider from a blank server to registered, delegator-backed, attesting Aztec sequencers." +displayed_sidebar: operatorsSidebar +--- + +# Staking provider: zero to attesting + +The fastest path from a blank server to attesting Aztec sequencers backed by delegated stake. Pick your network and topology, fill the configuration panel once, copy commands as you go. + +:::info One of two ways to learn the operator stack +This **guided setup** is one entry point into the operator docs. It's the right starting place if you've never run an Aztec sequencer before. For lookup material once your node is live, the topic-organized reference pages stay authoritative: [Setup](/operate/operators/setup/sequencer_management) (deeper details on each subsystem), [Keystore management](/operate/operators/keystore), [Monitoring](/operate/operators/monitoring), [Sequencer management](/operate/operators/sequencer-management), and the [CLI / API references](/operate/operators/reference/cli-reference). When this guide hands you off to a deeper topic, it links to the reference page for that topic. +::: + +## Pick your network and topology + +<TrackPicker hideStake /> + +<ForceStakeMode mode="provider" /> + +The guide adapts to your choices: +- **Network** swaps RPC URL hints, contract addresses, and Docker image tag in every code block on every page. +- **Topology** changes the server requirements and the Docker Compose file (HA mode runs the same attester across two coordinated nodes). + +Your choices persist across pages. Flip any toggle at any time. Staking your own AZTEC instead of running on delegations? Switch to the [Solo sequencer guide](/operate/operators/solo-sequencer/). + +## What you'll do + +| # | Step | Time | +|---|------|------| +| 1 | [Pre-flight](/operate/operators/provider/pre-flight) | ~2 min | +| 2 | [Install Aztec toolchain](/operate/operators/provider/install-toolchain) | ~2 min | +| 3 | [Generate keystore](/operate/operators/provider/generate-keystore) | ~5 min | +| 4 | [Fund your accounts](/operate/operators/provider/fund-accounts) | ~5 min | +| 5 | [Configure environment](/operate/operators/provider/configure-environment) | ~5 min | +| 6 | [Create Docker Compose](/operate/operators/provider/docker-compose) | ~3 min | +| 7 | [Start the node](/operate/operators/provider/start-node) | 5 to 15 min | +| 8 | [Register as staking provider](/operate/operators/provider/register-sequencer) | ~10 min | +| 9 | [Verify everything works](/operate/operators/provider/verify) | ~5 min | +| 10 | [Updates, alerts, rewards](/operate/operators/provider/day-2-essentials) | ~5 min | +| 11 | [Provider operations](/operate/operators/provider/provider-operations) | ~5 min | + +## How to read it + +- **Code blocks personalize themselves.** Starting in [generate keystore](/operate/operators/provider/generate-keystore), a configuration panel appears at the top of pages that need it. Paste your RPC URLs, keys, and addresses once. Every code block on every page after that auto-fills with your values. Commands fill in with your real values and copy ready to run; keys stay masked in the panel inputs and live in session-only storage. +- **"What just happened?"** sections at the end of each step explain the why. Skip them on the first pass; come back when something breaks. +- **Sections in dashed boxes with a coloured tag** (such as "HA") only show because of your topology choice. Flip the toggles up top to see other paths. diff --git a/docs/lines/v6/docs-operate/operators/provider/install-toolchain.mdx b/docs/lines/v6/docs-operate/operators/provider/install-toolchain.mdx new file mode 100644 index 00000000000..10e071e3aab --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/install-toolchain.mdx @@ -0,0 +1,79 @@ +--- +title: "Install the Aztec toolchain" +description: "Install the aztec CLI and Foundry, the two tools you need to generate keys and interact with Aztec L1 contracts." +displayed_sidebar: operatorsSidebar +references: ["labs/aztec-up/bin/*"] +--- + +# Install the Aztec toolchain + +**Time:** ~2 minutes. The Aztec CLI is needed to generate validator keys and interact with L1 contracts. Foundry's `cast` is used throughout the Playbook to query contracts. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +## 1. Install Aztec + +```bash +bash -i <(curl -sL https://install.aztec.network) +``` + +The installer pulls the `latest` tag by default. The current Aztec-recommended release is <RecommendedVersion />. To pin to it, set `VERSION` before running the installer: + +<ConfigCode language="bash">{`VERSION={{RECOMMENDED_VERSION}} bash -i <(curl -sL https://install.aztec.network)`}</ConfigCode> + +## 2. Install Foundry + +You'll use `cast` later to query contracts and claim rewards. + +```bash +curl -L https://foundry.paradigm.xyz | bash && foundryup +``` + +## 3. Make the installation persistent + +Both installers append `PATH` lines to a shell rc file, but new shell sessions only pick them up if that rc file is loaded automatically. The fastest fix that survives reboots and new SSH sessions: + +```bash +# Source the rc file once in this session +source ~/.bashrc # or ~/.zshrc if you use zsh + +# Make sure login shells pick it up too (covers non-interactive SSH) +grep -q 'source ~/.bashrc' ~/.bash_profile 2>/dev/null || \ + echo '[ -f ~/.bashrc ] && source ~/.bashrc' >> ~/.bash_profile +``` + +If you run the node under `systemd` or as a different user, that process won't read your rc files. Set `PATH` explicitly in the unit file or use absolute paths (`/home/<user>/.aztec/bin/aztec`, `/home/<user>/.foundry/bin/cast`). + +## 4. Verify the installation + +Open a new shell or run `source ~/.bashrc`, then: + +```bash +aztec --version +cast --version +``` + +Expected output: `aztec` followed by the version you installed (the recommended pin is <RecommendedVersion />, or whatever `latest` resolves to today), plus `cast Version: ...`. + +<details open> +<summary>What just happened?</summary> + +You installed two tools and made them available in every new shell. + +- **`aztec` CLI** generates BLS and ECDSA keypairs for your sequencer and registers your validator on L1. +- **Foundry (`cast`)** is a CLI for interacting with Ethereum smart contracts. You'll use `cast call` to check your sequencer status and `cast send` to claim rewards. +- **The `~/.bash_profile` line** ensures non-interactive shells (the kind `ssh user@host "aztec --version"` uses) load `~/.bashrc` and find the new tools on `PATH`. + +</details> + +<details open> +<summary>Something wrong?</summary> + +- **`command not found: aztec` after reopening shell** — Your shell may not be `bash`. Run `echo $SHELL`. If it's `zsh`, source `~/.zshrc` instead and add the equivalent line to `~/.zprofile`. +- **Tools work in interactive shell but not via `ssh user@host "aztec --version"`** — The `~/.bash_profile` step above fixes this. Make sure that file exists and contains the source line. +- **`curl` fails** — Check your firewall allows HTTPS. Try `curl -I https://install.aztec.network`. + +</details> + diff --git a/docs/lines/v6/docs-operate/operators/provider/pre-flight.mdx b/docs/lines/v6/docs-operate/operators/provider/pre-flight.mdx new file mode 100644 index 00000000000..f208e59d3c6 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/pre-flight.mdx @@ -0,0 +1,141 @@ +--- +title: "Pre-flight" +description: "Hardware, software, network access, and tokens you need before starting. Adapts to your topology choice." +displayed_sidebar: operatorsSidebar +--- + +import NodeHardware from "../_node_hardware.mdx"; + +# Pre-flight + +**Time:** ~2 minutes. Confirm you have everything before starting. Every item is required. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +:::warning Reliability is a hard requirement +An Aztec sequencer must stay online 24/7 with a stable internet connection. The protocol slashes 2,000 AZTEC per offense when you miss attestation duties. + +Before starting, confirm your setup can sustain: + +- **Power:** backed by UPS or hosted on infrastructure with N+1 power. +- **Network:** not behind a NAT or firewall you don't control; not a cellular hotspot; not a residential connection prone to ISP outages. +- **Compute:** machine that doesn't sleep, doesn't reboot for OS updates without your control, and doesn't share resources with workloads that could cause downtime. +- **Operator availability:** someone responds to alerts within a few hours (you, an on-call rotation, or a monitoring service). + +If any of these is uncertain, consider delegating instead. +::: + +## Hardware + +Each server needs: + +<NodeHardware /> + +The CPU must be Skylake or newer: on x86 the node's cryptography library (barretenberg) is compiled with `-march=skylake`, so it uses instructions that older chips lack and fails on them (some 2015 CPUs are pre-Skylake). Co-hosting an L1 node needs more storage; see [Hardware spec](/operate/operators/concepts/hardware#co-hosting-an-l1-node). + +<IfChoice axis="topology" is="single"> + +You need **one server** with a static public IP. + +<IfCount gt={1}> + +You configured **<Count />** attesters. Running more attester identities on the same node does not multiply the spec: the node validates each block proposal once and then signs cheaply per attester, so one node with 1 attester and one node with many attesters need roughly the same CPU and RAM. What multiplies the spec is the number of **nodes** you run, not the attester count. See [Hardware spec](/operate/operators/concepts/hardware) for details. + +</IfCount> + +</IfChoice> + +<IfChoice axis="topology" is="ha" label="HA topology"> + +In HA mode the **same attester identity** runs across multiple nodes for redundancy. The nodes coordinate via a shared PostgreSQL database that prevents double-signing. This is not the same as splitting N attesters across N nodes. + +You need **two servers** running the same attester identity, each meeting the spec above, with: + +- **Network:** static public IPs on both +- **Shared PostgreSQL** reachable by both nodes for HA coordination +- **Private link:** the nodes need a low-latency path to the shared database. Same VPC or a Wireguard mesh works. + +If one node fails, the other continues attesting without missing duties. See [High availability sequencers](/operate/operators/setup/high_availability_sequencers) for the full setup. + +</IfChoice> + +## Software + +You need this on **every** server in your topology. + +- **OS:** Ubuntu 22.04+, Debian 12+, or macOS. Windows WSL2 also works but is not recommended for production. +- **Docker + Docker Compose:** installed and running. Verify with: + +```bash +docker --version && docker compose version +``` + +- **jq:** JSON processor used in verification commands. `sudo apt install -y jq` on Ubuntu/Debian or `brew install jq` on macOS. Verify with `jq --version`. + +<details open> +<summary>Need to install Docker?</summary> + +**Ubuntu / Debian:** + +```bash +sudo apt-get update +sudo apt-get install -y ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ + -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +echo "deb [arch=$(dpkg --print-architecture) \ + signed-by=/etc/apt/keyrings/docker.asc] \ + https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +sudo apt-get update +sudo apt-get install -y docker-ce docker-ce-cli \ + containerd.io docker-compose-plugin + +sudo usermod -aG docker $USER +newgrp docker +``` + +**macOS:** install [Docker Desktop](https://docs.docker.com/desktop/install/mac-install/). + +**Verify:** `docker run hello-world`. + +</details> + +## Network access + +- **L1 Ethereum endpoints:** an execution RPC, a blob-serving consensus (beacon) RPC, and a debug/trace RPC. Self-hosted (for example Geth plus Lighthouse or Prysm in supernode mode) or a hosted provider that supports all three. The beacon client must serve blob sidecars, which has specific version and flag requirements. See [L1 RPC requirements](/operate/operators/concepts/l1-rpc) before you choose a provider; it is the most common setup blocker. +- **Port 40400** open for both TCP and UDP (P2P). +- **Port 8080** available locally (Aztec HTTP API). Do not expose it publicly. + +<IfChoice axis="topology" is="ha" label="HA topology"> + +Additional ports for the publisher node: + +- **Port 8880** (admin API) reachable only over the private link from the sequencer node. **Never** expose this to the internet. + +</IfChoice> + +## Tokens + +- **AZTEC tokens:** zero required from you. Delegators will provide the stake. Your provider registration in the GSE costs only L1 gas. +- **ETH for gas:** around 0.01 ETH per sequencer is a comfortable opening top-up at current L1 prices. Size it precisely with the [funding calculator](./fund-accounts#fund-the-publisher), which scans real network activity using your L1 RPC. +- **Commission rate:** decide before registering. + +<details open> +<summary>What just happened?</summary> + +You confirmed your servers meet Aztec's minimum requirements. The node runs as a Docker container that connects to Ethereum L1 (for anchoring proofs and reading rollup state) and the Aztec P2P network (for receiving transactions and participating in consensus). + +The delegated stake backing each attester is required by the Rollup contract to prevent Sybil attacks. + +</details> + +--- + +**Stuck?** Ask in [Discord](https://discord.com/invite/aztec). diff --git a/docs/lines/v6/docs-operate/operators/provider/provider-operations.mdx b/docs/lines/v6/docs-operate/operators/provider/provider-operations.mdx new file mode 100644 index 00000000000..a23d3b7af01 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/provider-operations.mdx @@ -0,0 +1,214 @@ +--- +title: "Provider operations" +description: "Ongoing operations for staking providers: Split-contract handling, provider config updates, queue management, and recovery from common failure modes." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/node-keystore/src/*", "labs/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts", "labs/yarn-project/cli/src/cmds/validator_keys/*"] +--- + +# Provider operations + +A provider-specific operations reference. All commands assume you've completed [register as sequencer](./register-sequencer) (provider mode), and your **provider ID** is filled in the configuration panel. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +<OperatorConfig /> + +## Split contracts: one per delegation + +When a delegator stakes to your provider, the protocol auto-creates a [Splits-V2 Split contract](https://docs.splits.org/core/split) for that delegation. The Split holds the agreed commission split (your `providerTakeRate` to your `providerRewardsRecipient`, the rest to the delegator's vault). It is **frozen** at the commission rate that was active *when the delegation happened* — later `updateProviderTakeRate` calls only affect *new* delegations. + +**Per-delegation lifecycle:** + +1. Delegator stakes to your provider on the dashboard. +2. Protocol pulls one keystore off your queue. +3. Protocol creates the Split contract for that delegation. +4. Protocol activates the attester. +5. **You** update that attester's keystore: set `coinbase = <Split address>`, then reload the keystore with an admin API call (no restart needed). +6. Rewards land in the Split. Getting them out is two permissionless steps: anyone calls `distribute()` on the Split, which moves the balance into the Splits warehouse and credits each recipient; then each recipient calls `withdraw()` on the warehouse. Your commission share goes to your rewards-recipient address; the delegator's share goes to their vault. + +You repeat step 5 for every new delegation. This can be fully automated: the protocol emits a `StakedWithProvider` event on each delegation that carries the new `attester` and its `coinbaseSplitContractAddress`, so a listener can write the coinbase into the keystore and reload it without a restart. See [Automate the coinbase update](#automate-the-coinbase-update) below. + +:::info Alternative: one EOA coinbase, distribute offchain +The per-delegation coinbase update is the default flow, not the only one. You can instead point every attester's coinbase at a single address you control (EOA, multisig, or Safe) and run the [payout script](https://github.com/AztecProtocol/aztec-staking-payout) to compute and send each delegator's share at a published commission and cadence. This removes the per-delegation keystore edit entirely. The trade-off: you take on a recurring offchain distribution (the script emits a Multicall3 ERC20 batch whose gas scales with your delegator count, plus a per-run audit JSON for delegators to verify), and the split is enforced by your published commitment rather than onchain by the Split contract. See [the configuration step](./configure-environment#alternative-eoa-coinbase-with-the-payout-script) and the [forum announcement](https://forum.aztec.network/t/operator-commission-adjustments/8588). +::: + +### Find a Split address + +Each delegation emits a `StakedWithProvider` event from the staking registry that pairs the activated `attester` with its `coinbaseSplitContractAddress`. Reading this event is the scalable way to get the mapping, and it is what an automated setup listens for. Query your provider's delegations directly: + +<ConfigCode language="bash">{`RPC={{ETH_RPC}} +# RPC providers cap eth_getLogs to a limited block range per call, +# so query a recent window rather than the whole chain. +LATEST=$(cast block-number --rpc-url "$RPC") +cast logs \\ + --from-block $((LATEST - 900)) --to-block latest \\ + --address {{STAKING_REGISTRY_ADDR}} \\ + 0xc91c8b4e934fb8f46c6d08a34d60ceb8dbe4ab6844a7040ca4c580e5a837d5c7 \\ + $(cast to-uint256 {{PROVIDER_ID}}) \\ + --rpc-url "$RPC"`}</ConfigCode> + +Pass the topics directly: the first is the `keccak256` hash of the `StakedWithProvider` signature, the second is your `{{PROVIDER_ID}}` padded to 32 bytes, so only your delegations come back. In each returned log the third indexed topic is the `attester`, and the first non-indexed field in `data` is its `coinbaseSplitContractAddress`. An unbounded query (no block range) scans from genesis and most RPCs reject it, so keep each call within your provider's block-range cap (which varies by provider) and page backward if you need older delegations. + +For a quick one-off lookup you can also read it from the staking dashboard: open your provider page, expand **Sequencer Registered (x)**, and read the Split address next to the attester. + +### Automate the coinbase update + +A production provider does not do this by hand. Subscribe to the `StakedWithProvider` event filtered by your `{{PROVIDER_ID}}`, and on each event set the new attester's `coinbase` to the `coinbaseSplitContractAddress` from the log, then reload the keystore over the admin API (no restart). The manual `distribute()` and `withdraw()` reward steps are permissionless and can be scheduled on the same listener. + +### Update the attester's keystore + +Edit `~/.aztec/keystore/keyN.json`: + +```json +{ + "schemaVersion": 1, + "validators": [{ + "attester": { "eth": "0x...", "bls": "0x..." }, + "publisher": ["0x..."], + "coinbase": "0x<SPLIT_CONTRACT_ADDRESS>", + "feeRecipient": "0x000...000" + }] +} +``` + +Then tell the running node to reload the keystore from disk, which applies the new `coinbase` without a restart: + +```bash +docker exec -it aztec-sequencer curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_reloadKeystore","params":[],"id":1}' +``` + +The admin API requires an API key by default. The node generates one at first startup and prints it once to the logs; it is reused across restarts because you set `DATA_DIRECTORY`. Pass it with the `x-api-key` header (or `Authorization: Bearer [ADMIN_API_KEY]`). See [admin API key authentication](../reference/changelog/v4.md#admin-api-key-authentication) for the key lifecycle and the opt-out flag. + +`aztecAdmin_reloadKeystore` re-reads the keystore directory and applies each validator's attester key, coinbase, and fee recipient. It does not reload L1 publisher signers, prover keys, or HA signer database connections; those still require a restart. See the [node API reference](../reference/node-api-reference.md#admin-api). Restarting the node also loads the new keystore, if you prefer: + +```bash +docker compose restart aztec-sequencer +``` + +## Queue management + +Your queue holds the unassigned keystores. Each delegation pulls one off. + +### Check queue depth + +<ConfigCode language="bash">{`cast call {{STAKING_REGISTRY_ADDR}} \\ + "getProviderQueueLength(uint256)(uint256)" \\ + {{PROVIDER_ID}} \\ + --rpc-url {{ETH_RPC}}`}</ConfigCode> + +Returns a single integer. Below your expected weekly delegation flow = you risk an empty queue. + +### Add more keystores + +Generate the new keystores with `aztec validator-keys new --staker-output`, then call `addKeysToProvider` (use the [following helper script](https://gist.github.com/koenmtb1/1b665d055fbc22581c288f90cdc60d88) to format the call). `--staker-output` is what produces the registration array (attester address plus BLS proof of possession) that `addKeysToProvider` consumes. Max 100 keystores per transaction; verify uniqueness before adding (the contract does **not** check for duplicates). + +:::warning Advance the derivation index when reusing a mnemonic +`aztec validator-keys new` always derives from address index 0 by default. If you generate a second batch from the **same mnemonic** without moving the index, you get the **same** attesters again, and the duplicates break your queue. Generate each new batch with `--address-index <N>`, where `N` is the number of attesters you have already derived from that mnemonic. For example, after a first batch of 50, generate the next 50 with: + +<ConfigCode language="bash">{`aztec validator-keys new --count 50 --staker-output \\ + --mnemonic "<your mnemonic>" \\ + --address-index 50 \\ + --gse-address {{GSE_ADDR}} \\ + --l1-rpc-urls {{ETH_RPC}} \\ + --l1-chain-id {{L1_CHAIN_ID}}`}</ConfigCode> + +`--count 50` writes all 50 into one staker output file. Reusing one mnemonic with an index offset means a single backup to secure and easy bookkeeping; a fresh mnemonic per batch (or per machine) avoids the index tracking and keeps each batch's keys independent if one backup is ever exposed. + +Note: `aztec validator-keys add <keystore>` appends attesters to an existing **node** keystore (and auto-advances the index for you), but it does **not** produce a staker output, so it cannot feed `addKeysToProvider`. Use `new --staker-output` for queue keystores. +::: + +### Recover from a duplicate keystore at the head of the queue + +If you accidentally added a duplicate keystore and a delegation now reverts, drip past the bad entries: + +<ConfigCode language="bash">{`# Drop the top N entries from your queue without activating them +cast send {{STAKING_REGISTRY_ADDR}} \\ + "dripProviderQueue(uint256,uint256)" \\ + {{PROVIDER_ID}} <number-to-drip> \\ + --rpc-url {{ETH_RPC}} \\ + --private-key <provider-admin-private-key>`}</ConfigCode> + +Run with the smallest number that gets past the offending duplicate. Each call costs gas, so don't over-drip. + +## Exit delegated positions + +You hold the attester keys for your delegations, so you can exit them yourself with an attester-initiated exit, without your delegators' withdrawer keys. Sign one exit authorization per attester and relay them all in a single transaction: + +<ConfigCode language="bash">{`aztec sign-attester-exit --private-key [ATTESTER_PRIVATE_KEY] --attester [ATTESTER_ADDRESS] \\ + --rollup {{ROLLUP_ADDR}} --l1-chain-id {{L1_CHAIN_ID}} --deadline [UNIX_TIMESTAMP] \\ + --output exits.json --append + +# Simulate first (no key needed), then submit from any funded key +aztec initiate-withdraw-by-attester-batch --dry-run --authorizations exits.json \\ + --rollup {{ROLLUP_ADDR}} --l1-chain-id {{L1_CHAIN_ID}} --l1-rpc-urls {{ETH_RPC}} + +aztec initiate-withdraw-by-attester-batch --authorizations exits.json \\ + --rollup {{ROLLUP_ADDR}} --l1-chain-id {{L1_CHAIN_ID}} --l1-rpc-urls {{ETH_RPC}} \\ + --private-key [RELAYER_PRIVATE_KEY]`}</ConfigCode> + +The stake goes to each delegator's withdrawer address. Attester-initiated exits are rate-limited to about 5% of the sequencer set per governance withdrawal delay, so for a batch larger than the current limit, `--up-to-limit` exits as many as the limit allows and leaves the rest for a later submission. + +See [Exiting a sequencer](/operate/operators/sequencer-management/exiting-a-sequencer) for the full procedure, the limits, and troubleshooting. + +## Provider config updates + +All three calls below must come from your `providerAdmin` address (the one returned by `providerConfigurations` field 1). + +### Change commission rate + +<ConfigCode language="bash">{`cast send {{STAKING_REGISTRY_ADDR}} \\ + "updateProviderTakeRate(uint256,uint16)" \\ + {{PROVIDER_ID}} <new-bps> \\ + --rpc-url {{ETH_RPC}} \\ + --private-key <provider-admin-private-key>`}</ConfigCode> + +`<new-bps>` is basis points: `500` = 5%, `700` = 7%, etc. + +:::warning Existing delegations keep the old rate +Commission changes apply only to **new** delegations. Existing delegations' Split contracts are frozen at the rate that was active when they were created. Communicate the change publicly so delegators know what they'll get if they re-delegate. +::: + +### Change rewards recipient + +<ConfigCode language="bash">{`cast send {{STAKING_REGISTRY_ADDR}} \\ + "updateProviderRewardsRecipient(uint256,address)" \\ + {{PROVIDER_ID}} <new-recipient> \\ + --rpc-url {{ETH_RPC}} \\ + --private-key <provider-admin-private-key>`}</ConfigCode> + +Takes effect immediately for **new** delegations. Existing Split contracts continue paying the old recipient (the recipient address is baked in when the Split is created). + +### Rotate provider admin + +<ConfigCode language="bash">{`cast send {{STAKING_REGISTRY_ADDR}} \\ + "updateProviderAdmin(uint256,address)" \\ + {{PROVIDER_ID}} <new-admin> \\ + --rpc-url {{ETH_RPC}} \\ + --private-key <current-admin-private-key>`}</ConfigCode> + +Use this for cold-storage rotation, multisig handoff, or compromise recovery. **The new admin is the only one who can subsequently call `addKeysToProvider` and the other update functions.** + +## Read your full provider state + +<ConfigCode language="bash">{`# Config (admin, take rate, rewards recipient) +cast call {{STAKING_REGISTRY_ADDR}} \\ + "providerConfigurations(uint256)(address,uint16,address)" \\ + {{PROVIDER_ID}} \\ + --rpc-url {{ETH_RPC}} + +# Queue length +cast call {{STAKING_REGISTRY_ADDR}} \\ + "getProviderQueueLength(uint256)(uint256)" \\ + {{PROVIDER_ID}} \\ + --rpc-url {{ETH_RPC}} + +# Pending admin (only nonzero if a transfer is in flight) +cast call {{STAKING_REGISTRY_ADDR}} \\ + "pendingProviderAdmins(uint256)(address)" \\ + {{PROVIDER_ID}} \\ + --rpc-url {{ETH_RPC}}`}</ConfigCode> diff --git a/docs/lines/v6/docs-operate/operators/provider/register-sequencer.mdx b/docs/lines/v6/docs-operate/operators/provider/register-sequencer.mdx new file mode 100644 index 00000000000..bec7e6b3748 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/register-sequencer.mdx @@ -0,0 +1,146 @@ +--- +title: "Register as staking provider" +description: "Register your provider entry on the Staking Registry and queue sequencer keystores for delegators." +displayed_sidebar: operatorsSidebar +references: ["docs/static/scripts/add-keys-to-provider.sh", "labs/yarn-project/cli/src/cmds/validator_keys/*"] +--- + +# Register as staking provider + +**Time:** ~10 minutes. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +<OperatorConfig /> + +## Staking-provider registration + +You're not staking your own tokens. You're registering as a provider that delegators can stake to. The flow is three transactions plus a metadata PR. + +You'll need: +- The attester addresses you generated (from `key1_staker_output.json`). +- A **provider admin address** that will manage your provider entry (call `addKeysToProvider`, update commission, etc.). Use a multisig or hardware wallet you control. +- A **rewards recipient address** that receives your commission share. Splits-V2 Split contracts (one per delegation) push the commission portion here. Fine to be the same as the admin if you want. +- A commission rate, in basis points (`500` = 5 percent, `700` = 7 percent, etc.). + +### 1. Register your provider + +`registerProvider` is **permissionless** — any address with ETH for gas can sign the call. The three role addresses you pass as arguments determine who gets which permission afterward. The signer of *this* transaction has no onchain role; only the gas cost is deducted from it. Use any funded EOA you control. + +<ConfigCode language="bash">{`cast send {{STAKING_REGISTRY_ADDR}} \\ + "registerProvider(address,uint16,address)" \\ + {{PROVIDER_ADMIN_ADDR}} \\ + {{COMMISSION_BPS}} \\ + {{REWARDS_RECIPIENT_ADDR}} \\ + --rpc-url {{ETH_RPC}} \\ + --private-key <any-funded-eoa-private-key>`}</ConfigCode> + +The transaction returns your `providerIdentifier`. **Save it** — every subsequent provider call needs it. Paste the registration transaction hash below to get a ready-to-run extraction command: + +<InlineCommand + token="TX_HASH" + label="registerProvider transaction hash" + placeholder="0x... (the tx hash from the cast send above)" +>{`cast receipt {{TX_HASH}} --rpc-url {{ETH_RPC}} | grep "return" | awk '{print $2}' | xargs cast to-dec`}</InlineCommand> + +Paste the result into the configuration panel as **Provider ID**. + +### 2. Add sequencer keystores to your queue + +Your provider queue holds the attester identities that get activated when delegators stake. The [`add-keys-to-provider.sh`](pathname:///scripts/add-keys-to-provider.sh) helper reads your `keyN_staker_output.json` (the file generated by `aztec validator-keys new --staker-output`) and prints a ready-to-run `addKeysToProvider` command with the BLS keys encoded correctly: + +<ConfigCode language="bash">{`curl -o add-keys-to-provider.sh https://docs.aztec.network/scripts/add-keys-to-provider.sh +chmod +x add-keys-to-provider.sh + +STAKING_REGISTRY={{STAKING_REGISTRY_ADDR}} \\ + PROVIDER_ID={{PROVIDER_ID}} \\ + ETH_RPC={{ETH_RPC}} \\ + ./add-keys-to-provider.sh ~/.aztec/keystore/key1_staker_output.json`}</ConfigCode> + +The script prints the `cast send` command with a placeholder for the admin private key. Review it, add your admin key (or `--ledger`), and run it. The signer must be the provider admin address you set at registration. + +:::warning Duplicates are not deduplicated onchain +The Staking Registry does **not** check for duplicate keys. If you accidentally register the same keystore twice, the duplicate sits at the top of your queue and breaks the next delegation. Recovery: call `dripProviderQueue(providerIdentifier, n)` to drop the bad entries. Verify your keystores are unique before adding. +::: + +:::warning One queue per provider, max 100 keystores per call +Each delegation pulls **one** keystore from your queue. If the queue is empty, new delegators can't activate sequencers. Add proactively, batched up to 100 entries per call to fit gas limits. +::: + +### 3. Submit your provider metadata + +To appear on the staking dashboard, open a PR adding a JSON file to the [staking-dashboard repo](https://github.com/AztecProtocol/staking-dashboard): + +- **Mainnet:** add `<providerId>-<your-name>.json` to `providers/`. Copy the format from [`_example.json`](https://github.com/AztecProtocol/staking-dashboard/blob/master/providers/_example.json). +- **Testnet:** same pattern under `providers-testnet/`. + +Required fields: `providerId`, `providerName`, `providerDescription`, `providerEmail`, `providerWebsite`, `providerLogoUrl`, `discordUsername`, optional `providerSelfStake` array. + +### 4. Wait for delegators (and configure coinbase as they arrive) + +Once you're listed and your queue has keystores, delegators can stake. Each delegation spawns: + +- **A new active attester**, drawn from your queue. +- **A Splits-V2 Split contract** that holds the reward distribution rules (your commission to the rewards-recipient address, the rest to the delegator's vault). + +In the default flow, you then update that attester's keystore to set `coinbase = <Split contract address>` and reload the keystore, so its rewards flow into the Split. See [Configure environment](./configure-environment#after-delegation-set-coinbase-to-the-split-contract) for the per-delegation update procedure. + +You can find each Split's address on the staking dashboard, in the **Sequencer Registered (x)** dropdown for your provider. The dashboard maps each active attester to its Split contract. + +:::info Alternative: EOA coinbase with the payout script +The Split flow is the default, not the only option. A provider can set the coinbase to an address they control and run the [payout script](https://github.com/AztecProtocol/aztec-staking-payout) to calculate and distribute delegator rewards at a published commission and cadence. See [the configuration step](/operate/operators/provider/configure-environment#alternative-eoa-coinbase-with-the-payout-script) and the [forum announcement](https://forum.aztec.network/t/operator-commission-adjustments/8588). +::: + +### Verify your provider entry + +The Staking Registry contract holds your provider configuration, keyed by your **provider ID** (the integer assigned by `registerProvider`). Paste that ID into the configuration panel as **Provider ID**, then run: + +Provider config (admin / take rate / rewards recipient): + +<ConfigCode language="bash">{`{ read PROVIDER_ADMIN; read TAKE_RATE; read REWARDS_RECIPIENT; } < <( + cast call {{STAKING_REGISTRY_ADDR}} \\ + "providerConfigurations(uint256)(address,uint16,address)" \\ + {{PROVIDER_ID}} \\ + --rpc-url {{ETH_RPC}} +) +echo "providerAdmin: $PROVIDER_ADMIN" +echo "providerTakeRate: $TAKE_RATE" +echo "providerRewardsRecipient: $REWARDS_RECIPIENT"`}</ConfigCode> + +Queue depth (how many keystores are ready to be assigned to delegators): + +<ConfigCode language="bash">{`QUEUE_LEN=$(cast call {{STAKING_REGISTRY_ADDR}} \\ + "getProviderQueueLength(uint256)(uint256)" \\ + {{PROVIDER_ID}} \\ + --rpc-url {{ETH_RPC}}) +echo "queueLength: $QUEUE_LEN"`}</ConfigCode> + +Field meanings: + +- **`providerAdmin`** (address) — authorized to call `addKeysToProvider`, `updateProviderTakeRate`, and other management functions. +- **`providerTakeRate`** (uint16) — your commission in basis points (`500` = 5 percent, `700` = 7 percent). +- **`providerRewardsRecipient`** (address) — where your commission share lands when each Split distributes. +- **`queueLength`** — unallocated keystores in your queue. **Each delegation pulls one off**, so keep this above your expected weekly delegation flow. See [Updates, alerts, rewards](./day-2-essentials#4-keep-your-provider-queue-stocked) for cron-style monitoring. + +:::tip Recruit delegators +List yourself on community provider directories (e.g. [dashtec.xyz](https://dashtec.xyz)). Publish your commission rate, uptime history, and contact info. Delegators favour providers who communicate and respond to incidents. +::: + +## L1 contract addresses (<NetworkName />) + +<ConfigCode language="text">{`Registry {{REGISTRY_ADDR}} +Rollup {{ROLLUP_ADDR}} +GSE {{GSE_ADDR}} +Slasher {{SLASHER_ADDR}}`}</ConfigCode> + +Full list: [docs.aztec.network/networks](https://docs.aztec.network/networks). + +<details open> +<summary>What just happened?</summary> + +You registered as a provider in the Governance Staking Escrow. You haven't staked anything; you've published your willingness to operate sequencer infrastructure on behalf of delegators, at the commission rate you set. As delegators stake to your queue, attester positions activate and your nodes begin attesting. Gross rewards go to your coinbase address; you keep your commission and forward the rest to delegators. + +</details> + diff --git a/docs/lines/v6/docs-operate/operators/provider/start-node.mdx b/docs/lines/v6/docs-operate/operators/provider/start-node.mdx new file mode 100644 index 00000000000..f514b2f666c --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/start-node.mdx @@ -0,0 +1,122 @@ +--- +title: "Start the node" +description: "Launch the Aztec sequencer container, watch it sync to the proven tip, and confirm the HTTP API is responding." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/aztec/src/cli/cmds/start_node.ts", "labs/yarn-project/stdlib/src/interfaces/aztec-node.ts", "labs/yarn-project/node-lib/src/config/index.ts"] +--- + +# Start the node + +**Time:** 5 to 15 minutes. Launch the container and wait for it to sync with the network. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +## 1. Start the node + +<IfChoice axis="topology" is="single"> + +```bash +docker compose up -d +``` + +</IfChoice> + +<IfChoice axis="topology" is="ha" label="HA topology"> + +Make sure the HA Postgres database is reachable from both nodes and the schema migration ran (see [Docker Compose](./docker-compose) for the `aztec migrate-ha-db up` command). Then start each node, in any order: + +```bash +ssh node-1 'cd ~/aztec-sequencer && docker compose up -d' +ssh node-2 'cd ~/aztec-sequencer && docker compose up -d' +``` + +Both nodes hold the same attester key. The shared database guarantees only one of them signs each duty, so order doesn't matter. + +</IfChoice> + +## 2. Watch the logs + +```bash +docker compose logs -f aztec-sequencer +``` + +Press `Ctrl+C` to exit the log view. The node continues running in the background. + +:::info Initial sync +On first start the node automatically downloads a state snapshot from a group of snapshot providers, then catches up the remaining blocks from L1. This is the default sync mode (`SYNC_MODE=snapshot`); snapshot sources are configured for you when you pass `--network`. Initial sync takes 5 to 30 minutes depending on your L1 endpoint speed and current chain height. You'll see blocks being processed in the logs. Wait until the node catches up to the current proven tip. See [Using and uploading snapshots](../setup/syncing_best_practices) for sync modes and custom snapshot sources. +::: + +## 3. Check sync progress + +In a new terminal, ask the node which L2 block tips it has seen: + +<ConfigCode language="bash">{`curl -s -X POST -H 'Content-Type: application/json' \\ + -d '{"jsonrpc":"2.0","method":"aztec_getChainTips","params":[],"id":1}' \\ + http://localhost:{{AZTEC_PORT}} | jq '.result | {proposed: .proposed.number, proven: .proven.block.number, finalized: .finalized.block.number}'`}</ConfigCode> + +Expected output looks like: + +```json +{ + "proposed": 75432, + "proven": 75408, + "finalized": 75392 +} +``` + +- **`proposed`** is the latest block the network is producing. As soon as your node is alive and on the right network, this number should increase every few seconds. +- **`proven`** is the latest block with a verified ZK proof submitted to L1. Lags behind `proposed` by an epoch or so. This is the "fully synced" signal. +- **`finalized`** is the latest block whose L1 finalization is also confirmed. Lags behind `proven` by an Ethereum epoch. + +`proven` and `finalized` may show `null` for the first few minutes after startup, before the node has seen a proven epoch. That's normal. Re-run the command after a minute or two; the numbers should appear and start climbing. + +## 4. Check node readiness and identity + +`aztec_isReady` returns `true` once the node is ready to accept transactions: + +<ConfigCode language="bash">{`curl -s -X POST -H 'Content-Type: application/json' \\ + -d '{"jsonrpc":"2.0","method":"aztec_isReady","params":[],"id":1}' \\ + http://localhost:{{AZTEC_PORT}} | jq -r '.result'`}</ConfigCode> + +`aztec_getNodeInfo` confirms which network and version you're on. Use it to catch "I started the node against the wrong `--network`" mistakes early: + +<ConfigCode language="bash">{`curl -s -X POST -H 'Content-Type: application/json' \\ + -d '{"jsonrpc":"2.0","method":"aztec_getNodeInfo","params":[],"id":1}' \\ + http://localhost:{{AZTEC_PORT}} | jq '.result | {nodeVersion, l1ChainId, rollupVersion, rollupAddress: .l1ContractAddresses.rollupAddress}'`}</ConfigCode> + +Expected output looks like: + +```json +{ + "nodeVersion": "6.0.0", + "l1ChainId": 1, + "rollupVersion": "...", + "rollupAddress": "0xE37A2663Cc9c119BA41572C40fAb0bbb23a5402f" +} +``` + +`l1ChainId: 1` is Ethereum mainnet, `11155111` is Sepolia. The `rollupAddress` should match the address in the [L1 contract addresses](./register-sequencer) table. + +## Before you continue + +- `proposed` is increasing every few seconds (node is alive). +- `proven` is non-null and converging on `proposed` (node is fully synced). +- `aztec_isReady` returns `true`. +- `aztec_getNodeInfo` shows the rollup address and L1 chain id you expect. +- Logs show no persistent errors. + +<details open> +<summary>Something wrong?</summary> + +- **"Error connecting to Ethereum"** — Check `ETHEREUM_HOSTS` in your `.env`. Test with `cast block-number --rpc-url YOUR_URL`. +- **Node exits immediately** — Run `docker compose logs aztec-sequencer` (without `-f`) to see the exit reason. Common causes: invalid keystore path, port conflict, or Docker out of disk space. +- **Sync stuck** — Your L1 endpoints may be rate-limited. Consider self-hosting Geth + Prysm or using a higher-tier provider plan. +- **P2P connection issues** — Verify port forwarding with `nc -zv YOUR_IP 40400`. Check both TCP and UDP are forwarded. + +</details> + +--- + +**Stuck?** Ask in [Discord](https://discord.com/invite/aztec). diff --git a/docs/lines/v6/docs-operate/operators/provider/verify.mdx b/docs/lines/v6/docs-operate/operators/provider/verify.mdx new file mode 100644 index 00000000000..d52079ba7a6 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/provider/verify.mdx @@ -0,0 +1,142 @@ +--- +title: "Verify everything works" +description: "Confirm your sequencer is healthy and visible on the network: one container check, one explorer check, one port check." +displayed_sidebar: operatorsSidebar +references: ["docs/static/scripts/check-attester.sh", "labs/yarn-project/stdlib/src/interfaces/aztec-node.ts"] +--- + +# Verify everything works + +**Time:** ~2 minutes. Three checks confirm your sequencer is synced, registered, and reachable. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="provider" /> + +<OperatorConfig /> + +## 1. Container healthy + +```bash +docker compose ps +``` + +Expected: + +``` +NAME IMAGE STATUS +aztec-sequencer azteclabs/aztec:... Up 12 minutes (healthy) +``` + +The `(healthy)` flag comes from the health check in your compose file. It probes the node's own API every 30 seconds and requires a proven L2 tip, so this one line covers sync progress, API readiness, and container stability. + +- `(health: starting)`: the node is still syncing. Initial sync takes 5 to 30 minutes. +- `(unhealthy)`: the API stopped answering or the node lost its proven tip. Check `docker compose logs -f aztec-sequencer`. +- `Up` with no health suffix: your compose file predates the health check. Add it from the [Docker Compose](./docker-compose) step. + +## 2. Find your sequencer on an explorer + +Paste your attester address into one of the community explorers: + +- **[Dashtec](https://dashtec.xyz/)**: validator status, queue position, attestation activity. +- **[Aztec Vision](https://aztec.vision/)**: sequencer explorer with per-attester activity. + +What you should see: + +- **Just registered**: your attester sits in the entry queue. Status stays `NONE` for the first 1 to 2 epochs until the queue is flushed. This is normal, not a failed deposit. +- **Active**: status `VALIDATING`, with attestations appearing whenever your sequencer is selected for a committee. + +<IfCount gt={1}> + +Check all <Count /> attesters, not just the first. One stuck in the queue is easy to miss. + +</IfCount> + +Once attestations show up, your node has proven itself end to end: it is synced, signing, and publishing over P2P. + +:::note Community-maintained +These explorers are community projects and can lag the chain. If your network or attester is not listed, or you want an answer straight from the contracts, use the CLI check at the bottom of this page. More explorers are listed in [awesome-aztec](https://github.com/AztecProtocol/awesome-aztec#explorers---node-focussed). +::: + +## 3. P2P port reachable + +Until your first attestations appear on an explorer, a closed P2P port is invisible. Confirm it once, from a machine outside your network: + +<ConfigCode language="bash">{`nc -zv {{P2P_IP}} 40400`}</ConfigCode> + +Expected output: + +``` +Connection to <P2P_IP> 40400 port [tcp/*] succeeded! +``` + +After your sequencer starts attesting, this check is redundant: attestation activity already proves connectivity. + +<details> +<summary id="advanced-check-contract-state-directly">Advanced: check contract state directly (CLI)</summary> + +Explorers read the same contracts through an indexer. These checks query the contracts directly. + +**Quick status check.** The expected status is `1` (VALIDATING). Codes: `0` NONE, `1` VALIDATING, `2` ZOMBIE, `3` EXITING. A fresh deposit sits in the entry queue until an epoch boundary passes and someone calls `flushEntryQueue(uint256)` on the Rollup (anyone can, including you), so `0` during the first 1 to 2 epochs does not mean the deposit failed. + +<IfCount eq={1}> + +<ConfigCode language="bash">{`cast call {{ROLLUP_ADDR}} \\ + "getStatus(address)(uint8)" {{ATTESTER_ADDR}} \\ + --rpc-url {{ETH_RPC}}`}</ConfigCode> + +</IfCount> + +<IfCount gt={1}> + +Loop the check across every attester in your keystore: + +<ConfigCode language="bash">{`KEYSTORE=~/.aztec/keystore/key1_staker_output.json + +for i in $(seq 0 {{COUNT_MINUS_1}}); do + ATTESTER=$(jq -r ".[$i].attester" "$KEYSTORE") + STATUS=$(cast call {{ROLLUP_ADDR}} \\ + "getStatus(address)(uint8)" "$ATTESTER" \\ + --rpc-url {{ETH_RPC}}) + echo "Validator $((i+1)) of {{COUNT}} ($ATTESTER): status=$STATUS" +done`}</ConfigCode> + +All <Count /> should report `status=1`. Anything stuck on `0` (NONE) after a couple of epochs means the deposit didn't land or the entry queue hasn't been flushed. + +</IfCount> + +**Full attester report.** When a status is anything other than `1` and you need to see why, download [`check-attester.sh`](pathname:///scripts/check-attester.sh). It checks one or more attesters (status, effective balance, exit state, withdrawer) and prints a labeled, color-coded report with a health verdict per attester: + +<ConfigCode language="bash">{`curl -o check-attester.sh https://docs.aztec.network/scripts/check-attester.sh +chmod +x check-attester.sh + +# Check several attesters at once: comma-separated list +ATTESTER={{ATTESTER_ADDR}} \\ +ROLLUP={{ROLLUP_ADDR}} \\ +GSE={{GSE_ADDR}} \\ +ETH_RPC={{ETH_RPC}} \\ + ./check-attester.sh`}</ConfigCode> + +To confirm your provider entry (admin, take rate, rewards recipient), query the Staking Registry with your provider ID: + +<ConfigCode language="bash">{`{ read PROVIDER_ADMIN; read TAKE_RATE; read REWARDS_RECIPIENT; } < <( + cast call {{STAKING_REGISTRY_ADDR}} \\ + "providerConfigurations(uint256)(address,uint16,address)" \\ + {{PROVIDER_ID}} \\ + --rpc-url {{ETH_RPC}} +) +echo "providerAdmin: $PROVIDER_ADMIN" +echo "providerTakeRate: $TAKE_RATE" +echo "providerRewardsRecipient: $REWARDS_RECIPIENT"`}</ConfigCode> + +Field meanings and the queue-depth check are on the [register sequencer](./register-sequencer) page (provider section). + +</details> + +## You're live + +Your sequencers are registered and attesting. The nodes will be randomly selected for committees starting next epoch and earn AZTEC rewards for attesting to and proposing blocks. + +One step remains: go through [Updates, alerts, rewards](./day-2-essentials) to learn how to keep your nodes on the current version, set up monitoring alerts, manage reward distribution, and keep your provider queue stocked. + +If a node is running but proposals or attestations are not landing, see [Sequencer troubleshooting](../concepts/sequencer-troubleshooting). diff --git a/docs/lines/v6/docs-operate/operators/reference/changelog/index.md b/docs/lines/v6/docs-operate/operators/reference/changelog/index.md new file mode 100644 index 00000000000..e3aec12dd20 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/changelog/index.md @@ -0,0 +1,125 @@ +--- +id: changelog +displayed_sidebar: operatorsSidebar +title: Changelog +description: Comprehensive changelog documenting configuration changes, new features, and breaking changes across Aztec node versions. +--- + +## Overview + +This changelog documents all configuration changes, new features, and breaking changes across Aztec node versions. Each version has a dedicated page with detailed migration instructions. + +## Version history + +### [v6.0.0](./v6.md) + +Hard fork: a complete new deployment from genesis. Every artifact location moved, and the executor configuration was renamed with no fallback. + +**Key changes:** +- New locations for everything: `azteclabs/aztec` on DockerHub, `@aztec-labs/*` and `@aztec-foundation/*` on npm, `aztec-labs-eng/aztec-node` for source +- `ACVM_*` configuration renamed to `NOIR_EXECUTE_*`, with no fallback. A real-proving node fails to start on the old names; one without real proofs starts anyway on slow WASM simulation, so a clean startup does not prove the config migrated +- Attester-initiated sequencer exits, including batched exits for operators running many delegations. Live on the rollup now; the CLI wrappers ship from `v6.0.0-rc.3`, so on rc.1 call the contract directly +- New RPC methods: `aztec_getTxEffectMembershipWitness`, `aztec_getValidatorStatsBatch` +- `v6.0.0-rc.1` is Testnet only; Mainnet's first v6 release will be `v6.0.0` stable + +**Migration difficulty**: High + +[View full changelog →](./v6.md) + +--- + +### [v5.2.0](./v5.2.md) + +Node-operator hardening release: peerless nodes stop acting, slashing votes against your own validators are surfaced, and the RPC server gains timeout and CORS configuration. + +**Key changes:** +- A node whose p2p service fails to start now exits instead of running peerless +- A peerless node warns periodically, skips proposing, and rejects incoming transactions +- Logs and metrics when the network votes to slash your own validators +- L1 event watching moved from server-side filters to bounded `eth_getLogs` polling +- New settings: `SEQ_MIN_PEERS_TO_PROPOSE`, `P2P_HEALTH_MIN_PEERS`, `GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE`, `RPC_HTTP_KEEP_ALIVE_TIMEOUT_MS`, `RPC_HTTP_HEADERS_TIMEOUT_MS`, `RPC_CORS_ALLOWED_HEADERS`, `RPC_CORS_ALLOWED_ORIGINS`, `RPC_CORS_ALLOW_ANY_ORIGIN` (all defaults preserve previous behaviour) + +**Migration difficulty**: Low + +[View full changelog →](./v5.2.md) + +--- + +### [v4.3.x](./v4.3.md) + +Bundled binaries renamed under an `aztec-` prefix on `PATH`. v4.3.1 is a bug-fix release. + +**Key changes:** +- `aztec-up` no longer places bare-named binaries (`forge`, `cast`, `nargo`, `bb`, `pxe`, `txe`, `validator-client`, `blob-client`, ...) on `PATH`; use the `aztec-` prefixed names instead +- v4.3.1: fixes a block-stream error loop after finalization, prover proof-submission ordering, and released contract artifact version stamping + +**Migration difficulty**: Low + +[View full changelog →](./v4.3.md) + +--- + +### [v4.2.0](./v4.2.md) + +New features and configuration options for node operators. + +**Key changes:** +- Blob retrieval improvements with unified retry loop + +**Migration difficulty**: Low + +[View full changelog →](./v4.2.md) + +--- + +### [v4.x (Upgrade from Ignition)](./v4.md) + +Major upgrade from Ignition (v2.x) to Alpha (v4.x) with significant architectural changes. + +**Key changes:** +- Checkpoint-based block architecture (multiple L2 blocks per slot) +- Blob-only data publication (EIP-4844), calldata fallback removed +- Double signing slashing infrastructure +- HA signing with PostgreSQL for redundant sequencer nodes +- Admin API key authentication +- Sequencer environment variable renames +- Withdrawal delay increase (7 to 30 days) + +**Migration difficulty**: High + +[View full changelog →](./v4.md) + +--- + +### [v2.0.2 (from v1.2.1)](./v2.0.2.md) + +Major release with significant configuration simplification, keystore integration, and feature updates. + +**Key changes:** +- Simplified L1 contract address configuration (registry-only) +- Integrated keystore system for key management +- Removed component-specific settings in favor of global configuration +- Enhanced P2P transaction collection capabilities +- New invalidation controls for sequencers + +**Migration difficulty**: Moderate to High + +[View full changelog →](./v2.0.2.md) + +--- + +## Migration guides + +When upgrading between versions: + +1. Review the version-specific changelog for breaking changes +2. Follow the migration checklist for your node type +3. Test in a non-production environment first +4. Check the troubleshooting section for common upgrade issues +5. Join [Aztec Discord](https://discord.gg/aztec) for upgrade support + +## Related resources + +- [CLI Reference](../cli-reference.md) - Current command-line options +- [Node API Reference](../node-api-reference.md) - API documentation +- [Ethereum RPC Reference](../ethereum-rpc-reference.md) - L1 RPC usage diff --git a/docs/lines/v6/docs-operate/operators/reference/changelog/v2.0.2.md b/docs/lines/v6/docs-operate/operators/reference/changelog/v2.0.2.md new file mode 100644 index 00000000000..ca8fade1939 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/changelog/v2.0.2.md @@ -0,0 +1,366 @@ +--- +title: v2.0.2 (from v1.2.1) +description: Major release with configuration simplification, keystore integration, and enhanced features. Includes breaking changes requiring migration. +displayed_sidebar: operatorsSidebar +--- + +## Overview + +Version 2.0.2 introduces significant configuration simplification, an integrated keystore system, and enhanced P2P capabilities. This release includes breaking changes that require migration from v1.2.1. + +**Migration difficulty**: Moderate to High + +## Breaking changes + +### L1 contract addresses + +**v1.2.1:** +```bash +--rollup-address <value> ($ROLLUP_CONTRACT_ADDRESS) +--inbox-address <value> ($INBOX_CONTRACT_ADDRESS) +--outbox-address <value> ($OUTBOX_CONTRACT_ADDRESS) +--fee-juice-address <value> ($FEE_JUICE_CONTRACT_ADDRESS) +--staking-asset-address <value> ($STAKING_ASSET_CONTRACT_ADDRESS) +--fee-juice-portal-address <value> ($FEE_JUICE_PORTAL_CONTRACT_ADDRESS) +--registry-address <value> ($REGISTRY_CONTRACT_ADDRESS) +``` + +**v2.0.2:** +```bash +--registry-address <value> ($REGISTRY_CONTRACT_ADDRESS) +--rollup-version <value> ($ROLLUP_VERSION) # Default: canonical +``` + +**Migration**: Only registry address is required. All other contract addresses are derived automatically. + +### Keystore integration + +**v1.2.1:** +```bash +--sequencer.publisherPrivateKey <value> ($SEQ_PUBLISHER_PRIVATE_KEY) +--proverNode.publisherPrivateKey <value> ($PROVER_PUBLISHER_PRIVATE_KEY) +``` + +**v2.0.2:** +```bash +--proverNode.keyStoreDirectory <value> ($KEY_STORE_DIRECTORY) + +# Multiple publishers supported +--sequencer.publisherPrivateKeys <value> ($SEQ_PUBLISHER_PRIVATE_KEYS) +--sequencer.publisherAddresses <value> ($SEQ_PUBLISHER_ADDRESSES) + +--proverNode.publisherPrivateKeys <value> ($PROVER_PUBLISHER_PRIVATE_KEYS) +--proverNode.publisherAddresses <value> ($PROVER_PUBLISHER_ADDRESSES) +``` + +**Migration**: Create keystore directory, change singular to plural. Use `*_ADDRESSES` for remote signers. See [Advanced Keystore Guide](../../keystore/). + +### Validator configuration + +**v1.2.1:** +```bash +--sequencer.validatorPrivateKeys <value> ($VALIDATOR_PRIVATE_KEYS) +``` + +**v2.0.2:** +```bash +--sequencer.validatorPrivateKeys <value> ($VALIDATOR_PRIVATE_KEYS) +--sequencer.validatorAddresses <value> ($VALIDATOR_ADDRESSES) # For remote signers +--sequencer.disabledValidators <value> # Temporarily disable +``` + +### Sync mode relocated + +**v1.2.1:** Component-specific +```bash +--node.syncMode <value> ($SYNC_MODE) +--node.snapshotsUrl <value> ($SYNC_SNAPSHOTS_URL) +--proverNode.syncMode <value> ($SYNC_MODE) +--proverNode.snapshotsUrl <value> ($SYNC_SNAPSHOTS_URL) +``` + +**v2.0.2:** Global setting +```bash +--sync-mode <value> ($SYNC_MODE) # Options: full, snapshot, force-snapshot +--snapshots-url <value> ($SYNC_SNAPSHOTS_URL) +``` + +### World state separation + +**v1.2.1:** Prover-node-specific +```bash +--proverNode.worldStateBlockCheckIntervalMS <value> ($WS_BLOCK_CHECK_INTERVAL_MS) +--proverNode.worldStateProvenBlocksOnly <value> ($WS_PROVEN_BLOCKS_ONLY) +--proverNode.worldStateBlockRequestBatchSize <value> ($WS_BLOCK_REQUEST_BATCH_SIZE) +--proverNode.worldStateDbMapSizeKb <value> ($WS_DB_MAP_SIZE_KB) +--proverNode.archiveTreeMapSizeKb <value> ($ARCHIVE_TREE_MAP_SIZE_KB) +--proverNode.nullifierTreeMapSizeKb <value> ($NULLIFIER_TREE_MAP_SIZE_KB) +--proverNode.noteHashTreeMapSizeKb <value> ($NOTE_HASH_TREE_MAP_SIZE_KB) +--proverNode.messageTreeMapSizeKb <value> ($MESSAGE_TREE_MAP_SIZE_KB) +--proverNode.publicDataTreeMapSizeKb <value> ($PUBLIC_DATA_TREE_MAP_SIZE_KB) +--proverNode.worldStateDataDirectory <value> ($WS_DATA_DIRECTORY) +--proverNode.worldStateBlockHistory <value> ($WS_NUM_HISTORIC_BLOCKS) +``` + +**v2.0.2:** Global settings only +```bash +--world-state-data-directory <value> ($WS_DATA_DIRECTORY) +--world-state-db-map-size-kb <value> ($WS_DB_MAP_SIZE_KB) +--world-state-block-history <value> ($WS_NUM_HISTORIC_BLOCKS) +``` + +**Migration**: Move to global WORLD STATE section. Tree-specific map sizes and other world state settings removed. + +## Removed features + +### Faucet service +```bash +# All removed in v2.0.2 +--faucet +--faucet.apiServer +--faucet.apiServerPort <value> ($FAUCET_API_SERVER_PORT) +--faucet.viemPollingIntervalMS <value> ($L1_READER_VIEM_POLLING_INTERVAL_MS) +--faucet.l1Mnemonic <value> ($MNEMONIC) +--faucet.mnemonicAddressIndex <value> ($FAUCET_MNEMONIC_ADDRESS_INDEX) +--faucet.interval <value> ($FAUCET_INTERVAL_MS) +--faucet.ethAmount <value> ($FAUCET_ETH_AMOUNT) +--faucet.l1Assets <value> ($FAUCET_L1_ASSETS) +``` + +### L1 transaction monitoring + +All removed from archiver and sequencer: + +```bash +--archiver.gasLimitBufferPercentage <value> ($L1_GAS_LIMIT_BUFFER_PERCENTAGE) +--archiver.maxGwei <value> ($L1_GAS_PRICE_MAX) +--archiver.maxBlobGwei <value> ($L1_BLOB_FEE_PER_GAS_MAX) +--archiver.priorityFeeBumpPercentage <value> ($L1_PRIORITY_FEE_BUMP_PERCENTAGE) +--archiver.priorityFeeRetryBumpPercentage <value> ($L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE) +--archiver.fixedPriorityFeePerGas <value> ($L1_FIXED_PRIORITY_FEE_PER_GAS) +--archiver.maxAttempts <value> ($L1_TX_MONITOR_MAX_ATTEMPTS) +--archiver.checkIntervalMs <value> ($L1_TX_MONITOR_CHECK_INTERVAL_MS) +--archiver.stallTimeMs <value> ($L1_TX_MONITOR_STALL_TIME_MS) +--archiver.txTimeoutMs <value> ($L1_TX_MONITOR_TX_TIMEOUT_MS) +--archiver.txPropagationMaxQueryAttempts <value> ($L1_TX_PROPAGATION_MAX_QUERY_ATTEMPTS) +--archiver.cancelTxOnTimeout <value> ($L1_TX_MONITOR_CANCEL_TX_ON_TIMEOUT) + +# Same settings removed from --sequencer.* +``` + +**Migration**: L1 transaction management now uses optimized internal defaults. + +### Rollup constants from archiver + +All rollup constants now derived from L1 contracts: + +```bash +# All removed in v2.0.2 +--archiver.ethereumSlotDuration <value> ($ETHEREUM_SLOT_DURATION) +--archiver.aztecSlotDuration <value> ($AZTEC_SLOT_DURATION) +--archiver.aztecEpochDuration <value> ($AZTEC_EPOCH_DURATION) +--archiver.aztecTargetCommitteeSize <value> ($AZTEC_TARGET_COMMITTEE_SIZE) +--archiver.aztecProofSubmissionEpochs <value> ($AZTEC_PROOF_SUBMISSION_EPOCHS) +--archiver.depositAmount <value> ($AZTEC_DEPOSIT_AMOUNT) +--archiver.minimumStake <value> ($AZTEC_MINIMUM_STAKE) +--archiver.slashingQuorum <value> ($AZTEC_SLASHING_QUORUM) +--archiver.slashingRoundSize <value> ($AZTEC_SLASHING_ROUND_SIZE) +--archiver.governanceProposerQuorum <value> ($AZTEC_GOVERNANCE_PROPOSER_QUORUM) +--archiver.governanceProposerRoundSize <value> ($AZTEC_GOVERNANCE_PROPOSER_ROUND_SIZE) +--archiver.manaTarget <value> ($AZTEC_MANA_TARGET) +--archiver.provingCostPerMana <value> ($AZTEC_PROVING_COST_PER_MANA) +--archiver.exitDelaySeconds <value> ($AZTEC_EXIT_DELAY_SECONDS) + +# Same settings removed from --sequencer.* +``` + +### Node deployment options + +```bash +# All removed in v2.0.2 (moved to sandbox only) +--node.deployAztecContracts ($DEPLOY_AZTEC_CONTRACTS) +--node.deployAztecContractsSalt <value> ($DEPLOY_AZTEC_CONTRACTS_SALT) +--node.assumeProvenThroughBlockNumber <value> ($ASSUME_PROVEN_THROUGH_BLOCK_NUMBER) +--node.publisherPrivateKey <value> ($L1_PRIVATE_KEY) +``` + +**Migration**: Contract deployment now sandbox-only via `--sandbox.deployAztecContractsSalt`. For production, deploy contracts separately. + +### Other removed settings + +```bash +# Prover coordination +--proverNode.proverCoordinationNodeUrls <value> ($PROVER_COORDINATION_NODE_URLS) + +# Custom forwarder +--sequencer.customForwarderContractAddress <value> ($CUSTOM_FORWARDER_CONTRACT_ADDRESS) +--proverNode.customForwarderContractAddress <value> ($CUSTOM_FORWARDER_CONTRACT_ADDRESS) + +# Component-specific settings now global +--archiver.viemPollingIntervalMS <value> ($ARCHIVER_VIEM_POLLING_INTERVAL_MS) +--sequencer.viemPollingIntervalMS <value> ($L1_READER_VIEM_POLLING_INTERVAL_MS) +--blobSink.viemPollingIntervalMS <value> ($L1_READER_VIEM_POLLING_INTERVAL_MS) +--proverBroker.viemPollingIntervalMS <value> ($L1_READER_VIEM_POLLING_INTERVAL_MS) + +--archiver.rollupVersion <value> ($ROLLUP_VERSION) +--sequencer.rollupVersion <value> ($ROLLUP_VERSION) +--blobSink.rollupVersion <value> ($ROLLUP_VERSION) +--proverBroker.rollupVersion <value> ($ROLLUP_VERSION) +--pxe.rollupVersion <value> ($ROLLUP_VERSION) + +--archiver.dataStoreMapSizeKB <value> ($DATA_STORE_MAP_SIZE_KB) +--pxe.dataStoreMapSizeKB <value> ($DATA_STORE_MAP_SIZE_KB) +--blobSink.dataStoreMapSizeKB <value> ($DATA_STORE_MAP_SIZE_KB) +--proverBroker.dataStoreMapSizeKB <value> ($DATA_STORE_MAP_SIZE_KB) +--p2pBootstrap.dataStoreMapSizeKB <value> ($DATA_STORE_MAP_SIZE_KB) + +# Aztec node specific +--node.worldStateBlockCheckIntervalMS <value> ($WS_BLOCK_CHECK_INTERVAL_MS) +--node.archiverUrl <value> ($ARCHIVER_URL) +``` + +## New features + +### P2P transaction collection + +```bash +--p2p.txCollectionNodeRpcUrls <value> ($TX_COLLECTION_NODE_RPC_URLS) +--p2p.txCollectionFastNodeIntervalMs <value> ($TX_COLLECTION_FAST_NODE_INTERVAL_MS) +--p2p.txCollectionFastMaxParallelRequestsPerNode <value> ($TX_COLLECTION_FAST_MAX_PARALLEL_REQUESTS_PER_NODE) +--p2p.txCollectionNodeRpcMaxBatchSize <value> ($TX_COLLECTION_NODE_RPC_MAX_BATCH_SIZE) +--p2p.txCollectionFastNodesTimeoutBeforeReqRespMs <value> ($TX_COLLECTION_FAST_NODES_TIMEOUT_BEFORE_REQ_RESP_MS) +--p2p.txCollectionSlowNodesIntervalMs <value> ($TX_COLLECTION_SLOW_NODES_INTERVAL_MS) +--p2p.txCollectionSlowReqRespIntervalMs <value> ($TX_COLLECTION_SLOW_REQ_RESP_INTERVAL_MS) +--p2p.txCollectionSlowReqRespTimeoutMs <value> ($TX_COLLECTION_SLOW_REQ_RESP_TIMEOUT_MS) +--p2p.txCollectionReconcileIntervalMs <value> ($TX_COLLECTION_RECONCILE_INTERVAL_MS) +--p2p.txCollectionDisableSlowDuringFastRequests <value> ($TX_COLLECTION_DISABLE_SLOW_DURING_FAST_REQUESTS) +``` + +### P2P security and testing + +```bash +# Discovery and security +--p2p.p2pDiscoveryDisabled <value> ($P2P_DISCOVERY_DISABLED) +--p2p.p2pAllowOnlyValidators <value> ($P2P_ALLOW_ONLY_VALIDATORS) +--p2p.p2pMaxFailedAuthAttemptsAllowed <value> ($P2P_MAX_AUTH_FAILED_ATTEMPTS_ALLOWED) + +# Testing features +--p2p.dropTransactions <value> ($P2P_DROP_TX) +--p2p.dropTransactionsProbability <value> ($P2P_DROP_TX_CHANCE) + +# Transaction handling +--p2p.disableTransactions <value> ($TRANSACTIONS_DISABLED) +--p2p.txPoolDeleteTxsAfterReorg <value> ($P2P_TX_POOL_DELETE_TXS_AFTER_REORG) + +# Preferred peers +--p2p.preferredPeers <value> ($P2P_PREFERRED_PEERS) +``` + +### Sequencer invalidation controls + +```bash +--sequencer.attestationPropagationTime <value> ($SEQ_ATTESTATION_PROPAGATION_TIME) +--sequencer.secondsBeforeInvalidatingBlockAsCommitteeMember <value> ($SEQ_SECONDS_BEFORE_INVALIDATING_BLOCK_AS_COMMITTEE_MEMBER) +--sequencer.secondsBeforeInvalidatingBlockAsNonCommitteeMember <value> ($SEQ_SECONDS_BEFORE_INVALIDATING_BLOCK_AS_NON_COMMITTEE_MEMBER) +``` + +### Other new features + +```bash +# Archiver - skip validation (testing only) +--archiver.skipValidateBlockAttestations + +# Prover - transaction gathering timeout +--proverNode.txGatheringTimeoutMs <value> ($PROVER_NODE_TX_GATHERING_TIMEOUT_MS) +``` + +## Changed defaults + +| Flag | Environment Variable | v1.2.1 | v2.0.2 | +|------|---------------------|--------|--------| +| `--p2p.overallRequestTimeoutMs` | `$P2P_REQRESP_OVERALL_REQUEST_TIMEOUT_MS` | 4000 | **10000** | +| `--p2p.individualRequestTimeoutMs` | `$P2P_REQRESP_INDIVIDUAL_REQUEST_TIMEOUT_MS` | 2000 | **10000** | +| `--p2p.dialTimeoutMs` | `$P2P_REQRESP_DIAL_TIMEOUT_MS` | 1000 | **5000** | +| `--proverAgent.proverAgentPollIntervalMs` | `$PROVER_AGENT_POLL_INTERVAL_MS` | 100 | **1000** | +| `--bot.l1ToL2MessageTimeoutSeconds` | `$BOT_L1_TO_L2_TIMEOUT_SECONDS` | 60 | **3600** | +| `--bot.recipientEncryptionSecret` | `$BOT_RECIPIENT_ENCRYPTION_SECRET` | [Redacted] | **0x...cafecafe** | + +## Migration checklist + +### All nodes +- [ ] Update to `--registry-address` only (remove all other contract addresses) +- [ ] Add `--rollup-version canonical` if needed +- [ ] Move `--sync-mode` and `--snapshots-url` to global config +- [ ] Remove component-specific `--*.rollupVersion`, `--*.dataStoreMapSizeKB` +- [ ] Set global `--data-store-map-size-kb` if needed (default: 134217728 KB) + +### Sequencer nodes +- [ ] Create and configure `--sequencer.keyStoreDirectory` (actually `--proverNode.keyStoreDirectory`) +- [ ] Change `--sequencer.publisherPrivateKey` → `--sequencer.publisherPrivateKeys` +- [ ] Update `--sequencer.validatorPrivateKeys` or add `--sequencer.validatorAddresses` +- [ ] Remove all `--sequencer.gasLimitBufferPercentage` and related L1 settings +- [ ] Remove `--sequencer.customForwarderContractAddress`, `--sequencer.viemPollingIntervalMS` +- [ ] Consider using `--sequencer.disabledValidators` for temporary disabling + +### Prover nodes +- [ ] Create and configure `--proverNode.keyStoreDirectory` +- [ ] Change `--proverNode.publisherPrivateKey` → `--proverNode.publisherPrivateKeys` +- [ ] Move world state settings to global WORLD STATE section +- [ ] Remove `--proverNode.archiveTreeMapSizeKb` and other tree-specific sizes +- [ ] Remove `--proverNode.proverCoordinationNodeUrls`, `--proverNode.customForwarderContractAddress` +- [ ] Set `--proverNode.txGatheringTimeoutMs` if needed + +### Archiver nodes +- [ ] Remove all `--archiver.gasLimitBufferPercentage` and related L1 settings +- [ ] Remove `--archiver.ethereumSlotDuration`, `--archiver.aztecSlotDuration`, etc. +- [ ] Remove `--archiver.viemPollingIntervalMS` + +### P2P configuration +- [ ] Configure `--p2p.txCollectionNodeRpcUrls` if using external nodes +- [ ] Review `--p2p.p2pAllowOnlyValidators` security settings +- [ ] Consider using `--p2p.preferredPeers` + +### Sandbox/development +- [ ] Move deployment to `--sandbox.deployAztecContractsSalt` +- [ ] Configure `--sandbox.l1Mnemonic` if needed +- [ ] Remove all faucet flags + +## Troubleshooting + +### Node fails with contract address errors +**Solution**: Remove all individual contract addresses, keep only `--registry-address`, add `--rollup-version canonical` + +### Publisher key not found +**Solution**: +- Check `--proverNode.keyStoreDirectory` ($KEY_STORE_DIRECTORY) is set +- Change `--*.publisherPrivateKey` to `--*.publisherPrivateKeys` (plural) +- See [Advanced Keystore Guide](../../keystore/) + +### World state sync failures (prover) +**Solution**: +- Move `--proverNode.worldStateDataDirectory` to `--world-state-data-directory` +- Remove prover-specific world state settings +- Use global `--world-state-db-map-size-kb` + +### Slow P2P after upgrade +**Solution**: +- Configure `--p2p.txCollectionNodeRpcUrls` ($TX_COLLECTION_NODE_RPC_URLS) +- Adjust `--p2p.txCollectionFastNodeIntervalMs` + +### Validator not attesting +**Solution**: +- Check not in `--sequencer.disabledValidators` list +- Verify `--sequencer.validatorPrivateKeys` or `--sequencer.validatorAddresses` +- Check keystore permissions + +### Missing sync snapshots +**Solution**: +- Move `--node.syncMode` to `--sync-mode` (global) +- Set `--snapshots-url` at global level + +## Next steps + +- [How to Run a Sequencer Node](../../setup/sequencer-setup.md) - Updated setup instructions +- [Advanced Keystore Usage](../../keystore/) - Keystore configuration +- [Ethereum RPC Calls Reference](../ethereum-rpc-reference.md) - Infrastructure requirements +- [Aztec Discord](https://discord.gg/aztec) - Upgrade support \ No newline at end of file diff --git a/docs/lines/v6/docs-operate/operators/reference/changelog/v4.2.md b/docs/lines/v6/docs-operate/operators/reference/changelog/v4.2.md new file mode 100644 index 00000000000..af55adb9a60 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/changelog/v4.2.md @@ -0,0 +1,32 @@ +--- +title: v4.2.0 +description: New features and configuration options for node operators. +displayed_sidebar: operatorsSidebar +--- + +## Overview + +**Migration difficulty**: TODO + +## Breaking changes + +## New features + +### Blob retrieval improvements + +Blob retrieval now uses a unified retry loop that alternates between consensus clients and file stores, replacing the previous multi-phase approach. This reduces retrieval latency from ~12s to ~1.5-3s when blobs aren't immediately available in file stores. + +Non-supernode consensus hosts are automatically detected at startup and skipped during blob fetching, avoiding wasted requests. + +**Configuration:** + +```bash +BLOB_PREFER_FILESTORES=false # Try file stores before consensus (default: false) +BLOB_FILE_STORE_TIMEOUT_MS=10000 # HTTP timeout for blob file store requests in ms (default: 10000) +``` + +Set `BLOB_PREFER_FILESTORES=true` if your file stores are faster or more reliable than your consensus clients. + +## Changed defaults + +## Troubleshooting diff --git a/docs/lines/v6/docs-operate/operators/reference/changelog/v4.3.md b/docs/lines/v6/docs-operate/operators/reference/changelog/v4.3.md new file mode 100644 index 00000000000..915fe1de753 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/changelog/v4.3.md @@ -0,0 +1,57 @@ +--- +title: v4.3 +description: Operator-facing changes for the v4.3.x releases. +--- + +## v4.3.1 + +**Migration difficulty:** none. v4.3.1 is a bug-fix release with no configuration changes and no operator action required beyond upgrading. + +- **Tip-store finalize guarded against deleting live tips** ([#23505](https://github.com/AztecProtocol/aztec-packages/pull/23505), backport of [#23295](https://github.com/AztecProtocol/aztec-packages/pull/23295)): fixes a bug where finalization could delete data still referenced by a live tip, locking the node's block stream into a persistent error loop that required wiping the archiver database to recover. +- **Prover waits for the previous epoch to be proven** ([#23457](https://github.com/AztecProtocol/aztec-packages/pull/23457)): prover nodes now wait for the previous epoch's proof to land before submitting a proof that builds on it, instead of failing the submission. +- **Released contract artifacts stamped with the release version** ([#23851](https://github.com/AztecProtocol/aztec-packages/pull/23851), backport of [#23470](https://github.com/AztecProtocol/aztec-packages/pull/23470)): released `noir-contracts` JSON artifacts no longer carry a `"dev"` aztec version. + +## v4.3.0 + +**Migration difficulty:** low. The only change likely to require operator action is the renaming of bundled binaries under an `aztec-` prefix. + +### Breaking changes + +#### Bundled binaries renamed under `aztec-` prefix on `PATH` + +`aztec-up` previously placed bundled tooling directly into `$HOME/.aztec/current/bin` under bare names (`forge`, `cast`, `nargo`, `bb`, `pxe`, `txe`, `validator-client`, `blob-client`, ...). Operators with their own `forge` or `nargo` install elsewhere on `PATH` could end up silently using the wrong binary depending on resolution order, and the bundle could shadow unrelated projects. + +In v4.3.0, every bundled binary is exposed **only** under its `aztec-`-prefixed name. Bare names are no longer placed on `PATH` by `aztec-up`. + +| Was on `PATH` | Now | +|---|---| +| `forge` | `aztec-forge` | +| `cast` | `aztec-cast` | +| `anvil` | `aztec-anvil` | +| `chisel` | `aztec-chisel` | +| `nargo` | `aztec-nargo` | +| `noir-profiler` | `aztec-noir-profiler` | +| `bb` | `aztec-bb` | +| `bb-cli` | `aztec-bb-cli` | +| `pxe` | `aztec-pxe` | +| `txe` | `aztec-txe` | +| `validator-client` | `aztec-validator-client` | +| `blob-client` | `aztec-blob-client` | + +`aztec`, `aztec-wallet`, and `aztec-up` keep their existing names. + +**Operator action:** any operator scripts, systemd units, dockerfiles, or run-books that invoke `forge`, `cast`, `anvil`, `nargo`, `bb`, `pxe`, `txe`, `validator-client`, or `blob-client` directly from the bundle path must switch to the `aztec-` prefixed names. References to `aztec`, `aztec-wallet`, and `aztec-up` are unaffected. + +References: [#22902](https://github.com/AztecProtocol/aztec-packages/pull/22902), [#22709](https://github.com/AztecProtocol/aztec-packages/pull/22709). + +### Other notable changes + +These items do not require operator action but are called out in the [v4.3.0 release notes](https://github.com/AztecProtocol/aztec-packages/releases/tag/v4.3.0): + +- **Sequencer signs the last block before archiver sync** ([#22117](https://github.com/AztecProtocol/aztec-packages/pull/22117)) — correctness and ordering improvement around block signing. +- **Release image stamps `stdlib/package.json` with the release version** ([#23393](https://github.com/AztecProtocol/aztec-packages/pull/23393)) — fixes a published-artifact metadata mismatch that affected downstream consumers of the stdlib package. +- **macOS `aztec-up` install ergonomics** ([#23310](https://github.com/AztecProtocol/aztec-packages/pull/23310), [#23335](https://github.com/AztecProtocol/aztec-packages/pull/23335)) — `aztec-up` now falls back to no-timeout when `/usr/bin/timeout` is absent, and `add_crate.sh` uses `perl -i` instead of GNU-specific `sed -i`. Affects operators who bootstrap nodes via `aztec-up` on macOS. + +### Troubleshooting + +If a previously-working operator command suddenly errors with `command not found: forge` (or `cast`, `nargo`, `bb`, etc.) after upgrading to v4.3.0, switch the call site to the `aztec-` prefixed binary or install the standalone tool separately. diff --git a/docs/lines/v6/docs-operate/operators/reference/changelog/v4.md b/docs/lines/v6/docs-operate/operators/reference/changelog/v4.md new file mode 100644 index 00000000000..86e5b8af3f2 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/changelog/v4.md @@ -0,0 +1,334 @@ +--- +title: v4.x (Upgrade from Ignition) +description: Breaking changes and migration guide for upgrading from Ignition (v2.x) to Alpha (v4.x). +displayed_sidebar: operatorsSidebar +--- + +## Overview + +**Migration difficulty**: High + +## Breaking changes + +### Node.js upgraded to v24 + +Node.js minimum version changed from v22 to v24.12.0. + +### Bot fee padding configuration renamed + +The bot configuration for fee padding has been renamed from "base fee" to "min fee". + +**v3.x:** + +```bash +--bot.baseFeePadding <value> ($BOT_BASE_FEE_PADDING) +``` + +**v4.0.0:** + +```bash +--bot.minFeePadding <value> ($BOT_MIN_FEE_PADDING) +``` + +**Migration**: Update your configuration to use the new flag name and environment variable. + +### L2Tips API restructured with checkpoint information + +The `getL2Tips()` RPC endpoint now returns a restructured response with additional checkpoint tracking. + +**v3.x response:** + +```json +{ + "latest": { "number": 100, "hash": "0x..." }, + "proven": { "number": 98, "hash": "0x..." }, + "finalized": { "number": 95, "hash": "0x..." } +} +``` + +**v4.0.0 response:** + +```json +{ + "proposed": { "number": 100, "hash": "0x..." }, + "checkpointed": { + "block": { "number": 99, "hash": "0x..." }, + "checkpoint": { "number": 10, "hash": "0x..." } + }, + "proven": { + "block": { "number": 98, "hash": "0x..." }, + "checkpoint": { "number": 9, "hash": "0x..." } + }, + "finalized": { + "block": { "number": 95, "hash": "0x..." }, + "checkpoint": { "number": 8, "hash": "0x..." } + } +} +``` + +**Migration**: + +- Replace `tips.latest` with `tips.proposed` +- For `checkpointed`, `proven`, and `finalized` tips, access block info via `.block` (e.g., `tips.proven.block.number`) + +### Block gas limits reworked + +The byte-based block size limit has been removed and replaced with field-based blob limits and automatic gas budget computation from L1 rollup limits. + +**Removed:** + +```bash +--maxBlockSizeInBytes <value> ($SEQ_MAX_BLOCK_SIZE_IN_BYTES) +``` + +**Changed to optional (now auto-computed from L1 if not set):** + +```bash +--maxL2BlockGas <value> ($SEQ_MAX_L2_BLOCK_GAS) +--maxDABlockGas <value> ($SEQ_MAX_DA_BLOCK_GAS) +``` + +**New (proposer):** + +```bash +--perBlockAllocationMultiplier <value> ($SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER) +--maxTxsPerCheckpoint <value> ($SEQ_MAX_TX_PER_CHECKPOINT) +``` + +**New (validator):** + +```bash +--validateMaxL2BlockGas <value> ($VALIDATOR_MAX_L2_BLOCK_GAS) +--validateMaxDABlockGas <value> ($VALIDATOR_MAX_DA_BLOCK_GAS) +--validateMaxTxsPerBlock <value> ($VALIDATOR_MAX_TX_PER_BLOCK) +--validateMaxTxsPerCheckpoint <value> ($VALIDATOR_MAX_TX_PER_CHECKPOINT) +``` + +**Migration**: Remove `SEQ_MAX_BLOCK_SIZE_IN_BYTES` from your configuration. Per-block L2 and DA gas budgets are now derived automatically as `(checkpointLimit / maxBlocks) * multiplier`, where the multiplier defaults to 2. You can still override `SEQ_MAX_L2_BLOCK_GAS` and `SEQ_MAX_DA_BLOCK_GAS` explicitly, but they will be capped at the checkpoint-level limits. Validators can now set independent per-block and per-checkpoint limits via the `VALIDATOR_` env vars; when not set, only checkpoint-level protocol limits are enforced. + +### Setup phase allow list requires function selectors + +The transaction setup phase allow list now enforces function selectors, restricting which specific functions can run during setup on whitelisted contracts. Previously, any public function on a whitelisted contract or class was permitted. + +The semantics of the environment variable `TX_PUBLIC_SETUP_ALLOWLIST` have changed: + +**v3.x:** + +```bash +--txPublicSetupAllowList <value> ($TX_PUBLIC_SETUP_ALLOWLIST) +``` + +The variable fully **replaced** the hardcoded defaults. Format allowed entries without selectors: `I:address`, `C:classId`. + +**v4.0.0:** + +```bash +--txPublicSetupAllowListExtend <value> ($TX_PUBLIC_SETUP_ALLOWLIST) +``` + +The variable now **extends** the hardcoded defaults (which are always present). Selectors are now mandatory. An optional flags segment can be appended for additional validation: + +``` +I:address:selector[:flags] +C:classId:selector[:flags] +``` + +Where `flags` is a `+`-separated list of: +- `os` — `onlySelf`: only allow calls where msg_sender == contract address +- `rn` — `rejectNullMsgSender`: reject calls with a null msg_sender +- `cl=N` — `calldataLength`: enforce exact calldata length of N fields + +Example: `C:0xabc:0x1234:os+cl=4` + +**Migration**: If you were using `TX_PUBLIC_SETUP_ALLOWLIST`, ensure all entries include function selectors. Note the variable now adds to defaults rather than replacing them. If you were not setting this variable, no action is needed — the hardcoded defaults now include the correct selectors automatically. + +### Token removed from default setup allowlist + +Token class-based entries (`_increase_public_balance` and `transfer_in_public`) have been removed from the default public setup allowlist. FPC-based fee payments using custom tokens no longer work out of the box. + +This change was made because Token class IDs change with aztec-nr releases, making the allowlist impossible to keep up to date with new library releases. In addition, `transfer_in_public` requires complex additional logic to be built into the node to prevent mass transaction invalidation attacks. **FPC-based fee payment with custom tokens won't work on mainnet alpha**. + +**Migration**: Node operators who need FPC support must manually add Token entries via `TX_PUBLIC_SETUP_ALLOWLIST`. Example: + +```bash +TX_PUBLIC_SETUP_ALLOWLIST="C:<tokenClassId>:<increaseBalanceSelector>:os+cl=3,C:<tokenClassId>:<transferInPublicSelector>:cl=5" +``` + +Replace `<tokenClassId>` with the deployed Token contract class ID and `<increaseBalanceSelector>`/`<transferInPublicSelector>` with the respective function selectors. Keep in mind that this will only work on local network setups, since even if you as an operator add these entries, other nodes will not have them and will not pick up these transactions. + +### Sequencer environment variable renames + +Several sequencer environment variables have been renamed: + +| Old variable | New variable | +|---|---| +| `SEQ_TX_POLLING_INTERVAL_MS` | `SEQ_POLLING_INTERVAL_MS` | +| `SEQ_MAX_L1_TX_INCLUSION_TIME_INTO_SLOT` | `SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT` | +| `SEQ_MAX_TX_PER_BLOCK` | `SEQ_MAX_TX_PER_CHECKPOINT` | +| `SEQ_MAX_BLOCK_SIZE_IN_BYTES` | Removed (see [Block gas limits reworked](#block-gas-limits-reworked)) | + +**Migration**: Search your configuration for the old variable names and replace them. The node will not recognize the old names. + +### Double signing slashing + +New slashable offenses have been introduced for duplicate proposals and duplicate attestations. Penalty amounts are currently set to 0, but the detection infrastructure is active. + +If you run redundant sequencer nodes, you **must** enable high-availability signing with PostgreSQL to prevent accidental double signing: + +```bash +VALIDATOR_HA_SIGNING_ENABLED=true +VALIDATOR_HA_DATABASE_URL=postgresql://<user>:<password>@<host>:<port>/<database> +VALIDATOR_HA_NODE_ID=<unique-id-per-node> +``` + +Run the database migration before starting your nodes: + +```bash +aztec migrate-ha-db up --database-url <url> +``` + +**Migration**: If you run a single node, no action is required. If you run redundant nodes for high availability, configure HA signing immediately. See the [High Availability Sequencers](../../setup/high_availability_sequencers) guide for details. + +### Blob-only data publication + +Transaction data is now published entirely via EIP-4844 blobs. The calldata fallback has been removed. + +Your consensus client (e.g., Lighthouse, Prysm) must run as a **supernode** or **semi-supernode** to make blobs available for retrieval. Standard pruning configurations will not retain blobs long enough. + +You should also configure blob file stores for redundancy: + +```bash +BLOB_FILE_STORE_URLS=<comma-separated-read-urls> +BLOB_FILE_STORE_UPLOAD_URL=<upload-url> +BLOB_ARCHIVE_API_URL=<archive-api-url> +``` + +**Migration**: Ensure your consensus client is configured as a supernode. If you previously relied on calldata for data availability, switch to blob-based retrieval. See the [Blob Storage](../../setup/blob_storage) guide for configuration details. + +### Withdrawal delay increase + +The governance execution delay has increased from 7 days to 30 days. This extends the time required for staker withdrawals from approximately 15 days to approximately 38 days. + +**Migration**: No configuration changes needed. Be aware that withdrawal processing will take longer after the upgrade. + +### Prover architecture change + +The prover now runs as a node subsystem rather than a separate standalone process. Start it alongside your node using the `--prover-node` flag: + +```bash +aztec start --node --prover-node +``` + +**Migration**: If you were running the prover as a separate process, update your deployment to run it as part of the node with `--prover-node`. + +## Removed features + +## New features + +### Initial ETH per fee asset configuration + +A new environment variable `AZTEC_INITIAL_ETH_PER_FEE_ASSET` has been added to configure the initial exchange rate between ETH and the fee asset (AZTEC) at contract deployment. This value uses 1e12 precision. + +**Default**: `10000000` (0.00001 ETH per AZTEC) + +**Configuration:** + +```bash +--initialEthPerFeeAsset <value> ($AZTEC_INITIAL_ETH_PER_FEE_ASSET) +``` + +This replaces the previous hardcoded default and allows network operators to set the starting price point for the fee asset. + +### `reloadKeystore` admin RPC endpoint + +Node operators can now update validator attester keys, coinbase, and fee recipient without restarting the node by calling the new `reloadKeystore` admin RPC endpoint. + +What is updated on reload: + +- Validator attester keys (add, remove, or replace) +- Coinbase and fee recipient per validator +- Publisher-to-validator mapping + +What is NOT updated (requires restart): + +- L1 publisher signers +- Prover keys +- HA signer connections + +New validators must use a publisher key already initialized at startup. Reload is rejected with a clear error if validation fails. + +### Admin API key authentication + +The admin JSON-RPC endpoint now supports auto-generated API key authentication. + +**Behavior:** + +- A cryptographically secure API key is auto-generated at first startup and displayed once via stdout +- Only the SHA-256 hash is persisted to `<dataDirectory>/admin/api_key_hash` +- The key is reused across restarts when `--data-directory` is set +- Supports both `x-api-key` and `Authorization: Bearer <key>` headers +- Health check endpoint (`GET /status`) is excluded from auth (for k8s probes) + +**Configuration:** + +```bash +--admin-api-key-hash <hex> ($AZTEC_ADMIN_API_KEY_HASH) # Use a pre-generated SHA-256 key hash +--disable-admin-api-key ($AZTEC_DISABLE_ADMIN_API_KEY) # Disable auth entirely +--reset-admin-api-key ($AZTEC_RESET_ADMIN_API_KEY) # Force key regeneration +``` + +**Helm charts**: Admin API key auth is disabled by default (`disableAdminApiKey: true`). Set to `false` in production values to enable. + +**Migration**: No action required — auth is opt-out. To enable, ensure `--disable-admin-api-key` is not set and note the key printed at startup. + +### Transaction pool error codes for RPC callers + +Transaction submission via RPC now returns structured rejection codes when a transaction is rejected by the mempool: + +- `LOW_PRIORITY_FEE` — tx priority fee is too low +- `INSUFFICIENT_FEE_PAYER_BALANCE` — fee payer doesn't have enough balance +- `NULLIFIER_CONFLICT` — conflicting nullifier already in pool + +**Impact**: Improved developer experience — callers can now programmatically handle specific rejection reasons. + +### RPC transaction replacement price bump + +Transactions submitted via RPC that clash on nullifiers with existing pool transactions must now pay at least X% more in priority fee to replace them. The same bump applies when the pool is full and the incoming tx needs to evict the lowest-priority tx. P2P gossip behavior is unchanged. + +**Configuration:** + +```bash +P2P_RPC_PRICE_BUMP_PERCENTAGE=10 # default: 10 (percent) +``` + +Set to `0` to disable the percentage-based bump (still requires strictly higher fee). + +### Validator-specific block limits + +Validators can now enforce per-block and per-checkpoint limits independently from the sequencer (proposer) limits. This allows operators to accept proposals that exceed their own proposer settings, or to reject proposals that are too large even if the proposer's limits allow them. + +**Configuration:** + +```bash +VALIDATOR_MAX_L2_BLOCK_GAS=<value> # Max L2 gas per block for validation +VALIDATOR_MAX_DA_BLOCK_GAS=<value> # Max DA gas per block for validation +VALIDATOR_MAX_TX_PER_BLOCK=<value> # Max txs per block for validation +VALIDATOR_MAX_TX_PER_CHECKPOINT=<value> # Max txs per checkpoint for validation +``` + +When not set, no per-block limit is enforced for that dimension — only checkpoint-level protocol limits apply. These do not fall back to the `SEQ_` values. + +### Setup allow list extendable via network config + +The setup phase allow list can now be extended via the network configuration JSON (`txPublicSetupAllowListExtend` field). This allows network operators to distribute additional allowed setup functions to all nodes without requiring code changes. The local environment variable takes precedence over the network-json value. + +## Changed defaults + +## Troubleshooting + +## Next steps + +- [How to Run a Sequencer Node](../../setup/sequencer_management) - Updated setup instructions +- [Advanced Keystore Usage](../../keystore/index.md) - Keystore configuration +- [Ethereum RPC Calls Reference](../ethereum_rpc_reference) - Infrastructure requirements +- [Aztec Discord](https://discord.gg/aztec) - Upgrade support diff --git a/docs/lines/v6/docs-operate/operators/reference/changelog/v5.2.md b/docs/lines/v6/docs-operate/operators/reference/changelog/v5.2.md new file mode 100644 index 00000000000..dd935369b82 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/changelog/v5.2.md @@ -0,0 +1,81 @@ +--- +title: v5.2.0 +description: "Operator-facing changes in the v5.2.0 release: peer-gating, own-validator slash warnings, and new RPC server settings." +displayed_sidebar: operatorsSidebar +--- + +## Overview + +**Migration difficulty**: low. No action is required. Every new setting defaults to the previous behaviour, protocol constants are unchanged, and v5.1.0 and v5.2.0 nodes interoperate, so you can roll your fleet in any order with no downtime window. + +## Behaviour changes + +### A node whose p2p service fails to start now exits + +Previously a node whose libp2p stack failed to start kept running with a dead p2p service: peerless, silently useless, and indistinguishable from a healthy node in the process table. It now logs `Failed to start p2p service` at fatal and aborts startup. The usual cause is the TCP listener failing to bind, so free or change the P2P port and restart. + +### A peerless node stops acting + +While a node has zero connected peers it: + +- warns periodically that gossip and transaction propagation are unavailable, +- skips building and proposing checkpoints while below `SEQ_MIN_PEERS_TO_PROPOSE` (default `1`), +- does not file data-withholding slashing offenses for slots probed while peerless, +- rejects incoming `sendTx` calls rather than dropping them silently. + +L1-only duties (governance and slashing votes, prune, invalidation) still run, and setups that disable p2p entirely are unaffected. Set `SEQ_MIN_PEERS_TO_PROPOSE=0` to restore the previous proposing behaviour. + +### You are warned when the network votes to slash your validators + +The node logs `Own validator 0x... targeted by slashing vote (N of Q votes needed to slash)` on every vote naming one of your attesters, and `Own validator 0x... was slashed for ...` once a round executes. The same signal is exported as metrics: alert on `aztec_slasher_own_validator_current_round_votes_max` against `aztec_slasher_quorum_size`. See [Metrics reference](/operate/operators/monitoring/metrics-reference). + +### L1 event watching moved off `eth_newFilter` + +L1 event watchers now poll `eth_getLogs` over a bounded block range instead of installing server-side filters. Behind a load-balanced RPC provider, filter polls could be routed to a backend that had never seen the filter, producing request storms of 30 to 40 req/s while silently missing slashing and governance events. Hosted providers no longer need to support filter methods. + +### A stale governance payload no longer wastes gas + +A sequencer left with an outdated `GOVERNANCE_PROPOSER_PAYLOAD` kept signalling a payload that had already executed, burning roughly 100k gas per slot. The node now detects a live or already-executed proposal for the configured payload and stops signalling. Set `GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE=true` to keep signalling anyway. + +### `GET /status` returns a JSON body + +The status endpoint previously set only an HTTP status code. It now also returns per-component health as JSON, for example `{ "ok": true, "components": { "p2p": { "healthy": true, "enabled": true, "connectedPeers": 12 } } }`. Consumers that only read the HTTP status code are unaffected. + +### JSON-RPC internal errors use code `-32603` + +Internal server failures returned `-32600` ("Invalid Request"); they now return `-32603` ("Internal error"), which is what the JSON-RPC 2.0 specification defines. Genuine malformed requests still return `-32600`. + +## New configuration + +All defaults preserve v5.1.0 behaviour. See the [CLI reference](/operate/operators/reference/cli-reference) for the full flag list. + +| Environment variable | Flag | Default | What it does | +| --- | --- | --- | --- | +| `SEQ_MIN_PEERS_TO_PROPOSE` | `--sequencer.minPeersToPropose` | `1` | Minimum connected peers required to build and propose a checkpoint. `0` disables the gate. | +| `P2P_HEALTH_MIN_PEERS` | `--p2p.p2pHealthMinPeers` | `0` | Peer count below which the p2p component reports unhealthy on `/status`. `0` never fails. | +| `GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE` | `--sequencer.governanceProposerForcePayloadVote` | `false` | Keep signalling the configured governance payload even after its proposal went live or executed. | +| `RPC_HTTP_KEEP_ALIVE_TIMEOUT_MS` | `--rpc-http-keep-alive-timeout-ms` | `5000` | HTTP keep-alive timeout for JSON-RPC connections. | +| `RPC_HTTP_HEADERS_TIMEOUT_MS` | `--rpc-http-headers-timeout-ms` | `60000` | Timeout for receiving complete HTTP headers on JSON-RPC connections. | +| `RPC_CORS_ALLOWED_HEADERS` | `--rpc-cors-allowed-headers` | empty | Headers allowed in cross-origin JSON-RPC requests. | +| `RPC_CORS_ALLOWED_ORIGINS` | `--rpc-cors-allowed-origins` | empty | Origins allowed to make credentialed cross-origin JSON-RPC requests. | +| `RPC_CORS_ALLOW_ANY_ORIGIN` | `--rpc-cors-allow-any-origin` | `false` | Allow credentialed cross-origin JSON-RPC requests from any origin. | +| `USE_AWS_LOGGING` | | `false` | Map log levels to AWS severity values. | + +## Reliability and performance + +- **Prover memory.** The prover stack held every checkpoint's transactions, circuit inputs, and proofs in memory long after they were needed, some of it duplicating data already on disk. That data is now released promptly and read back from disk on demand. +- **Epoch proving retries instead of giving up.** An epoch is declared failed only when its submission window expires, not on the first fault. A chain prune during proving no longer counts as a failure, stuck epochs are not pointlessly re-proved, and per-checkpoint failure diagnostics are uploaded as they happen. +- **Mempool.** Incoming gossiped transactions are no longer held up by the pool's background archiving work, and every transaction dropped from the mempool during block building is logged at warn with its reason. +- **Archiver.** Reorged blocks are fully cleaned up, including blocks whose transactions were re-included elsewhere, and a race reading L2 to L1 message proofs is fixed. +- **Sync.** A race causing spurious `block_not_available` errors is fixed. +- **L1.** Spurious "insufficient funds" errors when simulating L1 calls are fixed, and the KZG trusted setup loads at startup rather than stalling the first blob operation. +- **Slashing accountability.** Block proposals containing invalid transactions are now correctly flagged as slashable; an error in transaction validation previously let such proposers escape accountability. +- **Observability.** JSON-RPC servers report request-level metrics, and JSON-RPC clients accept and send cookies so load balancers using session affinity work. + +## Security + +Everything the node parses from untrusted input now has explicit size bounds, closing memory-exhaustion holes: tree sibling paths and vectors are bounded, arrays in the `Tx` and `HashedValues` schemas are capped, simulation overrides and checkpoint queries on the RPC surface are capped, and gaps in checkpoint block-shape and block-count validation are closed. Most of this lives in shared serialization code, so clients talking to a node they do not trust get the same protection. + +## Full release notes + +[v5.2.0 on GitHub](https://github.com/AztecProtocol/aztec-packages/releases/tag/v5.2.0) diff --git a/docs/lines/v6/docs-operate/operators/reference/changelog/v6.md b/docs/lines/v6/docs-operate/operators/reference/changelog/v6.md new file mode 100644 index 00000000000..b8328065128 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/changelog/v6.md @@ -0,0 +1,413 @@ +--- +title: v6.0.0 +description: "Operator-facing changes in the v6 release: when to switch, new image and package locations, the noir-execute config rename, the trace-capable L1 endpoint requirement, and attester-initiated sequencer exits." +displayed_sidebar: operatorsSidebar +--- + +## Overview + +**Migration difficulty**: high, mostly because almost everything you pull from moved. + +The v6 rollup is a fresh deployment starting from genesis. Run your v6 node with a new, +empty data directory: no v5 chain state carries over. + +Two things to settle before you start: every artifact location changed, and the +`ACVM_*` configuration variables were renamed with no fallback. A node doing real +proving fails to start on the old names; one that is not falls back to WASM +simulation and starts anyway, much slower, so a clean startup is not evidence that +your configuration migrated. + +## When to switch on Mainnet + +The v6 rollup becomes canonical on Mainnet when governance executes the v6 upgrade +proposal. The earliest execution time is **Monday 26 October 2026, 08:00 UTC**. Until then, +v5 is the canonical rollup and your v5 node keeps doing the work. + +You have two options. + +**Option 1 (recommended): stand up a v6 node in standby now.** Start a v6 node next to +your v5 node any time before the execution date. At startup it compares the canonical +rollup's genesis archive root, VK tree root and protocol contracts hash with its own. +While v5 is still canonical they do not match, so the node enters standby mode: it serves +only a health endpoint (which reports healthy, so liveness probes pass), polls L1 once a +minute, and uses very few resources. Once the v6 rollup becomes canonical, the node +logs `Compatible rollup found at <address>. Exiting standby mode.` and starts normally, +with no action from you. You see this at startup: + +```text +Rollup at 0x... is incompatible: ... Entering standby mode. Will poll every 60s for a compatible rollup... +``` + +If the v6 node shares a host with the v5 node, give it its own data directory, container name, +`AZTEC_PORT` and `P2P_PORT`. Standby binds the HTTP port as soon as it starts, and the node binds +the P2P port when it leaves standby, so reusing the v5 values makes the v6 node fail at one of +those two moments. + +Standby only works for a node that follows the canonical rollup, which is the default. +Do not pin `ROLLUP_VERSION` to the v6 rollup's version: the v6 rollup is not in the +registry until the proposal executes, so the node cannot look it up and fails to start. + +Keep the v5 node running until the v6 node has left standby and is attesting. + +**Option 2: upgrade at execution time.** Stop your v5 node and start the v6 node on +Monday 26 October 2026 at 08:00 UTC, or as soon after as the proposal executes. This +works, but your sequencer does nothing until the v6 node has started and synced, and you +have to be there at the time. + +:::warning High-availability setups sharing a signing database +v5 nodes delete other rollups' signing records from a shared PostgreSQL HA signing +database when they start. If a v5 replica restarts after the v6 replica has started +signing, the v6 replica loses its double-signing protection. Give the v6 replicas their +own database, or do not restart v5 replicas after the switch to v6. See +[Signing history and rollup upgrades](/operate/operators/setup/high_availability_sequencers#signing-history-and-rollup-upgrades). +::: + +## New repository, image and package locations + +Everything moved. Old locations keep what was already published, so existing v5 setups +keep working, but nothing new lands there. + +| | v5 and earlier | v6 | +| --- | --- | --- | +| Source repository | `AztecProtocol/aztec-packages` | `aztec-labs-eng/aztec-node` | +| Docker images | `aztecprotocol/aztec` | `azteclabs/aztec` | +| Prover agent image | `aztecprotocol/aztec-prover-agent` | `azteclabs/aztec-prover-agent` | +| npm (client packages) | `@aztec/*` | `@aztec-labs/*` | +| npm (foundation packages) | `@aztec/bb.js`, `@aztec/noir-*` | `@aztec-foundation/*` | +| Aztec.nr | in the monorepo | `aztec-labs-eng/aztec-nr` | + +Update your image references first. A compose file or Helm values still pointing at +`aztecprotocol/aztec` will pull a v5 image and fail against a v6 network. + +```diff +- image: aztecprotocol/aztec:5.2.0 ++ image: azteclabs/aztec:6.0.0 +``` + +The `@aztec` npm scope keeps the packages published for earlier versions, so an existing +project pinned to a v5 version continues to resolve. The packages now depend on upstream +`viem` instead of the `@aztec/viem` fork. + +## Behaviour changes + +### World-state history retention uses one policy + +```bash +--world-state-history-retention <policy> ($WS_HISTORY_RETENTION) +``` + +Replace `WS_NUM_HISTORIC_CHECKPOINTS=N` or `WS_NUM_HISTORIC_BLOCKS=N` with `WS_HISTORY_RETENTION=rolling:N`. +Replace values below 1 with `WS_HISTORY_RETENTION=archive`. The former `--world-state-checkpoint-history` flag is removed. + +- `rolling:N` retains the latest N finalized checkpoints and all newer state. The default is `rolling:64`. +- `pinned:N` retains checkpoint N and every later checkpoint permanently, including every block of the pinned checkpoint. +- `archive` retains all history from genesis and disables history pruning. + +Rolling counts and pinned checkpoint numbers must be positive integers. Increasing a rolling window accumulates history +as the chain advances. Pinned and archive modes reject databases missing required history, even with `SYNC_MODE=force-snapshot`. +Use a fresh world-state database to rebuild missing history. Fresh databases try compatible snapshots before replaying from +genesis; pinned mode only accepts snapshots in the pinned checkpoint or earlier, and archive mode requires full history. + +### `ACVM_*` configuration renamed to `NOIR_EXECUTE_*` + +Protocol circuits are now executed with noir's `noir-execute` rather than the `acvm` +binary, and the configuration naming the executor follows the tool. What you point it at +is unchanged. + +| Old | New | +| --- | --- | +| `ACVM_BINARY_PATH` | `NOIR_EXECUTE_BINARY_PATH` | +| `ACVM_WORKING_DIRECTORY` | `NOIR_EXECUTE_WORKING_DIRECTORY` | +| `--sequencer.acvmBinaryPath` | `--sequencer.noirExecuteBinaryPath` | +| `--sequencer.acvmWorkingDirectory` | `--sequencer.noirExecuteWorkingDirectory` | +| `--proverNode.acvmBinaryPath` | `--proverNode.noirExecuteBinaryPath` | +| `--proverNode.acvmWorkingDirectory` | `--proverNode.noirExecuteWorkingDirectory` | + +```diff +- ACVM_BINARY_PATH=/usr/src/labs-aztec-toolchain/bin/noir-execute +- ACVM_WORKING_DIRECTORY=/usr/src/acvm ++ NOIR_EXECUTE_BINARY_PATH=/usr/src/labs-aztec-toolchain/bin/noir-execute ++ NOIR_EXECUTE_WORKING_DIRECTORY=/usr/src/noir-execute +``` + +**There is no fallback to the old names.** A prover agent exits with `Requested real +proving but no path to bb or noir-execute binaries provided`; a prover node or sequencer +configured for real proofs throws from `BBNativeRollupProver.new` when it tries to stat +an undefined path. Only a node running without real proofs survives, by falling back to +WASM simulation, which is much slower, so a node that appears to start may still be +badly degraded. + +If you run the release image it sets the new variables itself, and its default working +directory moves from `/usr/src/acvm` to `/usr/src/noir-execute`. Check any values you +override. + +### A trace-capable L1 endpoint is required by default + +`ETHEREUM_ALLOW_NO_DEBUG_HOSTS` now defaults to `false`. + +| Flag | Environment Variable | Previous | New | +| --- | --- | --- | --- | +| `--archiver.ethereumAllowNoDebugHosts` | `ETHEREUM_ALLOW_NO_DEBUG_HOSTS` | `true` | `false` | + +At startup the node checks that the endpoint it traces with (`ETHEREUM_DEBUG_HOSTS`, or +`ETHEREUM_HOSTS` when that is unset) serves `debug_traceTransaction` or +`trace_transaction`. It used to log a warning and start when neither worked. It now +refuses to start, with an error naming `ETHEREUM_ALLOW_NO_DEBUG_HOSTS`. + +A checkpoint published through a non-standard contract, rather than by a direct call to +the rollup, cannot be read from the transaction calldata alone: the node needs a trace to +recover it. A node without one starts normally and then stops syncing at the first such +checkpoint, until it is pointed at a trace-capable endpoint and restarted. Failing at +startup surfaces the problem before the node is relied on. + +**Migration**: point `ETHEREUM_DEBUG_HOSTS` at an endpoint that serves trace methods +(see [L1 RPC requirements](/operate/operators/concepts/l1-rpc#debug-traces)). If you +accept the risk of stalling, set `ETHEREUM_ALLOW_NO_DEBUG_HOSTS=true` to keep the old +behaviour. + +### Attester-initiated sequencer exits + +A position can now be exited by its attester, not only by its withdrawer. Exits can also +be batched: each attester signs an exit authorization, and one relayer submits them all in +a single transaction. This is what makes it practical to exit tens or hundreds of +positions, for example as a staking provider. + +The stake still goes to the position's **withdrawer**, never to the attester. An +attester-initiated exit starts the exit delay; the withdrawer then names the recipient, +and anyone can finalize once the delay has passed. Attester-initiated exits are +rate-limited to about 5% of the sequencer set per rolling window, only work on the +canonical rollup, and so on Mainnet only after the v6 upgrade executes. + +See [Exiting a sequencer](/operate/operators/sequencer-management/exiting-a-sequencer) for +the full procedure, the limits, and troubleshooting. In short: + +```bash +# Each attester signs an authorization, appended to one file +aztec sign-attester-exit --private-key $ATTESTER_KEY --attester $ATTESTER \ + --rollup $ROLLUP --l1-chain-id $L1_CHAIN_ID --deadline $DEADLINE \ + --output exits.json --append + +# Simulate, then submit with any funded key +aztec initiate-withdraw-by-attester-batch --dry-run --authorizations exits.json \ + --rollup $ROLLUP --l1-chain-id $L1_CHAIN_ID --l1-rpc-urls $L1_RPC +aztec initiate-withdraw-by-attester-batch --authorizations exits.json \ + --rollup $ROLLUP --l1-chain-id $L1_CHAIN_ID --l1-rpc-urls $L1_RPC \ + --private-key $RELAYER_KEY +``` + +`aztec initiate-withdraw-by-attester` exits a single position in a transaction sent from +the attester key itself. + +Three functions are live on the rollup: + +| Function | What it does | +| --- | --- | +| `initiateWithdrawByAttester(address)` | Exit one position. Caller must be the attester. | +| `initiateWithdrawByAttesterBatch(...)` | Relay many attester-signed exits. Reverts if the rate limit cannot fit the whole batch. | +| `initiateWithdrawByAttesterBatchUpToLimit(...)` | Relay many attester-signed exits, processing only as many from the start of the list as the rate limit allows and leaving the rest for later. | + +Both batch functions revert the whole transaction if any entry they process is invalid: +an expired deadline, a bad signature, an attester that is already exiting or has no stake. +`UpToLimit` does not check the entries it leaves for later. +Run `--dry-run` before submitting. Pass `--up-to-limit` to the batch command to use the +`UpToLimit` function. + +#### Calling the contract directly + +If you cannot use the CLI, call the rollup with `cast`. Exit a single position, signed +and submitted by the attester: + +```bash +cast send $ROLLUP "initiateWithdrawByAttester(address)" $ATTESTER \ + --private-key $ATTESTER_KEY --rpc-url $L1_RPC +``` + +Batching needs an EIP-712 signature per attester. The domain is +`{ name: "Aztec Rollup", version: "1", chainId: <L1 chain id>, verifyingContract: <rollup> }` +and the type is `AttesterExit(address attester,uint256 deadline)`. Sign one per position: + +```bash +cast wallet sign --private-key $ATTESTER_KEY --data '{ + "domain":{"name":"Aztec Rollup","version":"1","chainId":'$L1_CHAIN_ID',"verifyingContract":"'$ROLLUP'"}, + "types":{"AttesterExit":[{"name":"attester","type":"address"},{"name":"deadline","type":"uint256"}]}, + "primaryType":"AttesterExit", + "message":{"attester":"'$ATTESTER'","deadline":"'$DEADLINE'"} +}' +``` + +Then relay them. Each entry is `(attester, deadline, (v, r, s))`, and the relayer pays +the gas, so the submitting key need not be an attester: + +```bash +cast send $ROLLUP \ + "initiateWithdrawByAttesterBatchUpToLimit((address,uint256,(uint8,bytes32,bytes32))[])" \ + "[($ATTESTER_0,$DEADLINE,($V0,$R0,$S0)),($ATTESTER_1,$DEADLINE,($V1,$R1,$S1))]" \ + --private-key $RELAYER_KEY --rpc-url $L1_RPC +``` + +`deadline` is a unix timestamp and must be in the future. Each attester may appear only +once per batch. + +Withdrawer-initiated exits are unchanged. + +### `remove-l1-validator` requires `--recipient` + +`aztec remove-l1-validator` now requires `--recipient`, the address that receives the withdrawn stake. Previously it +always named the attester's own address as the recipient, sending the stake to the hot attester key (for delegated +stake, a key the provider holds), and a recipient cannot be changed once set. + +**Migration**: add `--recipient [RECIPIENT_ADDRESS]` to any script that calls `remove-l1-validator`. Without it the +command fails before sending anything. The same command, sent from the withdrawer key, names the recipient after an +attester-initiated exit. + +### `aztec sequencers` is removed + +The `aztec sequencers` command (`list`, `add`, `remove`, `who-next`, `flush`) is gone. Its defaults pointed at a local +development chain, and nothing used it. + +**Migration**: use `aztec remove-l1-validator --recipient [RECIPIENT_ADDRESS]` to exit and `aztec add-l1-validator` to +deposit. `aztec debug-rollup` lists the registered validators and the current proposer. To flush the entry queue, call +the rollup directly: + +```bash +cast send $ROLLUP "flushEntryQueue(uint256)" 8 --private-key [PRIVATE_KEY] --rpc-url $L1_RPC +``` + +Each call activates up to 8 queued validators, so repeat it while `getEntryQueueLength()` is non-zero and flushes are +available. + +### Provers can skip full-epoch proving + +A new prover node setting stops the node from proving complete epochs on its own: + +```bash +--proverNode.proverNodeDisableFullEpochProving ($PROVER_NODE_DISABLE_FULL_EPOCH_PROVING) +``` + +The default is `false`, so nothing changes unless you set it. With it set, the prover node +logs `Full epoch proving disabled` at startup and only proves the partial epochs +requested through its `startProof` admin method. Use it to run a prover that serves paid +partial-epoch proofs (for example, for early withdrawals) without competing for full +epoch proofs. + +### Change the log level without a restart + +The node admin config has a new `logLevel` key. Set it with `aztecAdmin_setConfig`, using +the same format as `LOG_LEVEL`, and read it back with `aztecAdmin_getConfig`: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_setConfig","params":[{"logLevel":"debug;trace:sequencer,p2p"}],"id":1}' +``` + +The change applies to the node process immediately, including loggers created later. It +does not reach processes the node spawns, and a restart reverts to `LOG_LEVEL`. + +### Publishers must afford the L1 transaction before work starts + +Nodes now check that a publisher holds enough ETH for its L1 transaction before doing +the work behind it. The amount checked is a worst case: the transaction's gas limit +times the max fee per gas, plus blob fees, at current L1 fees. At a 1 gwei base fee a +proposal needs a few thousandths of an ETH. + +- **Sequencers**: if no publisher for the proposer can afford a checkpoint proposal, + the node skips the slot before building or gossiping anything. It logs an error with + each publisher's balance and the required amount, and records the + `insufficient_publisher_balance` reason on + `aztec.sequencer.checkpoint.precheck_failed_count`. Governance and slashing votes are + still attempted. If a publisher can afford the proposal but not the votes bundled + with it, the votes are dropped and the proposal is sent alone. +- **Provers**: a prover node still proves an epoch while no publisher can afford the + proof submission, but holds the finished proof back instead of sending it. It + retries every 12 seconds and submits as soon as a publisher is funded, until the + proof submission window closes. `aztec.prover_node.proof_submission_unaffordable_count` + counts the held back proofs. Nothing changes when proof publishing is disabled. +- **Selection**: a publisher with a recently mined transaction is no longer ranked + below an idle one with less ETH, so a dust-funded idle publisher can no longer win + every proposal. A publisher whose transaction was rejected for insufficient funds is + skipped until its balance increases, or for at most five minutes. + +### Publisher balances are checked every minute + +Sequencers and prover nodes now read every publisher's balance once a minute, whether +or not the publisher has sent anything, and price a reference transaction at current +L1 fees: the first checkpoint proposal of an epoch for a sequencer, an epoch proof +submission for a prover. At startup the node logs a `Publisher balances` line with +every publisher's balance, at error level if none can afford the reference +transaction. While that lasts, it logs `No publisher can afford a proposal` (or +`an epoch proof`) at most every 10 minutes, naming the publisher to fund and how +much ETH it is missing. The skipped proposal and held back proof errors name them too. + +A warning is logged when a publisher's balance falls below a multiple of the cost of +the reference transaction: + +```bash +--sequencer.publisherLowBalanceWarningMultiplier <value> ($PUBLISHER_LOW_BALANCE_WARNING_MULTIPLIER) +--proverNode.publisherLowBalanceWarningMultiplier <value> ($PUBLISHER_LOW_BALANCE_WARNING_MULTIPLIER) +``` + +The default is `4`. If you use [automatic top-ups](/operate/operators/solo-sequencer/fund-accounts#optional-automatic-top-ups), +set `PUBLISHER_FUNDING_THRESHOLD` above the warning level; the node warns if it is not. +Top-ups are now checked every minute instead of every 2 minutes. + +Metric changes (Prometheus names): + +- `aztec_l1_publisher_balance_eth` is now recorded every minute for every publisher, + instead of only after a transaction is sent. Alerts on it now fire for a publisher + that has never sent, or stopped sending because it cannot afford to. It gains an + `aztec_l1_tx_scope` label (`sequencer` or `prover`). +- New `aztec_l1_publisher_sends_affordable`: how many reference transactions each + publisher's balance covers at current fees. Alert when it drops below 1. +- New `aztec_l1_publisher_required_balance_eth`: the cost of one reference transaction + at current fees. +- New `aztec_l1_publisher_funder_balance_eth`: the balance of the funding account used + for automatic top-ups. +- `aztec_l1_balance_eth` is deprecated and will be removed in a later release. Use + `aztec_l1_publisher_balance_eth` instead. +- `aztec_l1_gas_price_wei` and `aztec_l1_blob_base_fee_wei` are now exported. They were + defined before but never reached the exporter. + +See [Metrics reference](/operate/operators/monitoring/metrics-reference#l1-publisher-eth-balance) +for example alert rules. + +## New node RPC methods + +Two additions to the JSON-RPC surface. Both are additive; nothing was removed. + +| Method | What it is for | +| --- | --- | +| `aztec_getTxEffectMembershipWitness` | Membership proof for a transaction's effects. | +| `aztec_getValidatorStatsBatch` | Validator statistics for several attesters in one call. | + +Both currently render under "Other methods" in the +[node API reference](/operate/operators/reference/node_api_reference) because the +generator has no grouping for them yet. + +## Upgrade checklist + +1. Point your image references at `azteclabs/aztec` and `azteclabs/aztec-prover-agent`. +2. Rename `ACVM_*` to `NOIR_EXECUTE_*` in your environment, compose files or Helm values, + **before** starting a v6 node. +3. Confirm any overridden working directory matches the new default if you rely on the + release image. +4. Give the v6 node a new, empty data directory. The v6 rollup starts from genesis. +5. Fund your sequencer and prover publishers. A node whose publishers cannot afford the + L1 transaction skips the proposal or holds back the proof submission. Alert on + `aztec_l1_publisher_sends_affordable`, and move any alert or dashboard on + `aztec_l1_balance_eth` to `aztec_l1_publisher_balance_eth`. +6. Check that `ETHEREUM_DEBUG_HOSTS` (or `ETHEREUM_HOSTS`, if you leave it unset) serves + `debug_traceTransaction` or `trace_transaction`. A v6 node refuses to start without + one unless you set `ETHEREUM_ALLOW_NO_DEBUG_HOSTS=true`. +7. On Mainnet, start the v6 node before the upgrade executes and let it wait in + [standby](#when-to-switch-on-mainnet), leaving `ROLLUP_VERSION` unset. Keep the v5 node + running until the v6 node has left standby and is attesting. +8. If you run HA signing on a shared PostgreSQL database, give the v6 replicas their own + database or do not restart v5 replicas after the switch to v6. + +## Full release notes + +For the developer-facing changes in this release (the npm scope moves in detail, the +aztec-nr repository move, Aztec.js artifact and return-type changes, and the removed CLI +commands), see the [migration notes](/developers/docs/resources/migration_notes). diff --git a/docs/lines/v6/docs-operate/operators/reference/cli-reference.md b/docs/lines/v6/docs-operate/operators/reference/cli-reference.md new file mode 100644 index 00000000000..2ed895ce4d3 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/cli-reference.md @@ -0,0 +1,1153 @@ +--- +displayed_sidebar: operatorsSidebar +title: Cli Reference +description: A reference of the --help output when running aztec start. +references: ["labs/yarn-project/aztec/src/cli/aztec_start_options.ts", "labs/yarn-project/aztec/src/cli/cli.ts"] +keywords: + [ + aztec, + prover, + node, + blockchain, + L2, + scaling, + ethereum, + zero-knowledge, + ZK, + setup, + ] +tags: + - prover + - node + - tutorial + - infrastructure +--- + +**Configuration notes:** + +- The environment variable name corresponding to each flag is shown as $ENV_VAR on the right hand side. +- If two subsystems can contain the same configuration option, only one needs to be provided. For example, `--archiver.blobSinkUrl` and `--sequencer.blobSinkUrl` point to the same value. + +```bash + + MISC + + --network <value> ($NETWORK) + Network to run Aztec on + + --enable-auto-shutdown ($ENABLE_AUTO_SHUTDOWN) + Soft-shutdown the node when the canonical rollup is no longer compatible (protocol constants diverge), keeping the health server up so K8s probes keep passing. Only applies to nodes following the canonical rollup. + + --sync-mode <value> (default: snapshot) ($SYNC_MODE) + Set sync mode to `full` to always sync via L1, `snapshot` to download a snapshot if there is no local data, `force-snapshot` to download even if there is local data. + + --snapshots-urls <value> (default: ) ($SYNC_SNAPSHOTS_URLS) + Base URLs for snapshots index, comma-separated. + + --fisherman-mode ($FISHERMAN_MODE) + Whether to run in fisherman mode. + + LOCAL_NETWORK + + --local-network + Starts Aztec Local Network + + --local-network.l1Mnemonic <value> (default: test test test test test test test test test test test junk)($MNEMONIC) + Mnemonic for L1 accounts. Will be used + + --local-network.testAccounts (default: true) ($TEST_ACCOUNTS) + Deploy test accounts on local network start + + API + + --port <value> (default: 8080) ($AZTEC_PORT) + Port to run the Aztec Services on + + --admin-port <value> (default: 8880) ($AZTEC_ADMIN_PORT) + Port to run admin APIs of Aztec Services on + + --admin-api-key-hash <value> ($AZTEC_ADMIN_API_KEY_HASH) + SHA-256 hex hash of a pre-generated admin API key. When set, the node uses this hash for authentication instead of auto-generating a key. + + --disable-admin-api-key ($AZTEC_DISABLE_ADMIN_API_KEY) + Disable API key authentication on the admin RPC endpoint. By default, a key is auto-generated, displayed once, and its hash is persisted. + + --reset-admin-api-key [value] ($AZTEC_RESET_ADMIN_API_KEY) + Force-generate a new admin API key, replacing any previously persisted key hash. The new key is displayed once at startup. + + --node-debug ($AZTEC_NODE_DEBUG) + Expose debug endpoints (e.g. mineBlock) on the main RPC port + + --api-prefix <value> ($API_PREFIX) + Prefix for API routes on any service that is started + + --rpcMaxBatchSize <value> (default: 100) ($RPC_MAX_BATCH_SIZE) + Maximum allowed batch size for JSON RPC batch requests. + + --rpcMaxBodySize <value> (default: 1mb) ($RPC_MAX_BODY_SIZE) + Maximum allowed batch size for JSON RPC batch requests. + + --rpc-http-keep-alive-timeout-ms <value> (default: 5000) ($RPC_HTTP_KEEP_ALIVE_TIMEOUT_MS) + HTTP keep-alive timeout for JSON RPC connections in milliseconds. + + --rpc-http-headers-timeout-ms <value> (default: 60000) ($RPC_HTTP_HEADERS_TIMEOUT_MS) + Timeout for receiving complete HTTP headers on JSON RPC connections in milliseconds. + + --rpc-cors-allowed-headers <value> (default: ) ($RPC_CORS_ALLOWED_HEADERS) + Headers allowed in cross-origin JSON RPC requests, separated by commas. + + --rpc-cors-allowed-origins <value> (default: ) ($RPC_CORS_ALLOWED_ORIGINS) + Origins allowed to make credentialed cross-origin JSON RPC requests, separated by commas. + + --rpc-cors-allow-any-origin ($RPC_CORS_ALLOW_ANY_ORIGIN) + Allow credentialed cross-origin JSON RPC requests from any origin. + + ETHEREUM + + --l1-chain-id <value> ($L1_CHAIN_ID) + The chain ID of the ethereum host. + + --l1-rpc-urls <value> ($ETHEREUM_HOSTS) + List of URLs of Ethereum RPC nodes that services will connect to (comma separated). + + --l1-consensus-host-urls <value> ($L1_CONSENSUS_HOST_URLS) + List of URLs of the Ethereum consensus nodes that services will connect to (comma separated) + + --l1-consensus-host-api-keys <value> ($L1_CONSENSUS_HOST_API_KEYS) + List of API keys for the corresponding L1 consensus clients, if needed. Added to the end of the corresponding URL as "?key=<api-key>" unless a header is defined + + --l1-consensus-host-api-key-headers <value> ($L1_CONSENSUS_HOST_API_KEY_HEADERS) + List of header names for the corresponding L1 consensus client API keys, if needed. Added to the corresponding request as "<api-key-header>: <api-key>" + + L1 CONTRACTS + + --registry-address <value> ($REGISTRY_CONTRACT_ADDRESS) + The deployed L1 registry contract address. + + --rollup-version <value> ($ROLLUP_VERSION) + The version of the rollup. + + STORAGE + + --data-directory <value> ($DATA_DIRECTORY) + Optional dir to store data. If omitted will store in memory. + + --data-store-map-size-kb <value> (default: 134217728) ($DATA_STORE_MAP_SIZE_KB) + The maximum possible size of a data store DB in KB. Can be overridden by component-specific options. + + WORLD STATE + + --world-state-data-directory <value> ($WS_DATA_DIRECTORY) + Optional directory for the world state database + + --world-state-db-map-size-kb <value> ($WS_DB_MAP_SIZE_KB) + The maximum possible size of the world state DB in KB. Overwrites the general dataStoreMapSizeKb. + + --world-state-history-retention <value> (default: rolling:64) ($WS_HISTORY_RETENTION) + World-state history retention: rolling:N retains N finalized checkpoints, pinned:N retains checkpoint N onwards, archive retains all history. + + AZTEC NODE + + --node + Starts Aztec Node with options + + ARCHIVER + + --archiver.blobSinkMapSizeKb <value> ($BLOB_SINK_MAP_SIZE_KB) + The maximum possible size of the blob sink DB in KB. Overwrites the general dataStoreMapSizeKb. + + --archiver.blobAllowEmptySources <value> ($BLOB_ALLOW_EMPTY_SOURCES) + Whether to allow having no blob sources configured during startup + + --archiver.blobFileStoreUrls <value> ($BLOB_FILE_STORE_URLS) + URLs for filestore blob archive, comma-separated. Tried in order until blobs are found. + + --archiver.blobFileStoreUploadUrl <value> ($BLOB_FILE_STORE_UPLOAD_URL) + URL for uploading blobs to filestore (s3://, gs://, file://) + + --archiver.blobHealthcheckUploadIntervalMinutes <value> ($BLOB_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES) + Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour) + + --archiver.blobPreferFilestores <value> ($BLOB_PREFER_FILESTORES) + Whether to prefer filestores over consensus clients when fetching blobs. Default: false. + + --archiver.blobFileStoreTimeoutMs <value> ($BLOB_FILE_STORE_TIMEOUT_MS) + Timeout in ms for HTTP requests to the blob file store. Default: 10000 (10s). + + --archiver.archiveApiUrl <value> ($BLOB_ARCHIVE_API_URL) + The URL of the archive API + + --archiver.archiverPollingIntervalMS <value> (default: 500) ($ARCHIVER_POLLING_INTERVAL_MS) + The polling interval in ms for retrieving new L2 blocks and encrypted logs. + + --archiver.archiverBatchSize <value> (default: 100) ($ARCHIVER_BATCH_SIZE) + The number of L2 blocks the archiver will attempt to download at a time. + + --archiver.archiverStoreMapSizeKb <value> ($ARCHIVER_STORE_MAP_SIZE_KB) + The maximum possible size of the archiver DB in KB. Overwrites the general dataStoreMapSizeKb. + + --archiver.blockDurationMs <value> ($SEQ_BLOCK_DURATION_MS) + Duration per block in milliseconds when building multiple blocks per slot. Used to derive orphan proposed block pruning timing. + + --archiver.checkpointProposalSyncGraceSeconds <value> ($CHECKPOINT_PROPOSAL_SYNC_GRACE_SECONDS) + Consensus grace in seconds for a received checkpoint proposal to materialize into local proposed state. + + --archiver.skipValidateCheckpointAttestations <value> + Skip validating checkpoint attestations (for testing purposes only) + + --archiver.skipPromoteProposedCheckpointDuringL1Sync <value> + Skip promoting proposed checkpoints during L1 sync (for testing purposes only) + + --archiver.maxAllowedEthClientDriftSeconds <value> (default: 300) ($MAX_ALLOWED_ETH_CLIENT_DRIFT_SECONDS) + Maximum allowed drift in seconds between the Ethereum client and current time. + + --archiver.ethereumAllowNoDebugHosts <value> ($ETHEREUM_ALLOW_NO_DEBUG_HOSTS) + Whether to allow starting the archiver without debug/trace method support on Ethereum hosts + + --archiver.archiverSkipHistoricalLogsCheck <value> ($ARCHIVER_SKIP_HISTORICAL_LOGS_CHECK) + Skip the startup check that probes the L1 RPC for historical Rollup contract logs. Set to true to bypass the check when the connected RPC node is known to prune old logs. + + --archiver.orphanPruneNoProposalTolerance <value> (default: 1) ($ARCHIVER_ORPHAN_PRUNE_NO_PROPOSAL_TOLERANCE) + Local tolerance in seconds before pruning an orphan block when no checkpoint proposal was received. + + --archiver.skipOrphanProposedBlockPruning <value> ($ARCHIVER_SKIP_ORPHAN_PROPOSED_BLOCK_PRUNING) + Skip pruning orphan proposed blocks that have no matching proposed checkpoint. + + --archiver.testPreloadStandardContracts <value> ($TEST_PRELOAD_STANDARD_CONTRACTS) + Preload the standard contracts (AuthRegistry, PublicChecks, HandshakeRegistry) into the contract store at block 0. For test environments only, and only safe when genesis seeds the matching registration/deployment nullifiers; otherwise a later onchain publish would collide with the block-0 preload. + + SEQUENCER + + --sequencer + Starts Aztec Sequencer with options + + --sequencer.blockDurationMs <value> (default: 3000) ($SEQ_BLOCK_DURATION_MS) + Duration per block in milliseconds, used to derive how many blocks fit in a slot. + + --sequencer.validatorPrivateKeys <value> (default: [Redacted]) ($VALIDATOR_PRIVATE_KEYS) + List of private keys of the validators participating in attestation duties + + --sequencer.validatorAddresses <value> (default: ) ($VALIDATOR_ADDRESSES) + List of addresses of the validators to use with remote signers + + --sequencer.disableValidator <value> ($VALIDATOR_DISABLED) + Do not run the validator + + --sequencer.disabledValidators <value> (default: ) + Temporarily disable these specific validator addresses + + --sequencer.attestationPollingIntervalMs <value> (default: 200) ($VALIDATOR_ATTESTATIONS_POLLING_INTERVAL_MS) + Interval between polling for new attestations + + --sequencer.alwaysReexecuteBlockProposals <value> (default: true) + Whether to always reexecute block proposals, even for non-validator nodes (useful for monitoring network status). + + --sequencer.skipCheckpointProposalValidation <value> + Skip checkpoint proposal validation, including the live Inbox endpoint check, and always attest. For testing only (default: false) + + --sequencer.skipPushProposedBlocksToArchiver <value> + Skip pushing proposed blocks to archiver (test only) + + --sequencer.attestToEquivocatedProposals <value> + Agree to attest to equivocated checkpoint proposals (for testing purposes only) + + --sequencer.skipProposalSlotValidation <value> + Accept proposal validation regardless of slot timing (for testing only) + + --sequencer.validateMaxL2BlockGas <value> ($VALIDATOR_MAX_L2_BLOCK_GAS) + Maximum L2 block gas for validation. Proposals exceeding this limit are rejected. + + --sequencer.validateMaxDABlockGas <value> ($VALIDATOR_MAX_DA_BLOCK_GAS) + Maximum DA block gas for validation. Proposals exceeding this limit are rejected. + + --sequencer.validateMaxTxsPerBlock <value> ($VALIDATOR_MAX_TX_PER_BLOCK) + Maximum transactions per block for validation. Proposals exceeding this limit are rejected. + + --sequencer.validateMaxTxsPerCheckpoint <value> ($VALIDATOR_MAX_TX_PER_CHECKPOINT) + Maximum transactions per checkpoint for validation. Proposals exceeding this limit are rejected. + + --sequencer.nodeId <value> ($VALIDATOR_HA_NODE_ID) + The unique identifier for this node + + --sequencer.pollingIntervalMs <value> (default: 100) ($VALIDATOR_HA_POLLING_INTERVAL_MS) + The number of ms to wait between polls when a duty is being signed + + --sequencer.peerSigningTimeoutMs <value> (default: 3000) ($VALIDATOR_HA_SIGNING_TIMEOUT_MS) + How long to wait for a peer node's in-progress signing before giving up + + --sequencer.signerCallTimeoutMs <value> (default: 30000) ($VALIDATOR_SIGNER_CALL_TIMEOUT_MS) + Hard upper bound in ms on a single signer call before it is aborted (default 30000) + + --sequencer.maxStuckDutiesAgeMs <value> ($VALIDATOR_HA_MAX_STUCK_DUTIES_AGE_MS) + The maximum age of a stuck duty in ms (defaults to 2x Aztec slot duration) + + --sequencer.cleanupOldDutiesAfterHours <value> (default: 24) ($VALIDATOR_HA_OLD_DUTIES_MAX_AGE_H) + Duty retention in hours from signing start (must be positive; minimum two slots) + + --sequencer.signingProtectionMapSizeKb <value> ($SIGNING_PROTECTION_MAP_SIZE_KB) + Maximum size of the local signing-protection LMDB store in KB. Overwrites the general dataStoreMapSizeKb. + + --sequencer.allowEphemeralSigningProtection <value> ($VALIDATOR_ALLOW_EPHEMERAL_SIGNING_PROTECTION) + Allow local signing protection to run against an ephemeral store when no data directory is set (dev/test only). Ephemeral protection does not survive restarts, so this is unsafe for production validators. + + --sequencer.haSigningEnabled <value> ($VALIDATOR_HA_SIGNING_ENABLED) + Whether HA signing / slashing protection is enabled + + --sequencer.databaseUrl <value> ($VALIDATOR_HA_DATABASE_URL) + PostgreSQL connection string for validator HA signer (format: postgresql://user:password@host:port/database) + + --sequencer.poolMaxCount <value> (default: 10) ($VALIDATOR_HA_POOL_MAX) + Maximum number of clients in the pool + + --sequencer.poolMinCount <value> ($VALIDATOR_HA_POOL_MIN) + Minimum number of clients in the pool + + --sequencer.poolIdleTimeoutMs <value> (default: 10000) ($VALIDATOR_HA_POOL_IDLE_TIMEOUT_MS) + Idle timeout in milliseconds + + --sequencer.poolConnectionTimeoutMs <value> ($VALIDATOR_HA_POOL_CONNECTION_TIMEOUT_MS) + Connection timeout in milliseconds (0 means no timeout) + + --sequencer.sequencerPollingIntervalMS <value> (default: 500) ($SEQ_POLLING_INTERVAL_MS) + The number of ms to wait between polling for checking to build on the next slot. + + --sequencer.maxTxsPerCheckpoint <value> ($SEQ_MAX_TX_PER_CHECKPOINT) + The maximum number of txs across all blocks in a checkpoint. + + --sequencer.minTxsPerBlock <value> (default: 1) ($SEQ_MIN_TX_PER_BLOCK) + The minimum number of txs to include in a block. + + --sequencer.minValidTxsPerBlock <value> + The minimum number of valid txs (after execution) to include in a block. If not set, falls back to minTxsPerBlock. + + --sequencer.publishTxsWithProposals <value> ($SEQ_PUBLISH_TXS_WITH_PROPOSALS) + Whether to publish txs with proposals. + + --sequencer.maxL2BlockGas <value> ($SEQ_MAX_L2_BLOCK_GAS) + The maximum L2 block gas. + + --sequencer.maxDABlockGas <value> ($SEQ_MAX_DA_BLOCK_GAS) + The maximum DA block gas. + + --sequencer.perBlockAllocationMultiplier <value> (default: 1.2) ($SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER) + Per-block gas budget multiplier for both L2 and DA gas. Budget per block is (checkpointLimit / maxBlocks) * multiplier. Values greater than one allow early blocks to use more than their even share, relying on checkpoint-level capping for later blocks. + + --sequencer.perBlockDAAllocationMultiplier <value> (default: 1.5) ($SEQ_PER_BLOCK_DA_ALLOCATION_MULTIPLIER) + Per-block budget multiplier applied to DA gas and blob fields in place of perBlockAllocationMultiplier. Defaults higher than the general multiplier so the largest contract class deploy fits a single block. + + --sequencer.redistributeCheckpointBudget <value> (default: true) ($SEQ_REDISTRIBUTE_CHECKPOINT_BUDGET) + Redistribute remaining checkpoint budget evenly across remaining blocks instead of allowing a single block to consume the entire remaining budget. + + --sequencer.coinbase <value> ($COINBASE) + Recipient of block reward. + + --sequencer.feeRecipient <value> ($FEE_RECIPIENT) + Address to receive fees. + + --sequencer.noirExecuteWorkingDirectory <value> ($NOIR_EXECUTE_WORKING_DIRECTORY) + The working directory to use for simulation/proving + + --sequencer.noirExecuteBinaryPath <value> ($NOIR_EXECUTE_BINARY_PATH) + The path to the noir-execute binary + + --sequencer.governanceProposerPayload <value> ($GOVERNANCE_PROPOSER_PAYLOAD_ADDRESS) + The address of the payload for the governanceProposer + + --sequencer.governanceProposerForcePayloadVote <value> ($GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE) + Keep signalling the configured governance payload even if a proposal referencing it was already executed. + + --sequencer.l1PublishingTime <value> (default: 12) ($SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT) + How much time in seconds to allow in the slot for publishing the L1 transaction. + + --sequencer.secondsBeforeInvalidatingBlockAsCommitteeMember <value> (default: 144) ($SEQ_SECONDS_BEFORE_INVALIDATING_BLOCK_AS_COMMITTEE_MEMBER) + How many seconds to wait before trying to invalidate a block from the pending chain as a committee member (zero to never invalidate). The next proposer is expected to invalidate, so the committee acts as a fallback. + + --sequencer.secondsBeforeInvalidatingBlockAsNonCommitteeMember <value> (default: 432) ($SEQ_SECONDS_BEFORE_INVALIDATING_BLOCK_AS_NON_COMMITTEE_MEMBER) + How many seconds to wait before trying to invalidate a block from the pending chain as a non-committee member (zero to never invalidate). The next proposer is expected to invalidate, then the committee, so other sequencers act as a fallback. + + --sequencer.skipWaitForValidParentCheckpointOnL1 <value> + Bypass the parent checkpoint validity check before submitting a pipelined checkpoint, allowing the proposer to publish even when the parent landed on L1 with invalid attestations (for testing only) + + --sequencer.broadcastInvalidBlockProposal <value> + Broadcast invalid block proposals with corrupted state (for testing only) + + --sequencer.invalidBlockProposalIndexWithinCheckpoint <value> + Broadcast an invalid block proposal only at this indexWithinCheckpoint (for testing only) + + --sequencer.broadcastInvalidCheckpointProposalOnly <value> + Broadcast invalid checkpoint proposals while keeping the underlying block proposals valid (for testing only). When unset, the checkpoint follows broadcastInvalidBlockProposal. + + --sequencer.injectFakeAttestation <value> + Inject a fake attestation (for testing only) + + --sequencer.injectHighSValueAttestation <value> + Inject a malleable attestation with a high-s value (for testing only) + + --sequencer.injectUnrecoverableSignatureAttestation <value> + Inject an attestation with an unrecoverable signature (for testing only) + + --sequencer.injectYParityAttestation <value> + Inject a non-proposer attestation slot in yParity form in the packed L1 tuple (for testing only) + + --sequencer.republishAfterFakeAttestation <value> + Publish the checkpoint with a fake attestation first, then invalidate it and publish it again with the collected attestations within the same slot (for testing only) + + --sequencer.shuffleAttestationOrdering <value> + Shuffle attestation ordering to create invalid ordering (for testing only) + + --sequencer.expectedBlockProposalsPerSlot <value> ($SEQ_EXPECTED_BLOCK_PROPOSALS_PER_SLOT) + Expected number of block proposals per slot for P2P peer scoring. 0 (default) disables block proposal scoring. Set to a positive value to enable. + + --sequencer.checkpointProposalSyncGraceSeconds <value> (default: 6) ($CHECKPOINT_PROPOSAL_SYNC_GRACE_SECONDS) + Consensus grace in seconds for a received checkpoint proposal to materialize into local proposed state. Defaults to twice the block duration. + + --sequencer.maxTxsPerBlock <value> ($SEQ_MAX_TX_PER_BLOCK) + The maximum number of txs to include in a block. + + --sequencer.attestationPropagationTime <value> (default: 2) ($SEQ_ATTESTATION_PROPAGATION_TIME) + How many seconds it takes for proposals and attestations to travel across the p2p layer (one-way). + + --sequencer.checkpointProposalPrepareTime <value> (default: 1) ($SEQ_CHECKPOINT_PROPOSAL_PREPARE_TIME) + Local time in seconds between the last block build finishing and the checkpoint proposal being ready for p2p send. + + --sequencer.minBlockDuration <value> (default: 2) ($SEQ_MIN_BLOCK_DURATION) + Minimum block-building time in seconds still worth allocating if the proposer starts late. + + --sequencer.maxBlocksPerCheckpoint <value> (default: 24) ($MAX_BLOCKS_PER_CHECKPOINT) + Maximum number of blocks the sequencer packs into a single checkpoint, and the maximum indexWithinCheckpoint accepted on inbound block proposals. + + --sequencer.buildCheckpointIfEmpty <value> ($SEQ_BUILD_CHECKPOINT_IF_EMPTY) + Have sequencer build and publish an empty checkpoint if there are no txs + + --sequencer.minBlocksForCheckpoint <value> + Minimum number of blocks required for a checkpoint proposal (test only) + + --sequencer.skipPublishingCheckpointsPercent <value> ($SEQ_SKIP_CHECKPOINT_PUBLISH_PERCENT) + Percent probability (0 - 100) of sequencer skipping checkpoint publishing (testing only) + + --sequencer.skipBroadcastProposals <value> + Skip broadcasting checkpoint and block proposals via gossipsub when proposer (for testing only) + + --sequencer.skipBroadcastCheckpointProposal <value> + Skip broadcasting only the CheckpointProposal via gossipsub when proposer; the held last block is broadcast standalone instead so peers still receive it as a proposed-but-uncheckpointed tip (for testing only) + + --sequencer.pauseProposingForSlots <value> + List of slots for which the sequencer will not produce a proposal (for testing only). Attestation paths are unaffected. + + --sequencer.minPeersToPropose <value> (default: 1) ($SEQ_MIN_PEERS_TO_PROPOSE) + Minimum number of connected p2p peers required to build and propose a checkpoint (zero to disable the check). Ignored when p2p is disabled by config. + + --sequencer.allowUnsafeInboxCatchupCapacity <value> + Allow starting with fewer block opportunities per checkpoint than a mandatory streaming-Inbox backlog needs (for testing only). + + --sequencer.txPublicSetupAllowListExtend <value> ($TX_PUBLIC_SETUP_ALLOWLIST) + Additional entries to extend the default setup allow list. Format: I:address:selector[:flags],C:classId:selector[:flags]. Flags: os (onlySelf), rn (rejectNullMsgSender), cl=N (calldataLength), joined with +. + + --sequencer.keyStoreDirectory <value> ($KEY_STORE_DIRECTORY) + Location of key store directory + + --sequencer.sequencerPublisherPrivateKeys <value> (default: ) ($SEQ_PUBLISHER_PRIVATE_KEYS) + The private keys to be used by the sequencer publisher. + + --sequencer.sequencerPublisherAddresses <value> (default: ) ($SEQ_PUBLISHER_ADDRESSES) + The addresses of the publishers to use with remote signers + + --sequencer.blobAllowEmptySources <value> ($BLOB_ALLOW_EMPTY_SOURCES) + Whether to allow having no blob sources configured during startup + + --sequencer.blobFileStoreUrls <value> ($BLOB_FILE_STORE_URLS) + URLs for filestore blob archive, comma-separated. Tried in order until blobs are found. + + --sequencer.blobFileStoreUploadUrl <value> ($BLOB_FILE_STORE_UPLOAD_URL) + URL for uploading blobs to filestore (s3://, gs://, file://) + + --sequencer.blobHealthcheckUploadIntervalMinutes <value> ($BLOB_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES) + Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour) + + --sequencer.blobPreferFilestores <value> ($BLOB_PREFER_FILESTORES) + Whether to prefer filestores over consensus clients when fetching blobs. Default: false. + + --sequencer.blobFileStoreTimeoutMs <value> ($BLOB_FILE_STORE_TIMEOUT_MS) + Timeout in ms for HTTP requests to the blob file store. Default: 10000 (10s). + + --sequencer.archiveApiUrl <value> ($BLOB_ARCHIVE_API_URL) + The URL of the archive API + + --sequencer.sequencerPublisherAllowInvalidStates <value> (default: true) ($SEQ_PUBLISHER_ALLOW_INVALID_STATES) + True to use publishers in invalid states (timed out, cancelled, etc) if no other is available + + --sequencer.sequencerPublisherForwarderAddress <value> ($SEQ_PUBLISHER_FORWARDER_ADDRESS) + Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only) + + --sequencer.sequencerPublisherPreviousL1BlockWaitTimeoutMs <value> (default: 8000) ($SEQ_PUBLISHER_PREVIOUS_L1_BLOCK_WAIT_TIMEOUT_MS) + How long to wait for the previous L1 block before sending scheduled publisher txs anyway, in milliseconds. + + --sequencer.sequencerPublisherPreviousL1BlockWaitPollIntervalMs <value> (default: 500) ($SEQ_PUBLISHER_PREVIOUS_L1_BLOCK_WAIT_POLL_INTERVAL_MS) + Poll interval while waiting for the previous L1 block before scheduled publisher txs, in milliseconds. + + --sequencer.l1TxFailedStore <value> ($L1_TX_FAILED_STORE) + Store for failed L1 transaction inputs (test networks only). Format: gs://bucket/path + + --sequencer.publisherFundingThreshold <value> ($PUBLISHER_FUNDING_THRESHOLD) + Min ETH balance below which a publisher gets funded. Specified in ether (e.g. 0.1). Unset = funding disabled. + + --sequencer.publisherFundingAmount <value> ($PUBLISHER_FUNDING_AMOUNT) + Amount of ETH to send when funding a publisher. Specified in ether (e.g. 0.5). Unset = funding disabled. + + --sequencer.publisherLowBalanceWarningMultiplier <value> (default: 4) ($PUBLISHER_LOW_BALANCE_WARNING_MULTIPLIER) + Warn when a publisher balance is below this multiple of the ETH required for a reference L1 send (a first-of-epoch proposal for sequencers, an epoch proof for provers) at current fees. Publishers that cannot afford a send are never used for it. + + PROVER NODE + + --prover-node + Starts Aztec Prover Node with options + + --proverNode.keyStoreDirectory <value> ($KEY_STORE_DIRECTORY) + Location of key store directory + + --proverNode.noirExecuteWorkingDirectory <value> ($NOIR_EXECUTE_WORKING_DIRECTORY) + The working directory to use for simulation/proving + + --proverNode.noirExecuteBinaryPath <value> ($NOIR_EXECUTE_BINARY_PATH) + The path to the noir-execute binary + + --proverNode.bbWorkingDirectory <value> ($BB_WORKING_DIRECTORY) + The working directory to use for proving + + --proverNode.bbBinaryPath <value> ($BB_BINARY_PATH) + The path to the bb binary + + --proverNode.bbSkipCleanup <value> ($BB_SKIP_CLEANUP) + Whether to skip cleanup of bb temporary files + + --proverNode.numConcurrentIVCVerifiers <value> (default: 8) ($BB_NUM_IVC_VERIFIERS) + Max concurrent verifications for the RPC verifier (QueuedIVCVerifier). + + --proverNode.bbIVCConcurrency <value> (default: 1) ($BB_IVC_CONCURRENCY) + Thread count for the RPC IVC verifier. + + --proverNode.bbChonkVerifyMaxBatch <value> (default: 16) ($BB_CHONK_VERIFY_MAX_BATCH) + Upper bound on proofs per batch for the peer chonk batch verifier. Proofs are verified immediately as they arrive; this only caps how many can accumulate while a batch is already being processed. + + --proverNode.bbChonkVerifyConcurrency <value> (default: 6) ($BB_CHONK_VERIFY_BATCH_CONCURRENCY) + Thread count for the peer batch verifier parallel reduce. 0 = auto. + + --proverNode.bbDebugOutputDir <value> ($BB_DEBUG_OUTPUT_DIR) + When set, bb.js operations write input/output files and log equivalent CLI commands to this directory + + --proverNode.nodeUrl <value> ($AZTEC_NODE_URL) + The URL to the Aztec node to take proving jobs from + + --proverNode.proverId <value> ($PROVER_ID) + Hex value that identifies the prover. Defaults to the address used for submitting proofs if not set. + + --proverNode.failedProofStore <value> ($PROVER_FAILED_PROOF_STORE) + Store for failed proof inputs. Google cloud storage is only supported at the moment. Set this value as gs://bucket-name/path/to/store. + + --proverNode.enqueueConcurrency <value> (default: 50) ($PROVER_ENQUEUE_CONCURRENCY) + Max concurrent jobs the orchestrator serializes and enqueues to the broker. + + --proverNode.blobSinkMapSizeKb <value> ($BLOB_SINK_MAP_SIZE_KB) + The maximum possible size of the blob sink DB in KB. Overwrites the general dataStoreMapSizeKb. + + --proverNode.blobAllowEmptySources <value> ($BLOB_ALLOW_EMPTY_SOURCES) + Whether to allow having no blob sources configured during startup + + --proverNode.blobFileStoreUrls <value> ($BLOB_FILE_STORE_URLS) + URLs for filestore blob archive, comma-separated. Tried in order until blobs are found. + + --proverNode.blobFileStoreUploadUrl <value> ($BLOB_FILE_STORE_UPLOAD_URL) + URL for uploading blobs to filestore (s3://, gs://, file://) + + --proverNode.blobHealthcheckUploadIntervalMinutes <value> ($BLOB_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES) + Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour) + + --proverNode.blobPreferFilestores <value> ($BLOB_PREFER_FILESTORES) + Whether to prefer filestores over consensus clients when fetching blobs. Default: false. + + --proverNode.blobFileStoreTimeoutMs <value> ($BLOB_FILE_STORE_TIMEOUT_MS) + Timeout in ms for HTTP requests to the blob file store. Default: 10000 (10s). + + --proverNode.archiveApiUrl <value> ($BLOB_ARCHIVE_API_URL) + The URL of the archive API + + --proverNode.proverPublisherAllowInvalidStates <value> (default: true) ($PROVER_PUBLISHER_ALLOW_INVALID_STATES) + True to use publishers in invalid states (timed out, cancelled, etc) if no other is available + + --proverNode.publisherFundingThreshold <value> ($PUBLISHER_FUNDING_THRESHOLD) + Min ETH balance below which a publisher gets funded. Specified in ether (e.g. 0.1). Unset = funding disabled. + + --proverNode.publisherFundingAmount <value> ($PUBLISHER_FUNDING_AMOUNT) + Amount of ETH to send when funding a publisher. Specified in ether (e.g. 0.5). Unset = funding disabled. + + --proverNode.publisherLowBalanceWarningMultiplier <value> (default: 4) ($PUBLISHER_LOW_BALANCE_WARNING_MULTIPLIER) + Warn when a publisher balance is below this multiple of the ETH required for a reference L1 send (a first-of-epoch proposal for sequencers, an epoch proof for provers) at current fees. Publishers that cannot afford a send are never used for it. + + --proverNode.proverPublisherPrivateKeys <value> (default: ) ($PROVER_PUBLISHER_PRIVATE_KEYS) + The private keys to be used by the prover publisher. + + --proverNode.proverPublisherAddresses <value> (default: ) ($PROVER_PUBLISHER_ADDRESSES) + The addresses of the publishers to use with remote signers + + --proverNode.proverNodeMaxPendingJobs <value> (default: 10) ($PROVER_NODE_MAX_PENDING_JOBS) + The maximum number of pending jobs for the prover node + + --proverNode.proverNodePollingIntervalMs <value> (default: 1000) ($PROVER_NODE_POLLING_INTERVAL_MS) + The interval in milliseconds to poll for new jobs + + --proverNode.proverNodeMaxParallelBlocksPerEpoch <value> ($PROVER_NODE_MAX_PARALLEL_BLOCKS_PER_EPOCH) + The Maximum number of blocks to process in parallel while proving an epoch + + --proverNode.proverNodeFailedEpochStore <value> ($PROVER_NODE_FAILED_EPOCH_STORE) + File store where to upload node state when an epoch fails to be proven + + --proverNode.proverNodeEpochProvingDelayMs <value> + Optional delay in milliseconds to wait for late-arriving events (e.g. reorgs) to settle before starting top-tree proving for an epoch + + --proverNode.txGatheringIntervalMs <value> (default: 1000) ($PROVER_NODE_TX_GATHERING_INTERVAL_MS) + How often to check that tx data is available + + --proverNode.txGatheringBatchSize <value> (default: 10) ($PROVER_NODE_TX_GATHERING_BATCH_SIZE) + How many transactions to gather from a node in a single request + + --proverNode.txGatheringMaxParallelRequestsPerNode <value> (default: 100) ($PROVER_NODE_TX_GATHERING_MAX_PARALLEL_REQUESTS_PER_NODE) + How many tx requests to make in parallel to each node + + --proverNode.txGatheringTimeoutMs <value> (default: 120000) ($PROVER_NODE_TX_GATHERING_TIMEOUT_MS) + How long to wait for tx data to be available before giving up + + --proverNode.proverNodeDisableProofPublish <value> ($PROVER_NODE_DISABLE_PROOF_PUBLISH) + Whether the prover node skips publishing proofs to L1 + + --proverNode.proverNodeDisableFullEpochProving <value> ($PROVER_NODE_DISABLE_FULL_EPOCH_PROVING) + Whether the prover node skips proving complete epochs on its own. It still proves partial epochs requested through the startProof admin API, e.g. by a sidecar that pays for early withdrawal proofs. + + --proverNode.proofSubmissionTargetAddress <value> ($PROVER_NODE_PROOF_SUBMISSION_TARGET_ADDRESS) + Optional L1 address the submitEpochRootProof tx is sent to. Must expose the identical submitEpochRootProof ABI and forward to the rollup. Defaults to the rollup address. + + --proverNode.web3SignerUrl <value> ($WEB3_SIGNER_URL) + URL of the Web3Signer instance + + PROVER BROKER + + --prover-broker + Starts Aztec proving job broker + + --proverBroker.proverBrokerJobTimeoutMs <value> (default: 30000) ($PROVER_BROKER_JOB_TIMEOUT_MS) + Jobs are retried if not kept alive for this long + + --proverBroker.proverBrokerPollIntervalMs <value> (default: 1000) ($PROVER_BROKER_POLL_INTERVAL_MS) + The interval to check job health status + + --proverBroker.proverBrokerJobMaxRetries <value> (default: 3) ($PROVER_BROKER_JOB_MAX_RETRIES) + If starting a prover broker locally, the max number of retries per proving job + + --proverBroker.proverBrokerBatchSize <value> (default: 100) ($PROVER_BROKER_BATCH_SIZE) + The prover broker writes jobs to disk in batches + + --proverBroker.proverBrokerBatchIntervalMs <value> (default: 50) ($PROVER_BROKER_BATCH_INTERVAL_MS) + How often to flush batches to disk + + --proverBroker.proverBrokerMaxEpochsToKeepResultsFor <value> (default: 1) ($PROVER_BROKER_MAX_EPOCHS_TO_KEEP_RESULTS_FOR) + The maximum number of epochs to keep results for + + --proverBroker.proverBrokerStoreMapSizeKb <value> ($PROVER_BROKER_STORE_MAP_SIZE_KB) + The size of the prover broker's database. Will override the dataStoreMapSizeKb if set. + + --proverBroker.proverBrokerDebugReplayEnabled <value> ($PROVER_BROKER_DEBUG_REPLAY_ENABLED) + Enable debug replay mode for replaying proving jobs from stored inputs + + PROVER AGENT + + --prover-agent + Starts Aztec Prover Agent with options + + --proverAgent.proverAgentCount <value> (default: 1) ($PROVER_AGENT_COUNT) + Whether this prover has a local prover agent + + --proverAgent.proverAgentPollIntervalMs <value> (default: 1000) ($PROVER_AGENT_POLL_INTERVAL_MS) + The interval agents poll for jobs at + + --proverAgent.proverAgentProofTypes <value> ($PROVER_AGENT_PROOF_TYPES) + The types of proofs the prover agent can generate + + --proverAgent.proverBrokerUrl <value> ($PROVER_BROKER_HOST) + The URL where this agent takes jobs from + + --proverAgent.realProofs <value> (default: true) ($PROVER_REAL_PROOFS) + Whether to construct real proofs + + --proverAgent.proverTestDelayType <value> (default: fixed) ($PROVER_TEST_DELAY_TYPE) + The type of artificial delay to introduce + + --proverAgent.proverTestDelayMs <value> ($PROVER_TEST_DELAY_MS) + Artificial delay to introduce to all operations to the test prover. + + --proverAgent.proverTestDelayFactor <value> (default: 1) ($PROVER_TEST_DELAY_FACTOR) + If using realistic delays, what percentage of realistic times to apply. + + --proverAgent.proverTestVerificationDelayMs <value> (default: 10) ($PROVER_TEST_VERIFICATION_DELAY_MS) + The delay (ms) to inject during fake proof verification + + --proverAgent.cancelJobsOnStop <value> ($PROVER_CANCEL_JOBS_ON_STOP) + Whether to abort pending proving jobs when the orchestrator is cancelled. When false (default), jobs remain in the broker queue and can be reused on restart/reorg. + + --proverAgent.proofStore <value> ($PROVER_PROOF_STORE) + Optional proof input store for the prover + + P2P SUBSYSTEM + + --p2p-enabled [value] ($P2P_ENABLED) + Enable P2P subsystem + + --p2p.validateMaxTxsPerBlock <value> ($VALIDATOR_MAX_TX_PER_BLOCK) + Maximum transactions per block for validation. Overrides maxTxsPerBlock for gossip validation when set. + + --p2p.validateMaxTxsPerCheckpoint <value> ($VALIDATOR_MAX_TX_PER_CHECKPOINT) + Maximum transactions per checkpoint for validation. Used as fallback for maxTxsPerBlock when that is not set. + + --p2p.p2pDiscoveryDisabled <value> ($P2P_DISCOVERY_DISABLED) + A flag dictating whether the P2P discovery system should be disabled. + + --p2p.blockCheckIntervalMS <value> (default: 100) ($P2P_BLOCK_CHECK_INTERVAL_MS) + The frequency in which to check for new L2 blocks. + + --p2p.debugDisableColocationPenalty <value> ($DEBUG_P2P_DISABLE_COLOCATION_PENALTY) + DEBUG: Disable colocation penalty - NEVER set to true in production + + --p2p.peerCheckIntervalMS <value> (default: 30000) ($P2P_PEER_CHECK_INTERVAL_MS) + The frequency in which to check for new peers. + + --p2p.peerFailedBanTimeMs <value> (default: 300000) ($P2P_PEER_FAILED_BAN_TIME_MS) + How long to ban a peer after it fails maximum dial attempts. + + --p2p.l2QueueSize <value> (default: 1000) ($P2P_L2_QUEUE_SIZE) + Size of queue of L2 blocks to store. + + --p2p.listenAddress <value> (default: 0.0.0.0) ($P2P_LISTEN_ADDR) + The listen address. ipv4 address. + + --p2p.p2pPort <value> (default: 40400) ($P2P_PORT) + The port for the P2P service. Defaults to 40400 + + --p2p.p2pBroadcastPort <value> ($P2P_BROADCAST_PORT) + The port to broadcast the P2P service on (included in the node's ENR). Defaults to P2P_PORT. + + --p2p.p2pIp <value> ($P2P_IP) + The IP address for the P2P service. ipv4 address. + + --p2p.peerIdPrivateKey <value> ($PEER_ID_PRIVATE_KEY) + An optional peer id private key. If blank, will generate a random key. + + --p2p.peerIdPrivateKeyPath <value> ($PEER_ID_PRIVATE_KEY_PATH) + An optional path to store generated peer id private keys. If blank, will default to storing any generated keys in the root of the data directory. + + --p2p.bootstrapNodes <value> (default: ) ($BOOTSTRAP_NODES) + A list of bootstrap peer ENRs to connect to. Separated by commas. + + --p2p.bootstrapNodeEnrVersionCheck <value> ($P2P_BOOTSTRAP_NODE_ENR_VERSION_CHECK) + Whether to check the version of the bootstrap node ENR. + + --p2p.bootstrapNodesAsFullPeers <value> ($P2P_BOOTSTRAP_NODES_AS_FULL_PEERS) + Whether to consider our configured bootnodes as full peers + + --p2p.maxPeerCount <value> (default: 100) ($P2P_MAX_PEERS) + The maximum number of peers to connect to. + + --p2p.p2pHealthMinPeers <value> ($P2P_HEALTH_MIN_PEERS) + Minimum number of connected peers for p2p to be reported as healthy on the status endpoint. Zero never fails the check. + + --p2p.queryForIp <value> ($P2P_QUERY_FOR_IP) + If announceUdpAddress or announceTcpAddress are not provided, query for the IP address of the machine. Default is false. + + --p2p.publicIpServices <value> (default: https://api.ipify.org/,https://checkip.amazonaws.com/,https://ifconfig.me/ip,https://icanhazip.com/)($P2P_PUBLIC_IP_SERVICES) + Comma-separated HTTPS URLs that return plain-text public IPv4. Used when P2P_QUERY_FOR_IP is true and P2P_IP is unset. Tried in order until one succeeds. + + --p2p.gossipsubInterval <value> (default: 700) ($P2P_GOSSIPSUB_INTERVAL_MS) + The interval of the gossipsub heartbeat to perform maintenance tasks. + + --p2p.gossipsubD <value> (default: 8) ($P2P_GOSSIPSUB_D) + The D parameter for the gossipsub protocol. + + --p2p.gossipsubDlo <value> (default: 4) ($P2P_GOSSIPSUB_DLO) + The Dlo parameter for the gossipsub protocol. + + --p2p.gossipsubDhi <value> (default: 12) ($P2P_GOSSIPSUB_DHI) + The Dhi parameter for the gossipsub protocol. + + --p2p.gossipsubDLazy <value> (default: 8) ($P2P_GOSSIPSUB_DLAZY) + The Dlazy parameter for the gossipsub protocol. + + --p2p.gossipsubFloodPublish <value> ($P2P_GOSSIPSUB_FLOOD_PUBLISH) + Whether to flood publish messages. - For testing purposes only + + --p2p.gossipsubMcacheLength <value> (default: 12) ($P2P_GOSSIPSUB_MCACHE_LENGTH) + The number of gossipsub interval message cache windows to keep. + + --p2p.gossipsubMcacheGossip <value> (default: 3) ($P2P_GOSSIPSUB_MCACHE_GOSSIP) + How many message cache windows to include when gossiping with other peers. + + --p2p.gossipsubSeenTTL <value> (default: 1200000) ($P2P_GOSSIPSUB_SEEN_TTL) + How long to keep message IDs in the seen cache. + + --p2p.maxGossipClockDisparityMs <value> (default: 500) ($P2P_MAX_GOSSIP_CLOCK_DISPARITY_MS) + Maximum clock-disparity tolerance (ms) applied to both ends of proposal/attestation gossip receive windows. + + --p2p.gossipsubTxTopicWeight <value> (default: 1) ($P2P_GOSSIPSUB_TX_TOPIC_WEIGHT) + The weight of the tx topic for the gossipsub protocol. + + --p2p.gossipsubTxInvalidMessageDeliveriesWeight <value> (default: -20) ($P2P_GOSSIPSUB_TX_INVALID_MESSAGE_DELIVERIES_WEIGHT) + The weight of the tx invalid message deliveries for the gossipsub protocol. + + --p2p.gossipsubTxInvalidMessageDeliveriesDecay <value> (default: 0.5) ($P2P_GOSSIPSUB_TX_INVALID_MESSAGE_DELIVERIES_DECAY) + Determines how quickly the penalty for invalid message deliveries decays over time. Between 0 and 1. + + --p2p.peerPenaltyValues <value> (default: 2,10,50) ($P2P_PEER_PENALTY_VALUES) + The values for the peer scoring system. Passed as a comma separated list of values in order: low, mid, high tolerance errors. + + --p2p.peerBanDurationSeconds <value> (default: 86400) ($P2P_PEER_BAN_DURATION_SECONDS) + How long (in seconds) a peer is banned for once its score drops below the ban threshold. + + --p2p.doubleSpendSeverePeerPenaltyWindow <value> (default: 30) ($P2P_DOUBLE_SPEND_SEVERE_PEER_PENALTY_WINDOW) + The "age" (in L2 blocks) of a tx after which we heavily penalize a peer for sending it. + + --p2p.blockRequestBatchSize <value> (default: 20) ($P2P_BLOCK_REQUEST_BATCH_SIZE) + The number of blocks to fetch in a single batch. + + --p2p.archivedTxLimit <value> ($P2P_ARCHIVED_TX_LIMIT) + The number of transactions that will be archived. If the limit is set to 0 then archiving will be disabled. + + --p2p.trustedPeers <value> (default: ) ($P2P_TRUSTED_PEERS) + A list of trusted peer ENRs that will always be persisted. Separated by commas. + + --p2p.privatePeers <value> (default: ) ($P2P_PRIVATE_PEERS) + A list of private peer ENRs that will always be persisted and not be used for discovery. Separated by commas. + + --p2p.preferredPeers <value> (default: ) ($P2P_PREFERRED_PEERS) + A list of preferred peer ENRs that will always be persisted and not be used for discovery. Separated by commas. + + --p2p.p2pStoreMapSizeKb <value> ($P2P_STORE_MAP_SIZE_KB) + The maximum possible size of the P2P DB in KB. Overwrites the general dataStoreMapSizeKb. + + --p2p.txPublicSetupAllowListExtend <value> ($TX_PUBLIC_SETUP_ALLOWLIST) + Additional entries to extend the default setup allow list. Format: I:address:selector[:flags],C:classId:selector[:flags]. Flags: os (onlySelf), rn (rejectNullMsgSender), cl=N (calldataLength), joined with +. + + --p2p.maxPendingTxCount <value> (default: 1000) ($P2P_MAX_PENDING_TX_COUNT) + The maximum number of pending txs before evicting lower priority txs. + + --p2p.seenMessageCacheSize <value> (default: 100000) ($P2P_SEEN_MSG_CACHE_SIZE) + The number of messages to keep in the seen message cache + + --p2p.txValidationCacheSize <value> (default: 5000) ($P2P_TX_VALIDATION_CACHE_SIZE) + Maximum number of items to keep in the tx validation LRU cache. + + --p2p.p2pDisableStatusHandshake <value> ($P2P_DISABLE_STATUS_HANDSHAKE) + True to disable the status handshake on peer connected. + + --p2p.p2pAllowOnlyValidators <value> ($P2P_ALLOW_ONLY_VALIDATORS) + True to only permit validators to connect. + + --p2p.p2pMaxFailedAuthAttemptsAllowed <value> (default: 3) ($P2P_MAX_AUTH_FAILED_ATTEMPTS_ALLOWED) + Number of auth attempts to allow before peer is banned. Number is inclusive + + --p2p.dropTransactionsProbability <value> ($P2P_DROP_TX_CHANCE) + The probability that a transaction is discarded (0 - 1). - For testing purposes only + + --p2p.disableTransactions <value> ($TRANSACTIONS_DISABLED) + Whether transactions are disabled for this node. This means transactions will be rejected at the RPC and P2P layers. + + --p2p.txPoolDeleteTxsAfterReorg <value> ($P2P_TX_POOL_DELETE_TXS_AFTER_REORG) + Whether to delete transactions from the pool after a reorg instead of moving them back to pending. + + --p2p.debugP2PInstrumentMessages <value> ($DEBUG_P2P_INSTRUMENT_MESSAGES) + Alters the format of p2p messages to include things like broadcast timestamp FOR TESTING ONLY + + --p2p.broadcastEquivocatedProposals <value> + Broadcast block proposals even when a conflicting proposal for the same slot already exists in the pool (for testing purposes only). + + --p2p.skipIncomingProposals <value> + Drop incoming block and checkpoint proposals at the libp2p dispatch layer (for testing only) + + --p2p.skipProposalSlotValidation <value> + Accept proposal gossip regardless of slot timing (for testing only) + + --p2p.skipCheckpointProposalValidation <value> + Skip checkpoint proposal validation, including the live Inbox endpoint check, and always attest, broadcasting the attestation before processing the embedded last block. For testing only + + --p2p.minTxPoolAgeMs <value> (default: 2000) ($P2P_MIN_TX_POOL_AGE_MS) + Minimum age (ms) a transaction must have been in the pool before it is eligible for block building. + + --p2p.slashDataWithholdingToleranceSlots <value> (default: 3) ($SLASH_DATA_WITHHOLDING_TOLERANCE_SLOTS) + L2 slots to wait after a checkpoint slot before declaring its txs missing. Drives both the data-withholding slasher check and the missing-tx collection deadline. + + --p2p.p2pMissingTxCollectionDeadlineSlots <value> ($P2P_MISSING_TX_COLLECTION_DEADLINE_SLOTS) + Optional deadline (in L2 slots after the block slot) for collecting missing txs for unproven mined blocks. Clamped up to the data-withholding tolerance window so collection never gives up before the slash verdict. + + --p2p.priceBumpPercentage <value> (default: 10) ($P2P_RPC_PRICE_BUMP_PERCENTAGE) + Minimum percentage fee increase required to replace an existing tx via RPC. Even at 0%, replacement still requires paying at least 1 unit more. + + --p2p.keepFinalizedTxsForSlots <value> ($P2P_KEEP_FINALIZED_TXS_FOR_SLOTS) + Number of slots behind the finalized tip to keep finalized txs for before deleting them. 0 deletes at the finalized tip. + + --p2p.expectedBlockProposalsPerSlot <value> ($SEQ_EXPECTED_BLOCK_PROPOSALS_PER_SLOT) + Expected number of block proposals per slot for P2P peer scoring. 0 (default) disables block proposal scoring. Set to a positive value to enable. + + --p2p.maxTxsPerBlock <value> ($SEQ_MAX_TX_PER_BLOCK) + The maximum number of txs to include in a block. + + --p2p.checkpointProposalSyncGraceSeconds <value> (default: 6) ($CHECKPOINT_PROPOSAL_SYNC_GRACE_SECONDS) + Consensus grace in seconds for a received checkpoint proposal to materialize into local proposed state. Defaults to twice the block duration. + + --p2p.maxBlocksPerCheckpoint <value> (default: 24) ($MAX_BLOCKS_PER_CHECKPOINT) + Maximum number of blocks the sequencer packs into a single checkpoint, and the maximum indexWithinCheckpoint accepted on inbound block proposals. + + --p2p.blockDurationMs <value> (default: 3000) ($SEQ_BLOCK_DURATION_MS) + Duration per block in milliseconds, used to derive how many blocks fit in a slot. + + --p2p.overallRequestTimeoutMs <value> (default: 10000) ($P2P_REQRESP_OVERALL_REQUEST_TIMEOUT_MS) + The overall timeout for a request response operation. + + --p2p.individualRequestTimeoutMs <value> (default: 10000) ($P2P_REQRESP_INDIVIDUAL_REQUEST_TIMEOUT_MS) + The timeout for an individual request response peer interaction. + + --p2p.dialTimeoutMs <value> (default: 5000) ($P2P_REQRESP_DIAL_TIMEOUT_MS) + How long to wait for the dial protocol to establish a connection + + --p2p.p2pOptimisticNegotiation <value> ($P2P_REQRESP_OPTIMISTIC_NEGOTIATION) + Whether to use optimistic protocol negotiation when dialing to another peer (opposite of `negotiateFully`). + + --p2p.batchTxRequesterSmartParallelWorkerCount <value> (default: 10) ($P2P_BATCH_TX_REQUESTER_SMART_PARALLEL_WORKER_COUNT) + Max concurrent requests to smart peers for batch tx requester. + + --p2p.batchTxRequesterDumbParallelWorkerCount <value> (default: 10) ($P2P_BATCH_TX_REQUESTER_DUMB_PARALLEL_WORKER_COUNT) + Max concurrent requests to dumb peers for batch tx requester. + + --p2p.batchTxRequesterTxBatchSize <value> (default: 8) ($P2P_BATCH_TX_REQUESTER_TX_BATCH_SIZE) + Max transactions per request / chunk size for batch tx requester. + + --p2p.batchTxRequesterBadPeerThreshold <value> (default: 2) ($P2P_BATCH_TX_REQUESTER_BAD_PEER_THRESHOLD) + Failures before a peer is considered bad (see > threshold logic). + + --p2p.txCollectionFastNodesTimeoutBeforeReqRespMs <value> (default: 200) ($TX_COLLECTION_FAST_NODES_TIMEOUT_BEFORE_REQ_RESP_MS) + How long to wait before starting reqresp for fast collection + + --p2p.txCollectionFastNodeIntervalMs <value> (default: 500) ($TX_COLLECTION_FAST_NODE_INTERVAL_MS) + How many ms to wait between retried request to a node via RPC during fast collection + + --p2p.txCollectionNodeRpcUrls <value> (default: ) ($TX_COLLECTION_NODE_RPC_URLS) + A comma-separated list of Aztec node RPC URLs to use for tx collection + + --p2p.txCollectionFastMaxParallelRequestsPerNode <value> (default: 4) ($TX_COLLECTION_FAST_MAX_PARALLEL_REQUESTS_PER_NODE) + Maximum number of parallel requests to make to a node during fast collection + + --p2p.txCollectionNodeRpcMaxBatchSize <value> (default: 50) ($TX_COLLECTION_NODE_RPC_MAX_BATCH_SIZE) + Maximum number of transactions to request from a node in a single batch + + --p2p.txCollectionFileStoreUrls <value> (default: ) ($TX_COLLECTION_FILE_STORE_URLS) + A comma-separated list of file store URLs (s3://, gs://, file://, http://) for tx collection + + --p2p.txCollectionFileStoreFastDelayMs <value> (default: 2000) ($TX_COLLECTION_FILE_STORE_FAST_DELAY_MS) + Delay in ms from reqresp start before file store collection begins + + --p2p.txCollectionFileStoreFastWorkerCount <value> (default: 5) ($TX_COLLECTION_FILE_STORE_FAST_WORKER_COUNT) + Number of concurrent workers for fast file store collection + + --p2p.txCollectionFileStoreFastBackoffBaseMs <value> (default: 1000) ($TX_COLLECTION_FILE_STORE_FAST_BACKOFF_BASE_MS) + Base backoff time in ms for fast file store collection retries + + --p2p.txCollectionFileStoreFastBackoffMaxMs <value> (default: 5000) ($TX_COLLECTION_FILE_STORE_FAST_BACKOFF_MAX_MS) + Max backoff time in ms for fast file store collection retries + + --p2p.txFileStoreUrl <value> ($TX_FILE_STORE_URL) + URL for uploading txs to file storage (s3://, gs://, file://) + + --p2p.txFileStoreUploadConcurrency <value> (default: 10) ($TX_FILE_STORE_UPLOAD_CONCURRENCY) + Maximum number of concurrent tx uploads + + --p2p.txFileStoreMaxQueueSize <value> (default: 1000) ($TX_FILE_STORE_MAX_QUEUE_SIZE) + Maximum queue size for pending uploads (oldest dropped when exceeded) + + --p2p.txFileStoreEnabled <value> ($TX_FILE_STORE_ENABLED) + Enable uploading transactions to file storage + + P2P BOOTSTRAP + + --p2p-bootstrap + Starts Aztec P2P Bootstrap with options + + --p2pBootstrap.p2pBroadcastPort <value> ($P2P_BROADCAST_PORT) + The port to broadcast the P2P service on (included in the node's ENR). Defaults to P2P_PORT. + + --p2pBootstrap.peerIdPrivateKeyPath <value> ($PEER_ID_PRIVATE_KEY_PATH) + An optional path to store generated peer id private keys. If blank, will default to storing any generated keys in the root of the data directory. + + --p2pBootstrap.queryForIp <value> ($P2P_QUERY_FOR_IP) + If announceUdpAddress or announceTcpAddress are not provided, query for the IP address of the machine. Default is false. + + --p2pBootstrap.publicIpServices <value> (default: https://api.ipify.org/,https://checkip.amazonaws.com/,https://ifconfig.me/ip,https://icanhazip.com/)($P2P_PUBLIC_IP_SERVICES) + Comma-separated HTTPS URLs that return plain-text public IPv4. Used when P2P_QUERY_FOR_IP is true and P2P_IP is unset. Tried in order until one succeeds. + + TELEMETRY + + --tel.metricsCollectorUrl <value> ($OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) + The URL of the telemetry collector for metrics + + --tel.tracesCollectorUrl <value> ($OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) + The URL of the telemetry collector for traces + + --tel.logsCollectorUrl <value> ($OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) + The URL of the telemetry collector for logs + + --tel.otelCollectIntervalMs <value> (default: 60000) ($OTEL_COLLECT_INTERVAL_MS) + The interval at which to collect metrics + + --tel.otelExportTimeoutMs <value> (default: 30000) ($OTEL_EXPORT_TIMEOUT_MS) + The timeout for exporting metrics + + --tel.otelExcludeMetrics <value> (default: ) ($OTEL_EXCLUDE_METRICS) + A list of metric prefixes to exclude from export + + --tel.otelMinTraceDurationMs <value> (default: 10) ($OTEL_MIN_TRACE_DURATION_MS) + The minimum successful trace duration to export in milliseconds. Set to 0 to export all traces. + + --tel.otelBspMaxQueueSize <value> (default: 65536) ($OTEL_BSP_MAX_QUEUE_SIZE) + The maximum number of completed spans to queue before export. + + --tel.otelBspMaxExportBatchSize <value> (default: 4096) ($OTEL_BSP_MAX_EXPORT_BATCH_SIZE) + The maximum number of spans to send to the collector in a single export. + + --tel.otelBspScheduleDelayMs <value> (default: 5000) ($OTEL_BSP_SCHEDULE_DELAY) + How long to wait before exporting a partially filled batch of spans, in milliseconds. + + --tel.otelIncludeMetrics <value> (default: ) ($OTEL_INCLUDE_METRICS) + A list of metric prefixes to include in export (ignored if OTEL_EXCLUDE_METRICS is set) + + --tel.publicMetricsCollectorUrl <value> ($PUBLIC_OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) + A URL to publish a subset of metrics for public consumption + + --tel.publicMetricsCollectFrom <value> (default: ) ($PUBLIC_OTEL_COLLECT_FROM) + The role types to collect metrics from + + --tel.publicIncludeMetrics <value> (default: ) ($PUBLIC_OTEL_INCLUDE_METRICS) + A list of metric prefixes to publicly export + + --tel.publicMetricsOptOut <value> (default: true) ($PUBLIC_OTEL_OPT_OUT) + Whether to opt out of sharing optional telemetry + + BOT + + --bot + Starts Aztec Bot with options + + --bot.nodeUrl <value> ($AZTEC_NODE_URL) + The URL to the Aztec node to check for tx pool status. + + --bot.nodeAdminUrl <value> ($AZTEC_NODE_ADMIN_URL) + The URL to the Aztec node admin API to force-flush txs if configured. + + --bot.l1Mnemonic <value> ($BOT_L1_MNEMONIC) + The mnemonic for the account to bridge fee juice from L1. + + --bot.l1PrivateKey <value> ($BOT_L1_PRIVATE_KEY) + The private key for the account to bridge fee juice from L1. + + --bot.l1ToL2MessageTimeoutSeconds <value> (default: 3600) ($BOT_L1_TO_L2_TIMEOUT_SECONDS) + How long to wait for L1 to L2 messages to become available on L2 + + --bot.senderPrivateKey <value> ($BOT_PRIVATE_KEY) + Signing private key for the sender account. + + --bot.senderSalt <value> ($BOT_ACCOUNT_SALT) + The salt to use to deploy the sender account. + + --bot.tokenSalt <value> (default: 0x0000000000000000000000000000000000000000000000000000000000000001)($BOT_TOKEN_SALT) + The salt to use to deploy the token contract. + + --bot.txIntervalSeconds <value> (default: 60) ($BOT_TX_INTERVAL_SECONDS) + Every how many seconds should a new tx be sent. + + --bot.privateTransfersPerTx <value> (default: 1) ($BOT_PRIVATE_TRANSFERS_PER_TX) + How many private token transfers are executed per tx. + + --bot.publicTransfersPerTx <value> (default: 1) ($BOT_PUBLIC_TRANSFERS_PER_TX) + How many public token transfers are executed per tx. + + --bot.feePaymentMethod <value> (default: fee_juice) ($BOT_FEE_PAYMENT_METHOD) + How to handle fee payments. (Options: fee_juice) + + --bot.minFeePadding <value> (default: 3) ($BOT_MIN_FEE_PADDING) + How much is the bot willing to overpay vs. the current base fee + + --bot.noStart <value> ($BOT_NO_START) + True to not automatically setup or start the bot on initialization. + + --bot.txMinedWaitSeconds <value> (default: 180) ($BOT_TX_MINED_WAIT_SECONDS) + How long to wait for a tx to be mined before reporting an error. + + --bot.followChain <value> (default: NONE) ($BOT_FOLLOW_CHAIN) + Which chain the bot follows + + --bot.maxPendingTxs <value> (default: 128) ($BOT_MAX_PENDING_TXS) + Do not send a tx if the node's tx pool already has this many pending txs. + + --bot.flushSetupTransactions <value> ($BOT_FLUSH_SETUP_TRANSACTIONS) + Make a request for the sequencer to build a block after each setup transaction. + + --bot.l2GasLimit <value> ($BOT_L2_GAS_LIMIT) + L2 gas limit for the tx (empty to let the bot's wallet estimate). + + --bot.daGasLimit <value> ($BOT_DA_GAS_LIMIT) + DA gas limit for the tx (empty to let the bot's wallet estimate). + + --bot.contract <value> (default: TokenContract) ($BOT_TOKEN_CONTRACT) + Token contract to use + + --bot.maxConsecutiveErrors <value> ($BOT_MAX_CONSECUTIVE_ERRORS) + The maximum number of consecutive errors before the bot shuts down + + --bot.stopWhenUnhealthy <value> ($BOT_STOP_WHEN_UNHEALTHY) + Stops the bot if service becomes unhealthy + + --bot.botMode <value> (default: transfer) ($BOT_MODE) + Bot mode: transfer, amm, crosschain, or inbox + + --bot.l2ToL1MessagesPerTx <value> (default: 1) ($BOT_L2_TO_L1_MESSAGES_PER_TX) + Number of L2→L1 messages per tx (crosschain mode) + + --bot.l1ToL2SeedCount <value> ($BOT_L1_TO_L2_SEED_COUNT) + Max L1→L2 messages to keep in-flight (crosschain and inbox modes). Defaults to 1, or 512 in inbox mode. + + --bot.inboxMessagesPerBatch <value> (default: 4) ($BOT_INBOX_MESSAGES_PER_BATCH) + How many L1→L2 messages the inbox bot sends per atomic L1 batch (inbox mode) + + --bot.inboxConsumeMode <value> (default: mixed) ($BOT_INBOX_CONSUME_MODE) + Which L2 domain the inbox bot consumes through: mixed, public, or private (inbox mode) + + --bot.inboxSaturationIntervalSeconds <value> (default: 86400) ($BOT_INBOX_SATURATION_INTERVAL_SECONDS) + How often the inbox bot runs a full-bucket saturation batch, in seconds; 0 disables it + + PXE + + --pxe.l2BlockBatchSize <value> (default: 50) ($PXE_L2_BLOCK_BATCH_SIZE) + Maximum amount of blocks to pull from the stream in one request when synchronizing + + --pxe.proverEnabled <value> (default: true) ($PXE_PROVER_ENABLED) + Enable real proofs + + --pxe.syncChainTip <value> (default: proposed) ($PXE_SYNC_CHAIN_TIP) + Which chain tip to sync to (proposed, checkpointed, proven, finalized) + + --pxe.autoSync <value> (default: true) ($PXE_AUTO_SYNC) + Whether PXE syncs with the node automatically before each operation. Disable to let the caller (e.g. a wallet) drive syncs explicitly via pxe.sync(). + + --pxe.concurrentContractSyncEnabled <value> ($PXE_CONCURRENT_CONTRACT_SYNC_ENABLED) + Whether PXE speculatively syncs contracts it predicts will follow the one requested, running them concurrently with it. Repeated flows sync faster, but a wrong prediction spends unnecessary node requests. Experimental, off by default. + + --pxe.nodeUrl <value> ($AZTEC_NODE_URL) + Custom Aztec Node URL to connect to + + TXE + + --txe + Starts Aztec TXE with options +``` diff --git a/docs/lines/v6/docs-operate/operators/reference/ethereum-rpc-reference.md b/docs/lines/v6/docs-operate/operators/reference/ethereum-rpc-reference.md new file mode 100644 index 00000000000..604294d8e72 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/ethereum-rpc-reference.md @@ -0,0 +1,311 @@ +--- +id: ethereum_rpc_reference +displayed_sidebar: operatorsSidebar +title: Ethereum RPC call reference +description: A comprehensive reference of Ethereum RPC calls used by different Aztec node components, including archiver, sequencer, prover, and slasher nodes. +references: ["labs/yarn-project/ethereum/src/*", "labs/yarn-project/archiver/src/*", "labs/yarn-project/sequencer-client/src/publisher/*", "labs/yarn-project/slasher/src/*"] +--- + +This guide provides a comprehensive reference of Ethereum RPC calls used by different Aztec node components. Understanding these calls helps with infrastructure planning, monitoring, and debugging. + +## Prerequisites + +Before proceeding, you should: + +- Understand how Aztec nodes interact with Ethereum L1 +- Be familiar with Ethereum JSON-RPC API specifications +- Have basic knowledge of the viem library (Aztec's Ethereum client library) + +## Overview + +Aztec nodes interact with Ethereum L1 through the [viem](https://viem.sh) library, which provides a type-safe interface to Ethereum JSON-RPC methods. Different node components make different RPC calls based on their responsibilities: + +- **Archiver**: Monitors L1 for new blocks and events +- **Sequencer**: Proposes blocks and submits them to L1 +- **Prover**: Submits proofs to L1 +- **Validator**: Reads L1 state for validation +- **Slasher**: Monitors for misbehavior and submits slashing payloads + +## RPC call mapping + +This table shows the Ethereum JSON-RPC calls used by Aztec nodes: + +| Ethereum RPC Call | Description | +|------------------|-------------| +| `eth_getBlockByNumber` | Retrieve block information | +| `eth_blockNumber` | Get latest block number | +| `eth_getTransactionByHash` | Get transaction details | +| `eth_getTransactionReceipt` | Get transaction receipt | +| `eth_getTransactionCount` | Get account nonce | +| `eth_getLogs` | Retrieve event logs | +| `eth_getBalance` | Get account ETH balance | +| `eth_getCode` | Get contract bytecode | +| `eth_getStorageAt` | Read contract storage slot | +| `eth_chainId` | Get chain identifier | +| `eth_estimateGas` | Estimate gas for transaction | +| `eth_call` | Execute read-only call | +| `eth_sendRawTransaction` | Broadcast signed transaction | +| `eth_gasPrice` | Get current gas price | +| `eth_maxPriorityFeePerGas` | Get priority fee (EIP-1559) | + +## Archiver node + +The archiver continuously monitors L1 for new blocks and retrieves historical data. + +### Block retrieval + +**Purpose**: Sync L2 block data published to L1 + +**RPC calls used**: +- `eth_blockNumber` - Get latest L1 block number +- `eth_getLogs` - Retrieve rollup contract events +- `eth_getBlockByNumber` - Get block timestamps and metadata + +**Example RPC calls**: +```json +// eth_blockNumber +{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1} + +// eth_getLogs +{"jsonrpc":"2.0","method":"eth_getLogs","params":[{ + "fromBlock":"0x100", + "toBlock":"0x200", + "address":"0x...", + "topics":["0x..."] +}],"id":2} + +// eth_getBlockByNumber +{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x100",false],"id":3} +``` + +### L1 to L2 message retrieval + +**Purpose**: Track messages sent from L1 to L2 + +**RPC calls used**: +- `eth_getLogs` - Retrieve `MessageSent` events from Inbox contract + +### Contract event monitoring + +**Purpose**: Monitor contract deployments and updates + +**RPC calls used**: +- `eth_getLogs` - Retrieve events from ClassRegistry and InstanceRegistry + +**Events monitored**: +- `ContractClassPublished` +- `ContractInstancePublished` +- `ContractInstanceUpdated` +- `PrivateFunctionBroadcasted` +- `UtilityFunctionBroadcasted` + +## Sequencer node + +Sequencers propose blocks and submit them to L1, they also read L1 state to validate blocks and participate in consensus. + +### Transaction broadcasting + +**Purpose**: Submit block proposals to L1 + +**RPC calls used**: +- `eth_getTransactionCount` - Get nonce for sender account +- `eth_estimateGas` - Estimate gas for proposal transaction +- `eth_sendRawTransaction` - Broadcast signed transaction +- `eth_getTransactionReceipt` - Verify transaction inclusion + +**Example RPC calls**: +```json +// eth_getTransactionCount +{"jsonrpc":"2.0","method":"eth_getTransactionCount","params":["0x...","latest"],"id":1} + +// eth_estimateGas +{"jsonrpc":"2.0","method":"eth_estimateGas","params":[{ + "from":"0x...", + "to":"0x...", + "data":"0x..." +}],"id":2} + +// eth_sendRawTransaction +{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x..."],"id":3} + +// eth_getTransactionReceipt +{"jsonrpc":"2.0","method":"eth_getTransactionReceipt","params":["0x..."],"id":4} +``` + +### State reading + +**Purpose**: Read rollup state and validate proposals + +**RPC calls used**: +- `eth_call` - Read contract state +- `eth_getStorageAt` - Read specific storage slots +- `eth_blockNumber` - Get current L1 block for validation context +- `eth_getBlockByNumber` - Get block timestamps + +**Example RPC calls**: +```json +// eth_call +{"jsonrpc":"2.0","method":"eth_call","params":[{ + "to":"0x...", + "data":"0x..." +},"latest"],"id":1} + +// eth_getStorageAt +{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["0x...","0x0","latest"],"id":2} +``` + +### Gas management + +**Purpose**: Monitor gas prices and publisher account balances + +**RPC calls used**: +- `eth_getBalance` - Check publisher account balance +- `eth_gasPrice` / `eth_maxPriorityFeePerGas` - Get current gas prices + +### Block simulation + +**Purpose**: Validate block proposals before submission + +**RPC calls used**: +- `eth_call` - Simulate contract call to validate proposals + +## Prover node + +The prover submits validity proofs to L1. + +### Proof submission + +**Purpose**: Submit epoch proofs to the Rollup contract + +**RPC calls used**: +- `eth_getTransactionCount` - Get nonce for prover publisher +- `eth_estimateGas` - Estimate gas for proof submission +- `eth_sendRawTransaction` - Broadcast proof transaction +- `eth_getTransactionReceipt` - Confirm proof inclusion + +**Note**: Uses the same transaction flow as sequencer broadcasting + +### Chain state monitoring + +**Purpose**: Track L1 state for attestation validation + +**RPC calls used**: +- `eth_getBlockByNumber` - Get L1 timestamps for epoch calculations +- `eth_chainId` - Verify connected to correct chain + +## Slasher node + +The slasher monitors for validator misbehavior and submits slashing payloads. + +### Misbehavior detection + +**Purpose**: Monitor for slashable offenses and create slash payloads + +**RPC calls used**: +- `eth_getLogs` - Retrieve rollup events for analysis +- `eth_getBlockByNumber` - Get block timestamps for slashing proofs +- `eth_call` - Read validator state + +### Slashing payload submission + +**Purpose**: Submit slash payloads to L1 + +**RPC calls used**: +- `eth_getTransactionCount` - Get nonce for slasher account +- `eth_sendRawTransaction` - Broadcast slashing transaction +- `eth_getTransactionReceipt` - Verify slash transaction inclusion + +**Note**: Uses the same transaction flow as sequencer broadcasting + +## Shared infrastructure + +Aztec provides shared transaction management utilities for all components that submit to L1. + +### Core functionality + +**RPC calls used**: +- `eth_getTransactionCount` - Nonce management +- `eth_estimateGas` - Gas estimation +- `eth_gasPrice` / `eth_maxPriorityFeePerGas` - Gas pricing (EIP-1559) +- `eth_sendRawTransaction` - Transaction broadcasting +- `eth_getTransactionReceipt` - Transaction status checking +- `eth_getTransactionByHash` - Transaction lookup for replacement +- `eth_getBlockByNumber` - Block timestamp for timeout checks +- `eth_getBalance` - Publisher balance monitoring + +### Transaction lifecycle + +1. **Preparation**: Estimate gas and get gas price +2. **Nonce management**: Get and track nonce via `NonceManager` +3. **Signing**: Sign transaction with keystore +4. **Broadcasting**: Send via `eth_sendRawTransaction` +5. **Monitoring**: Poll with `eth_getTransactionReceipt` +6. **Replacement**: Replace stuck transactions if needed +7. **Cancellation**: Send zero-value transaction to cancel + +## RPC endpoint configuration + +### Environment variables + +Configure L1 RPC endpoints using: + +```bash +# Single endpoint +ETHEREUM_HOSTS=https://eth-mainnet.example.com + +# Multiple endpoints (fallback) +ETHEREUM_HOSTS=https://eth-mainnet-1.example.com,https://eth-mainnet-2.example.com + +# Consensus endpoints for archiver +L1_CONSENSUS_HOST_URLS=https://beacon-node.example.com +``` + +### Fallback configuration + +Aztec automatically retries failed requests on alternative endpoints when multiple RPC URLs are configured. This provides reliability and redundancy for critical operations. + +## Monitoring and debugging + +### RPC call logging + +Enable detailed RPC logging: + +```bash +LOG_LEVEL=debug # or verbose +``` + +Look for log entries related to: +- Transaction lifecycle and nonce management +- Block sync and event retrieval +- Block proposal submissions +- Contract interactions + +### Common issues + +**Issue**: `eth_getLogs` query exceeds limits + +**Solution**: +- Reduce block range in queries +- Use archive node with higher limits +- Implement chunked log retrieval + +**Issue**: Transaction replacement failures + +**Solution**: +- Ensure `eth_getTransactionCount` returns consistent nonces +- Configure appropriate gas price bumps +- Monitor transaction pool status + +**Issue**: Stale state reads + +**Solution**: +- Use specific block tags (not `latest`) +- Disable caching with `cacheTime: 0` +- Ensure RPC node is fully synced + +## Next steps + +- Review [How to Run a Sequencer Node](../setup/sequencer-setup.md) for operational guidance +- Explore [Advanced Keystore Patterns](../keystore/advanced-patterns.md) for complex key management +- Check [Useful Commands](../sequencer-management/useful-commands.md) for monitoring tools +- Join the [Aztec Discord](https://discord.gg/aztec) for infrastructure support diff --git a/docs/lines/v6/docs-operate/operators/reference/glossary.md b/docs/lines/v6/docs-operate/operators/reference/glossary.md new file mode 100644 index 00000000000..e7b1018a950 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/glossary.md @@ -0,0 +1,307 @@ +--- +id: glossary +displayed_sidebar: operatorsSidebar +title: Glossary +description: A comprehensive glossary of terms used throughout the Aztec network documentation, covering node operations, consensus, cryptography, and infrastructure concepts. +--- + +This glossary defines key terms used throughout the Aztec network documentation. Terms are organized alphabetically with cross-references to related concepts. + +## A + +### Agent + +See [Prover Agent](#prover-agent). + +### Archiver + +A component that monitors Ethereum L1 for rollup events and synchronizes L2 state. The archiver retrieves block data, contract deployments, and L1-to-L2 messages from the data availability layer. + +### Attestation + +A cryptographic signature from a sequencer committee member confirming the validity of a proposed block. Blocks require attestations from two-thirds of the committee plus one before submission to L1. + +### Attester + +The identity of a sequencer node in the network. The Ethereum address derived from the attester private key uniquely identifies the sequencer and is used to sign block proposals and attestations. + +## B + +### BIP44 + +Bitcoin Improvement Proposal 44 defines a standard derivation path for hierarchical deterministic wallets. Aztec uses BIP44 to derive multiple Ethereum addresses from a single mnemonic seed phrase. + +### Block Proposal + +A candidate block assembled by a sequencer containing ordered transactions. Proposals must be validated by the sequencer committee before submission to L1. + +### Bootnode + +A network node that facilitates peer discovery by maintaining lists of active peers. New nodes connect to bootnodes to discover and join the P2P network. + +### Broker + +See [Prover Broker](#prover-broker). + +## C + +### Coinbase + +The Ethereum address that receives L1 rewards and fees for a sequencer. If not specified in the keystore, defaults to the attester address. + +### Committee + +See [Sequencer Committee](#sequencer-committee). + +### Consensus + +The process by which sequencer nodes agree on the validity of proposed blocks through attestations and signatures. + +### Contract Class + +A published smart contract definition containing bytecode and function signatures. Multiple contract instances can be deployed from a single contract class. + +### Contract Instance + +A deployed instance of a contract class with a unique address and storage state. + +## D + +### Data Availability + +The guarantee that block data is accessible to network participants. Aztec publishes data to Ethereum L1 to ensure data availability for state reconstruction. + +### Derivation Path + +A hierarchical path used to derive cryptographic keys from a master seed. Follows the BIP44 standard for deterministic key generation. + +## E + +### EIP-1559 + +Ethereum Improvement Proposal 1559 introduces a base fee mechanism for transaction pricing. Aztec nodes use EIP-1559 gas pricing when submitting transactions to L1. + +### ENR (Ethereum Node Record) + +A signed record containing information about a network node, used for peer discovery in the P2P network. Bootnodes share their ENR for other nodes to connect. + +### Epoch + +A period of multiple L2 blocks that are proven together. Prover nodes generate a single validity proof for an entire epoch and submit it to the rollup contract. + +### Execution Layer + +The Ethereum L1 execution client (e.g., Geth, Nethermind) that processes transactions. Aztec nodes require access to an execution layer RPC endpoint. + +## F + +### Fee Recipient + +The Aztec address that receives unburnt transaction fees from blocks produced by a sequencer. Must be a deployed Aztec account. + +### Full Node + +A node that maintains a complete copy of the Aztec blockchain state and provides RPC interfaces for users to interact with the network without relying on third parties. + +## G + +### Gas Estimation + +The process of calculating the expected gas cost for an Ethereum transaction before submission. Aztec nodes estimate gas for L1 transactions like block proposals and proof submissions. + +## I + +### Inbox + +The L1 contract that receives messages sent from Ethereum to Aztec L2. The archiver monitors the Inbox for new L1-to-L2 messages. + +## J + +### JSON V3 Keystore + +An Ethereum standard for encrypted key storage using AES-128-CTR encryption and scrypt key derivation. Aztec supports JSON V3 keystores for secure key management. + +## K + +### Keystore + +A configuration file or encrypted store containing private keys for sequencer operations. Keystores define attester keys, publisher keys, coinbase addresses, and fee recipients. + +## L + +### L1 (Layer 1) + +#if(testnet) +Ethereum mainnet or testnet, serving as the base layer for Aztec's rollup. L1 provides data availability, settlement, and consensus for the L2. +#else +Ethereum mainnet, serving as the base layer for Aztec's rollup. L1 provides data availability, settlement, and consensus for the L2. +#endif + +### L2 (Layer 2) + +The Aztec network, a rollup scaling solution built on top of Ethereum L1. L2 processes transactions offchain and submits validity proofs to L1. + +### L1 Sync + +A synchronization mode where nodes reconstruct state by querying the rollup contract and data availability layer on Ethereum L1 directly. + +## M + +### Mempool + +The pool of unprocessed transactions waiting to be included in a block. Sequencers select transactions from the mempool when proposing blocks. + +### Merkle Tree + +A cryptographic data structure that enables efficient verification of data integrity and membership. Aztec uses Merkle trees for state commitments, note storage, and nullifier tracking. + +### Mnemonic + +A human-readable seed phrase (typically 12 or 24 words) used to generate deterministic cryptographic keys. Follows BIP39 standard for encoding. + +## N + +### Node + +A participant in the Aztec network. See [Full Node](#full-node), [Sequencer Node](#sequencer-node), [Prover Node](#prover-node), or [Bootnode](#bootnode). + +### Nonce + +A sequential number used to order transactions from an Ethereum account. Aztec nodes manage nonces when submitting transactions to L1. + +### Note Tree + +A Merkle tree containing encrypted notes representing private state in Aztec contracts. + +### Nullifier + +A unique value that marks a note as consumed, preventing double-spending. Nullifiers are published to L1 and tracked in the nullifier tree. + +## O + +### Outbox + +The L1 contract that receives messages sent from Aztec L2 to Ethereum. Used for withdrawals and cross-chain communication. + +## P + +### P2P (Peer-to-Peer) + +The network protocol used by Aztec nodes to discover peers, exchange transactions, and propagate blocks without central coordination. + +### Proof-of-Stake + +The consensus mechanism where sequencers lock collateral (stake) to participate in block production. Misbehavior results in stake slashing. + +### Prover Agent + +A stateless worker that executes proof generation jobs. Multiple agents can run in parallel to distribute proving workload. + +### Prover Broker + +A coordinator that manages the prover job queue, distributing work to agents and collecting results. + +### Prover Node + +Infrastructure that generates validity proofs for epochs of L2 blocks. Consists of a prover node coordinator, broker, and one or more agents. + +### Publisher + +The Ethereum account used by a sequencer to submit block proposals to L1. Must be funded with ETH to pay gas fees. If not specified, the attester key is used. + +### PXE (Private Execution Environment) + +The client-side component that executes private functions, manages user keys, and constructs privacy-preserving transactions. + +## R + +### Release line + +A major version of the Aztec software and the name it ships under. The v6 line is called **Tierra**, so every `6.x` release is Tierra. Networks (Alpha and Testnet) run a release line, while node configuration pins an exact version and keeps `NETWORK_NAME` as `mainnet` or `testnet`. See the [Networks page](/networks) for the version each network runs. + +### Registry + +The L1 contract that tracks deployed contract classes and instances. The archiver monitors Registry events to maintain a database of available contracts. + +### Remote Signer + +An external service (e.g., Web3Signer) that stores private keys and signs transactions remotely. Used for enhanced security in production deployments. + +### Rollup + +A scaling solution that processes transactions offchain and submits compressed data and validity proofs to L1. Aztec is a decentralized zkRollup with programmable privacy. + +### RPC (Remote Procedure Call) + +A protocol for remote communication. Aztec nodes expose JSON-RPC interfaces for client interaction and use RPC to communicate with Ethereum L1. + +## S + +### Sequencer Committee + +A rotating group of validators responsible for validating proposed blocks through attestations during a specific time period. + +### Sequencer Node + +A validator that assembles transactions into blocks, executes public functions, and participates in consensus through attestations. + +### Slashing + +The penalty mechanism that reduces or confiscates a sequencer's stake for provable misbehavior such as double-signing or prolonged downtime. + +### Slasher Node + +Infrastructure that monitors for validator misbehavior and submits slashing payloads to L1 when violations are detected. + +### Snapshot + +A pre-built database containing blockchain state at a specific block height. Nodes can download snapshots for faster synchronization. + +### Snapshot Sync + +A synchronization mode where nodes download pre-built state snapshots instead of reconstructing state from L1. Significantly faster than L1 sync. + +### Stake + +Collateral locked by a sequencer to participate in block production. Higher stake increases selection probability as block proposer. + +### State Tree + +A Merkle tree representing the current world state of all Aztec contracts and accounts. + +## T + +### Tierra + +The name of the v6 release line. See [Release line](#release-line). + +### Transaction Receipt + +A record of a transaction's execution on Ethereum, including status, gas used, and emitted events. Aztec nodes poll for receipts to confirm L1 transaction inclusion. + +## V + +### Validator + +See [Sequencer Node](#sequencer-node). The terms are used interchangeably in Aztec documentation. + +### Viem + +A TypeScript library providing type-safe interfaces to Ethereum JSON-RPC methods. Aztec nodes use viem for all L1 interactions. + +## W + +### Web3Signer + +An open-source remote signing service that stores keys securely and provides signing APIs. Commonly used for production sequencer deployments. + +### World State + +The complete state of the Aztec network at a given block height, including all contract storage, notes, and nullifiers. + +## Related Resources + +- [Node API Reference](./node-api-reference.md) - Complete API documentation for node JSON-RPC methods +- [Ethereum RPC Reference](./ethereum-rpc-reference.md) - L1 RPC calls used by Aztec components +- [Advanced Keystore Guide](../keystore/index.md) - Detailed keystore configuration options +- [CLI Reference](./cli-reference.md) - Complete command-line interface documentation diff --git a/docs/lines/v6/docs-operate/operators/reference/node-api-reference.md b/docs/lines/v6/docs-operate/operators/reference/node-api-reference.md new file mode 100644 index 00000000000..6d6a6b16b1b --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/node-api-reference.md @@ -0,0 +1,1495 @@ +--- +id: node_api_reference +displayed_sidebar: operatorsSidebar +title: Node JSON RPC API reference +description: Complete reference for the Aztec Node JSON RPC API, including block queries, transaction submission, world state access, and administrative operations. +references: ["labs/yarn-project/stdlib/src/interfaces/aztec-node.ts", "labs/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts"] +--- + +<!-- AUTO-GENERATED FILE. DO NOT EDIT MANUALLY. --> +<!-- Generated by docs/scripts/node_api_reference_generation/generate_node_api_reference.mts --> + +This document provides a complete reference for the Aztec Node JSON RPC API. All methods are exposed via JSON RPC on the node's configured ports. + +## API endpoint + +**Public RPC URL**: `http://localhost:8080` + +**Admin URL**: `http://localhost:8880` + +Note that the above ports are only defaults, and can be modified by setting `--port` and `--admin-port` flags upon startup. + +All methods use standard JSON RPC 2.0 format with methods prefixed by `aztec_` or `aztecAdmin_`. + +## Block queries + +### aztec_getBlockNumber + +Returns the block number at a given block tag, or the latest proposed block number when +`tip` is omitted. + +**Parameters**: + +1. `tip` - `BlockTagWithoutLatest | undefined` + +**Returns**: `number` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getBlockNumber","params":["0x1234..."],"id":1}' +``` + +### aztec_getCheckpointNumber + +Returns the checkpoint number at a given checkpoint tag, or the latest checkpointed number when +`tip` is omitted. The proposed-but-unconfirmed checkpoint frontier is archiver-internal and not +exposed over RPC, so `'proposed'` is not a valid checkpoint tag (see ). + +**Parameters**: + +1. `tip` - `CheckpointTag | undefined` + +**Returns**: `number` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getCheckpointNumber","params":["0x1234..."],"id":1}' +``` + +### aztec_getChainTips + +Returns the tips of the L2 chain. + +**Parameters**: None + +**Returns**: `L2Tips` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getChainTips","params":[],"id":1}' +``` + +### aztec_getL1Constants + +Returns the rollup constants for the current chain. + +**Parameters**: None + +**Returns**: `L1RollupConstants` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getL1Constants","params":[],"id":1}' +``` + +### aztec_getSyncedL2SlotNumber + +Returns the last L2 slot number for which the node has all L1 data needed to build the next checkpoint. + +**Parameters**: None + +**Returns**: `SlotNumber | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getSyncedL2SlotNumber","params":[],"id":1}' +``` + +### aztec_getSyncedL2EpochNumber + +Returns the last L2 epoch number that has been fully synchronized from L1. + +**Parameters**: None + +**Returns**: `number | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getSyncedL2EpochNumber","params":[],"id":1}' +``` + +### aztec_getSyncedL1Timestamp + +Returns the latest L1 timestamp according to the archiver's synced L1 view. + +**Parameters**: None + +**Returns**: `bigint | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getSyncedL1Timestamp","params":[],"id":1}' +``` + +### aztec_getBlock + +Unified block fetch. Returns the block identified by `param`, with optional fields controlled +by `options`. + +**Parameters**: + +1. `param` - `BlockHash | number | "latest"` - A block number, block hash, archive root, chain-tip name, or object variant. +2. `options` - `BlockIncludeOptions | undefined` - Narrowing options: `includeTransactions`, `includeL1PublishInfo`, `includeAttestations`. + +**Returns**: `BlockResponse | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getBlock","params":["latest","0x1234..."],"id":1}' +``` + +### aztec_getBlockData + +Lightweight block-metadata fetch. Returns the block identified by `param` without transaction +bodies or other optional context. Cheaper than `getBlock` for header-only access. + +**Parameters**: + +1. `param` - `BlockHash | number | "latest"` - A block number, block hash, archive root, chain-tip name, or object variant. + +**Returns**: `BlockData | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getBlockData","params":["latest"],"id":1}' +``` + +### aztec_getBlocks + +Returns up to `limit` blocks starting from `from`, projected to the +shape determined by `options`. + +**Parameters**: + +1. `from` - `number` +2. `limit` - `number` +3. `options` - `BlocksIncludeOptions | undefined` + +**Returns**: `BlockResponse[]` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getBlocks","params":[1,100,"0x1234..."],"id":1}' +``` + +### aztec_getCheckpoint + +Unified checkpoint fetch. Returns the checkpoint identified by `param`, with optional fields +controlled by `options`. + +**Parameters**: + +1. `param` - `CheckpointParameter` +2. `options` - `CheckpointIncludeOptions | undefined` + +**Returns**: `CheckpointResponse | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getCheckpoint","params":["0x1234...","0x1234..."],"id":1}' +``` + +### aztec_getCheckpoints + +Returns up to `limit` checkpoints starting from `from`, projected to the + shape determined by `options`. + +**Parameters**: + +1. `from` - `number` +2. `limit` - `number` +3. `options` - `CheckpointIncludeOptions | undefined` + +**Returns**: `CheckpointResponse[]` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getCheckpoints","params":[1,100,"0x1234..."],"id":1}' +``` + +### aztec_getCheckpointsData + +Gets lightweight checkpoint metadata for a contiguous range or for an entire epoch. + +**Parameters**: + +1. `query` - `CheckpointsQuery` - Either `{ from, limit }` or `{ epoch }`. + +**Returns**: `CheckpointData[]` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getCheckpointsData","params":["0x1234..."],"id":1}' +``` + +## Transaction operations + +### aztec_sendTx + +Method to submit a transaction to the p2p pool. + +**Parameters**: + +1. `tx` - `Tx` - The transaction to be submitted. + +**Returns**: `void` - Nothing. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_sendTx","params":[{"data":"0x..."}],"id":1}' +``` + +### aztec_getTxReceipt + +Fetches a transaction receipt for a given transaction hash. Always resolves to one of the lifecycle variants of +the union: a if the tx was included in a block, a +if it's still in the mempool of the connected Aztec node, or a if not found. + +**Parameters**: + +1. `txHash` - `TxHash` - The transaction hash. +2. `options` - `GetTxReceiptOptions | undefined` - Optional flags controlling which extra data is attached: `includeTxEffect` attaches the full + to a mined receipt, `includePendingTx` attaches the pending to a pending receipt, and +`includeProof` keeps the proof on that attached pending tx (only meaningful with `includePendingTx`). + +**Returns**: `TxReceipt` - A receipt of the transaction. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getTxReceipt","params":["0x1234...","0x1234..."],"id":1}' +``` + +### aztec_getTxEffect + +Gets a tx effect. + +**Deprecated**: Use `getTxReceipt(txHash, { includeTxEffect: true })` and read the `.txEffect` field instead. + +**Parameters**: + +1. `txHash` - `TxHash` - The hash of the tx corresponding to the tx effect. + +**Returns**: `IndexedTxEffect | undefined` - The requested tx effect with block info (or undefined if not found). + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getTxEffect","params":["0x1234..."],"id":1}' +``` + +### aztec_getTxByHash + +Method to retrieve a single pending tx. The tx's proof is stripped unless `includeProof` is set. + +**Parameters**: + +1. `txHash` - `TxHash` - The transaction hash to return. +2. `options` - `GetTxByHashOptions | undefined` - Options for the returned tx (eg whether to include its proof). + +**Returns**: `Tx | undefined` - The pending tx if it exists. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getTxByHash","params":["0x1234...","0x1234..."],"id":1}' +``` + +### aztec_getTxsByHash + +Method to retrieve multiple pending txs. The txs' proofs are stripped unless `includeProof` is set. + +**Parameters**: + +1. `txHashes` - `TxHash[]` +2. `options` - `GetTxByHashOptions | undefined` - Options for the returned txs (eg whether to include their proofs). + +**Returns**: `Tx[]` - The pending txs if exist. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getTxsByHash","params":[["0x1234..."],"0x1234..."],"id":1}' +``` + +### aztec_getPendingTxs + +Method to retrieve pending txs. The txs' proofs are stripped unless `includeProof` is set. + +**Parameters**: + +1. `limit` - `number | undefined` - The number of items to return. +2. `after` - `TxHash | undefined` - The last known pending tx. Used for pagination. +3. `options` - `GetTxByHashOptions | undefined` - Options for the returned txs (eg whether to include their proofs). + +**Returns**: `Tx[]` - The pending txs. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getPendingTxs","params":[100,"0x1234...","0x1234..."],"id":1}' +``` + +### aztec_getPendingTxCount + +Retrieves the number of pending txs + +**Parameters**: None + +**Returns**: `number` - The number of pending txs. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getPendingTxCount","params":[],"id":1}' +``` + +### aztec_isValidTx + +Returns true if the transaction is valid for inclusion at the current state. Valid transactions can be +made invalid by *other* transactions if e.g. they emit the same nullifiers, or come become invalid +due to e.g. the expiration_timestamp property. + +**Parameters**: + +1. `tx` - `Tx` - The transaction to validate for correctness. +2. `options` - `object | undefined` + +**Returns**: `TxValidationResult` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_isValidTx","params":[{"data":"0x..."},{}],"id":1}' +``` + +### aztec_simulatePublicCalls + +Simulates the public part of a transaction with the current state. +This currently just checks that the transaction execution succeeds. + +**Parameters**: + +1. `tx` - `Tx` - The transaction to simulate. +2. `skipFeeEnforcement` - `boolean | undefined` - If true, fee enforcement is skipped. +3. `overrides` - `SimulationOverrides | undefined` - Optional pre-simulation overrides applied to the ephemeral fork and contract DB +(publicStorage writes, contract instance overrides). + +**Returns**: `PublicSimulationOutput` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_simulatePublicCalls","params":[{"data":"0x..."},true,"0x1234..."],"id":1}' +``` + +## State queries + +### aztec_getPublicStorageAt + +Gets the storage value at the given contract storage slot. + +**Remarks**: The storage slot here refers to the slot as it is defined in Noir not the index in the merkle tree. +Aztec's version of `eth_getStorageAt`. + +**Parameters**: + +1. `referenceBlock` - `BlockHash | number | "latest"` - The block parameter (block number, block hash, or 'latest') at which to get the data. +2. `contract` - `AztecAddress` - Address of the contract to query. +3. `slot` - `Fr` - Slot to query. + +**Returns**: `Fr` - Storage value at the given contract slot. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getPublicStorageAt","params":["latest","0x1234...","0x1234..."],"id":1}' +``` + +### aztec_getWorldStateSyncStatus + +Returns the sync status of the node's world state + +**Parameters**: None + +**Returns**: `WorldStateSyncStatus` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getWorldStateSyncStatus","params":[],"id":1}' +``` + +## Membership witnesses + +### aztec_findLeavesIndexes + +Find the indexes of the given leaves in the given tree along with a block metadata pointing to the block in which +the leaves were inserted. + +**Parameters**: + +1. `referenceBlock` - `BlockHash | number | "latest"` - The block parameter (block number, block hash, or 'latest') at which to get the data. +2. `treeId` - `MerkleTreeId` - The tree to search in. +3. `leafValues` - `Fr[]` - The values to search for. + +**Returns**: `(DataInBlock<bigint> | undefined)[]` - The indices of leaves and the block metadata of a block in which the leaves were inserted. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_findLeavesIndexes","params":["latest",1,["0x1234..."]],"id":1}' +``` + +### aztec_getNullifierMembershipWitness + +Returns a nullifier membership witness for a given nullifier at a given block. + +**Parameters**: + +1. `referenceBlock` - `BlockHash | number | "latest"` - The block parameter (block number, block hash, or 'latest') at which to get the data. +2. `nullifier` - `Fr` - Nullifier we try to find witness for. + +**Returns**: `NullifierMembershipWitness | undefined` - The nullifier membership witness (if found). + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getNullifierMembershipWitness","params":["latest","0x1234..."],"id":1}' +``` + +### aztec_getLowNullifierMembershipWitness + +Returns a low nullifier membership witness for a given nullifier at a given block. + +**Remarks**: Low nullifier witness can be used to perform a nullifier non-inclusion proof by leveraging the "linked +list structure" of leaves and proving that a lower nullifier is pointing to a bigger next value than the nullifier +we are trying to prove non-inclusion for. + +**Parameters**: + +1. `referenceBlock` - `BlockHash | number | "latest"` - The block parameter (block number, block hash, or 'latest') at which to get the data. +2. `nullifier` - `Fr` - Nullifier we try to find the low nullifier witness for. + +**Returns**: `NullifierMembershipWitness | undefined` - The low nullifier membership witness (if found). + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getLowNullifierMembershipWitness","params":["latest","0x1234..."],"id":1}' +``` + +### aztec_getPublicDataWitness + +Returns a public data tree witness for a given leaf slot at a given block. + +**Remarks**: The witness can be used to compute the current value of the public data tree leaf. If the low leaf preimage corresponds to an +"in range" slot, means that the slot doesn't exist and the value is 0. If the low leaf preimage corresponds to the exact slot, the current value +is contained in the leaf preimage. + +**Parameters**: + +1. `referenceBlock` - `BlockHash | number | "latest"` - The block parameter (block number, block hash, or 'latest') at which to get the data. +2. `leafSlot` - `Fr` - The leaf slot we try to find the witness for. + +**Returns**: `PublicDataWitness | undefined` - The public data witness (if found). + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getPublicDataWitness","params":["latest","0x1234..."],"id":1}' +``` + +### aztec_getBlockHashMembershipWitness + +Returns a membership witness for a given block hash in the archive tree, against the reference block header's +`lastArchive.root`: the archive as it stood before the reference block itself was appended. This is the root a +circuit anchored on that header checks archive membership proofs against. + +Block hashes are the leaves of the archive tree. Each time a new block is added to the chain, +its block hash is appended as a new leaf to the archive tree. This method finds the membership +witness (leaf index and sibling path) for a given block hash, which can be used to prove that +a specific block exists in the chain's history. + +The reference block must exist. Every `BlockParameter` form selects that reference block, including +`{ archive }`, which identifies the block by its post-block archive root, so the witness is still against the +archive before that block. To prove membership against a supplied archive root itself, use +`getBlockHashMembershipWitnessAtArchive`. + +**Parameters**: + +1. `referenceBlock` - `BlockHash | number | "latest"` - The block whose header's `lastArchive` the witness is computed against. +2. `blockHash` - `BlockHash` - The block hash to find in the archive tree. + +**Returns**: `MembershipWitness | undefined` - The witness, or undefined if the block hash is not in that archive. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getBlockHashMembershipWitness","params":["latest","0x1234..."],"id":1}' +``` + +### aztec_getBlockHashMembershipWitnessAtArchive + +Returns a membership witness for a given block hash in the archive tree, against exactly the supplied archive +root. The root is the archive after some block's own hash was appended, so a block is a member of its own archive +and no later block needs to exist. + +Choosing a root does not establish that it is canonical or final: a node that has not yet seen a reorg can serve +a witness against a root that has been reorged away. The caller must pick a root its verifier accepts. + +**Parameters**: + +1. `reference` - `{ archive: Fr }` - The archive root to prove membership against. +2. `blockHash` - `BlockHash` - The block hash to find in the archive tree. + +**Returns**: `MembershipWitness | undefined` - The witness, or undefined if the block hash is not in that archive. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getBlockHashMembershipWitnessAtArchive","params":[{"archive":"0x1234..."},"0x1234..."],"id":1}' +``` + +### aztec_getNoteHashMembershipWitness + +Returns a membership witness for a given note hash at a given block. + +**Parameters**: + +1. `referenceBlock` - `BlockHash | number | "latest"` - The block parameter (block number, block hash, or 'latest') at which to get the data. +2. `noteHash` - `Fr` - The note hash we try to find the witness for. + +**Returns**: `MembershipWitness | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getNoteHashMembershipWitness","params":["latest","0x1234..."],"id":1}' +``` + +### aztec_getTxEffectMembershipWitness + +Returns a membership witness proving that tx `txHash` was included in its block and produced exactly the effects +the node reports for it. The witness is verified against the `txEffectsTreeRoot` of the block header of +`witness.blockNumber`. + +Returns `undefined` if the node does not know the tx. + +**Parameters**: + +1. `txHash` - `TxHash` - The tx to prove inclusion of. + +**Returns**: `TxEffectMembershipWitness | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getTxEffectMembershipWitness","params":["0x1234..."],"id":1}' +``` + +## L1 to L2 messages + +### aztec_getL1ToL2MessageMembershipWitness + +Returns the index and a sibling path for a leaf in the committed l1 to l2 data tree. + +**Parameters**: + +1. `referenceBlock` - `BlockHash | number | "latest"` - The block parameter (block number, block hash, or 'latest') at which to get the data. +2. `l1ToL2Message` - `Fr` - The l1ToL2Message to get the index / sibling path for. + +**Returns**: `[bigint, SiblingPath] | undefined` - A tuple of the index and the sibling path of the L1ToL2Message (undefined if not found). + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getL1ToL2MessageMembershipWitness","params":["latest","0x1234..."],"id":1}' +``` + +### aztec_getL1ToL2MessageIndex + +Returns the compact leaf index assigned to this L1 to L2 message as soon as the node has ingested it from L1, +before any L2 block consumes it. Returns undefined if the node has not yet seen the message. The message becomes +consumable once a block's L1-to-L2 message tree grows past this index. + +**Parameters**: + +1. `l1ToL2Message` - `Fr` + +**Returns**: `bigint | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getL1ToL2MessageIndex","params":["0x1234..."],"id":1}' +``` + +### aztec_getL2ToL1MembershipWitness + +Returns the L2-to-L1 membership witness for `message` emitted by tx `txHash`. The node selects +the smallest partial-proof root on the Outbox that covers the tx's checkpoint and builds the +witness against it. + +The node reads the Outbox roots lazily, pinned to its synced L1 block, so the witness reflects +the node's synced view. Returns `undefined` if the tx isn't yet in a block/epoch or no covering +root has landed on L1 as of the synced block. + +Caveat: cached roots that are sealed and L1-finalized are not re-validated. A reorg deeper than +L1 finality could leave the node serving a witness against a no-longer-canonical root. + +**Parameters**: + +1. `txHash` - `TxHash` - The tx whose L2-to-L1 message we want a witness for. +2. `message` - `Fr` - The message hash to prove inclusion of. +3. `messageIndexInTx` - `object | undefined` - Optional index of the message within the tx's L2-to-L1 messages; pass +this when the same message hash appears multiple times in the tx. + +**Returns**: `L2ToL1MembershipWitness | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getL2ToL1MembershipWitness","params":["0x1234...","0x1234...",{}],"id":1}' +``` + +### aztec_getL2ToL1Messages + +Returns all the L2 to L1 messages in an epoch. + +**Deprecated**: Use to get an L2-to-L1 message witness directly. + +**Parameters**: + +1. `epoch` - `number` - The epoch at which to get the data. + +**Returns**: `Fr[][][][]` - A nested array of the L2 to L1 messages in each tx of each block in each checkpoint in the epoch (empty +array if the epoch is not found). + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getL2ToL1Messages","params":[12345],"id":1}' +``` + +## Log queries + +### aztec_getPrivateLogsByTags + +Gets private logs matching the given tags. Returns one inner array per element of `query.tags`, in +input order. An empty inner array means no logs matched that tag. Set `query.includeEffects` to also +receive the tx's note hashes and nullifiers. + +The return type is the widest shape — `noteHashes`/`nullifiers` are typed as +optional even when `includeEffects: true` is set. JSON-RPC validation can't preserve a stricter +narrowing across the wire. Callers that want a narrowed type at the call site should use the typed +helpers in `pxe/src/tagging/get_all_logs_by_tags.ts`. + +**Parameters**: + +1. `query` - `PrivateLogsQuery` + +**Returns**: `LogResult[][]` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getPrivateLogsByTags","params":["0x1234..."],"id":1}' +``` + +### aztec_getPublicLogsByTags + +Gets public logs matching the given tags for the given contract. Returns one inner array per element +of `query.tags`, in input order. An empty inner array means no logs matched that tag. Set +`query.includeEffects` to also receive the tx's note hashes and nullifiers. + +The return type is the widest shape — see . + +**Parameters**: + +1. `query` - `PublicLogsQuery` + +**Returns**: `LogResult[][]` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getPublicLogsByTags","params":["0x1234..."],"id":1}' +``` + +## Contract queries + +### aztec_getContractClass + +Returns a registered contract class given its id. + +**Parameters**: + +1. `id` - `Fr` - Id of the contract class. + +**Returns**: `ContractClassPublic | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getContractClass","params":["0x1234..."],"id":1}' +``` + +### aztec_getContract + +Returns a publicly deployed contract instance given its address. Its current class id is resolved as of the given +reference block. + +Returns `undefined` if the instance has not been published (i.e. `publish_for_public_execution` was never called +on the `ContractInstanceRegistry`). A contract whose class has been updated will never return `undefined`: +scheduling an update requires the contract's deployment nullifier, which is only emitted by publishing, so any +updatable contract has necessarily been published. + +**Parameters**: + +1. `address` - `AztecAddress` - Address of the deployed contract. +2. `referenceBlock` - `BlockHash | number | "latest" | undefined` - The block parameter (block number, block hash, or 'latest') at which to get the data. +Defaults to 'latest'. + +**Returns**: `ContractInstanceWithAddress | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getContract","params":["0x1234...","latest"],"id":1}' +``` + +## Fee queries + +### aztec_getCurrentMinFees + +Method to fetch the current min fees. + +**Parameters**: None + +**Returns**: `GasFees` - The current min fees. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getCurrentMinFees","params":[],"id":1}' +``` + +### aztec_getPredictedMinFees + +Returns min fees a transaction submitted now may have to pay. When the node can compute it, the list +leads with the fee its public simulation would charge right now; the rest are the predicted min fees for +the current slot and next N slots, each accounting for the L1 gas oracle transition and congestion growth +based on the given mana usage estimate. Defaults to target usage (steady state). + +**Parameters**: + +1. `manaUsage` - `ManaUsageEstimate | undefined` - Expected mana usage per checkpoint (none, target, or limit). + +**Returns**: `GasFees[]` - An array of GasFees whose worst entry is the fee a client should pad and quote. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getPredictedMinFees","params":["target"],"id":1}' +``` + +### aztec_getMaxPriorityFees + +Method to fetch the current max priority fee of txs in the mempool. + +**Parameters**: None + +**Returns**: `GasFees` - The current max priority fees. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getMaxPriorityFees","params":[],"id":1}' +``` + +## Node information + +### aztec_isReady + +Method to determine if the node is ready to accept transactions. + +**Parameters**: None + +**Returns**: `boolean` - Flag indicating the readiness for tx submission. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_isReady","params":[],"id":1}' +``` + +### aztec_getNodeInfo + +Returns the information about the server's node. Includes current Node version, compatible Noir version, +L1 chain identifier, protocol version, and L1 address of the rollup contract. + +**Parameters**: None + +**Returns**: `NodeInfo` - The node information. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getNodeInfo","params":[],"id":1}' +``` + +### aztec_getNodeVersion + +Method to fetch the version of the package. + +**Parameters**: None + +**Returns**: `string` - The node package version + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getNodeVersion","params":[],"id":1}' +``` + +### aztec_getVersion + +Method to fetch the version of the rollup the node is connected to. + +**Parameters**: None + +**Returns**: `number` - The rollup version. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getVersion","params":[],"id":1}' +``` + +### aztec_getChainId + +Method to fetch the chain id of the base-layer for the rollup. + +**Parameters**: None + +**Returns**: `number` - The chain id. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getChainId","params":[],"id":1}' +``` + +### aztec_getL1ContractAddresses + +Method to fetch the currently deployed l1 contract addresses. + +**Parameters**: None + +**Returns**: `L1ContractAddresses` - The deployed contract addresses. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getL1ContractAddresses","params":[],"id":1}' +``` + +### aztec_getProtocolContractAddresses + +Method to fetch the protocol contract addresses. + +**Parameters**: None + +**Returns**: `ProtocolContractAddresses` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getProtocolContractAddresses","params":[],"id":1}' +``` + +### aztec_getEncodedEnr + +Returns the ENR of this node for peer discovery, if available. + +**Parameters**: None + +**Returns**: `string | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getEncodedEnr","params":[],"id":1}' +``` + +## Validator queries + +### aztec_getValidatorsStats + +Returns stats for validators if enabled. + +**Parameters**: None + +**Returns**: `ValidatorsStats` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getValidatorsStats","params":[],"id":1}' +``` + +### aztec_getValidatorStats + +Returns stats for a single validator if enabled. + +**Parameters**: + +1. `validatorAddress` - `EthAddress` +2. `fromSlot` - `SlotNumber | undefined` +3. `toSlot` - `SlotNumber | undefined` + +**Returns**: `SingleValidatorStats | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getValidatorStats","params":["0x1234...","100","100"],"id":1}' +``` + +### aztec_getValidatorStatsBatch + +Returns stats for up to 100 validators using a shared slot range. +The slot difference must not exceed the node sentinel's retained history window, and fromSlot must not exceed toSlot. +Results follow input order, including duplicates; missing validators or disabled statistics return null entries. + +**Parameters**: + +1. `validatorAddresses` - `EthAddress[]` +2. `fromSlot` - `SlotNumber | undefined` +3. `toSlot` - `SlotNumber | undefined` + +**Returns**: `object[]` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getValidatorStatsBatch","params":[["0x1234..."],"100","100"],"id":1}' +``` + +## P2P queries + +### aztec_getPeers + +Returns info for all connected, dialing, and cached peers. Only available when P2P is enabled. + +**Parameters**: + +1. `includePending` - `boolean | undefined` - If true, also include peers in the pending state. + +**Returns**: `PeerInfo[]` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getPeers","params":[true],"id":1}' +``` + +### aztec_getCheckpointAttestationsForSlot + +Queries the attestation pool for checkpoint attestations for the given slot. + +**Parameters**: + +1. `slot` - `SlotNumber` - The slot to query. +2. `proposalPayloadHash` - `string | undefined` - Hex-encoded keccak256 of the target proposal's signed payload hash. +When provided, only attestations whose payload hash matches are returned. +When omitted, all attestations for the slot are returned. + +**Returns**: `CheckpointAttestation[]` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getCheckpointAttestationsForSlot","params":["100","0x1234..."],"id":1}' +``` + +### aztec_getProposalsForSlot + +Returns block and checkpoint proposals retained in the attestation pool for the given slot. +Only available when P2P is enabled. + +**Parameters**: + +1. `slot` - `SlotNumber` + +**Returns**: `ProposalsForSlot` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getProposalsForSlot","params":["100"],"id":1}' +``` + +## Debug operations + +### aztec_getAllowedPublicSetup + +Returns the list of allowed public setup elements configured for this node. + +**Parameters**: None + +**Returns**: `AllowedElement[]` - The list of allowed elements. + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getAllowedPublicSetup","params":[],"id":1}' +``` + +## Admin API + +Administrative operations are exposed on port 8880 under the `aztecAdmin_` namespace. + +The admin API requires an API key by default. The node generates one at first startup and prints it once to the logs; pass it with the `x-api-key` header, as the examples below do, or as `Authorization: Bearer [ADMIN_API_KEY]`. See [admin API key authentication](./changelog/v4.md#admin-api-key-authentication). + +:::warning Security: Admin API Access +For security reasons, the admin port (8880) should **not be exposed** to the host machine in Docker deployments. The examples below show both CLI and Docker methods: + +**CLI Method** (when running with `aztec start` directly): + +```bash +curl -X POST http://localhost:8880 -H 'x-api-key: [ADMIN_API_KEY]' ... +``` + +**Docker Method** (when running with Docker Compose): + +```bash +docker exec -it <container-name> curl -X POST http://localhost:8880 -H 'x-api-key: [ADMIN_API_KEY]' ... +``` + +Replace `<container-name>` with your container name (e.g., `aztec-node`, `aztec-sequencer`, `prover-node`). +::: + +### aztecAdmin_getConfig + +Retrieves the configuration of this node. + +**Parameters**: None + +**Returns**: `AztecNodeAdminConfig` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' +``` + +### aztecAdmin_setConfig + +Updates the configuration of this node. + +**Parameters**: + +1. `config` - `object` - Updated configuration to be merged with the current one. + +**Returns**: `void` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_setConfig","params":[{}],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_setConfig","params":[{}],"id":1}' +``` + +### aztecAdmin_pauseSync + +Pauses archiver and world state syncing. + +**Parameters**: None + +**Returns**: `void` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_pauseSync","params":[],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_pauseSync","params":[],"id":1}' +``` + +### aztecAdmin_resumeSync + +Resumes archiver and world state syncing. + +**Parameters**: None + +**Returns**: `void` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_resumeSync","params":[],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_resumeSync","params":[],"id":1}' +``` + +### aztecAdmin_pauseSequencer + +Pauses block production. Pending txs remain in the mempool; no new blocks will be +produced until is called. Throws if no sequencer is running. + +**Parameters**: None + +**Returns**: `void` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_pauseSequencer","params":[],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_pauseSequencer","params":[],"id":1}' +``` + +### aztecAdmin_resumeSequencer + +Resumes block production previously paused via . + +**Parameters**: None + +**Returns**: `void` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_resumeSequencer","params":[],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_resumeSequencer","params":[],"id":1}' +``` + +### aztecAdmin_rollbackTo + +Pauses syncing and rolls back the database to the target L2 block number. + +**Parameters**: + +1. `targetBlockNumber` - `number` - The block number to roll back to. +2. `force` - `boolean | undefined` - If true, clears the world state db and p2p dbs if rolling back to behind the finalized block. +3. `resumeSync` - `boolean | undefined` - If true (default), resumes archiver and world state sync after rollback. + +**Returns**: `void` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_rollbackTo","params":[12345,true,true],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_rollbackTo","params":[12345,true,true],"id":1}' +``` + +### aztecAdmin_startSnapshotUpload + +Pauses syncing, creates a backup of archiver and world-state databases, and uploads them. Returns immediately. + +**Parameters**: + +1. `location` - `string` - The location to upload the snapshot to. + +**Returns**: `void` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_startSnapshotUpload","params":["0x1234..."],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_startSnapshotUpload","params":["0x1234..."],"id":1}' +``` + +### aztecAdmin_getSlashOffenses + +Returns all offenses applicable for the given round. + +**Parameters**: + +1. `round` - `bigint | 'all' | 'current'` + +**Returns**: `Offense[]` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getSlashOffenses","params":["current"],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getSlashOffenses","params":["current"],"id":1}' +``` + +### aztecAdmin_reloadKeystore + +Reloads keystore configuration from disk. + +What is updated: +- Validator attester keys +- Coinbase address per validator +- Fee recipient address per validator + +What is NOT updated (requires node restart): +- L1 publisher signers (the funded accounts that send L1 transactions) +- Prover keys +- HA signer PostgreSQL connections + +Notes: +- New validators must use a publisher key that was already configured at node + startup (or omit the publisher field to fall back to the attester key). + A validator with an unknown publisher key will cause the reload to be rejected. + +**Parameters**: None + +**Returns**: `void` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_reloadKeystore","params":[],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_reloadKeystore","params":[],"id":1}' +``` + +## Next steps + +- [How to Run a Sequencer Node](../setup/sequencer-setup.md) - Set up a node +- [Ethereum RPC Calls Reference](./ethereum-rpc-reference.md) - L1 RPC usage +- [CLI Reference](./cli-reference.md) - Command-line options +- [Aztec Discord](https://discord.gg/aztec) - Developer support diff --git a/docs/lines/v6/docs-operate/operators/reference/reading-logs.md b/docs/lines/v6/docs-operate/operators/reference/reading-logs.md new file mode 100644 index 00000000000..780608d108a --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/reference/reading-logs.md @@ -0,0 +1,108 @@ +--- +id: reading_logs +title: Reading your logs +description: A reference for common sequencer, prover, and node log messages, which are safe to ignore, and which need action. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/node-keystore/src/*", "labs/yarn-project/p2p/src/*", "labs/yarn-project/sequencer-client/src/*", "labs/yarn-project/archiver/src/*", "labs/yarn-project/prover-client/src/proving_broker/*"] +--- + +Node logs carry a mix of routine network noise and messages that need action. This page catalogues the ones operators see most often, grouped by subsystem, with a severity and what to do. + +Severity: + +- **Ignore**: routine, no action needed. +- **Investigate**: may be routine, may indicate a problem. Check the named thing before deciding. +- **Urgent**: act now. The node is not functioning correctly and rewards or stake are at risk. + +This is not an exhaustive list of every log line. It covers the messages that come up most often. If you hit something not listed here, the [Aztec Discord](https://discord.com/invite/aztec) operator channels are the best place to check. + +## Keystore + +### `ZodError: Expected object, received array` + +**Severity: Urgent.** The node will not start. + +``` +"issues": [ { "code": "invalid_type", "expected": "object", "received": "array", ... "message": "Expected object, received array" } ], +"name": "ZodError" +``` + +(usually printed twice in a row) + +A file in your keystore directory is a JSON **array**, but a keystore must be a JSON **object**, for example `{ "validators": [ ... ] }`. The usual cause is that the public staker-output file (for example `key1_staker_output.json`) was copied into the keystore directory alongside the keystore. That file is an array of validator entries meant for registration, not a keystore, so the node rejects it. + +**Action:** keep only your keystore file (for example `key1.json`) in the keystore directory. The `key1_staker_output.json` file belongs with your registration materials, not in the keystore directory. See [generating your keystore](/operate/operators/solo-sequencer/generate-keystore). + +## P2P / networking + +### `WARN ... reqresp ... err: { "code": "ERR_UNEXPECTED_EOF" }` + +**Severity: Ignore.** A peer closed a request/response stream early. This is routine churn in a peer-to-peer network and does not affect your attesting or proposing. No action. + +### `ERROR cli CodeError: no valid addresses were provided for transports [@libp2p/tcp]` + +**Severity: Urgent.** The node cannot bind its P2P transport, so it will not start. The advertised or listen address for P2P is missing or invalid. + +**Action:** set a valid P2P address in your node config and make sure the P2P port is reachable. See [the setup guide](/operate/operators/solo-sequencer/configure-environment) for the relevant variables. + +### `FATAL ... Failed to start p2p service` + +**Severity: Urgent.** The node will not start. Since v5.2.0 a p2p service that fails to start aborts node startup instead of leaving the node running with a dead libp2p stack. The usual cause is the TCP listener failing to bind: a port already in use, or an invalid `P2P_LISTEN_ADDR` / `P2P_PORT`. + +**Action:** free or change the P2P port and restart. A node that used to run past this error was peerless and silently useless, so the hard failure is the fix, not a regression. + +### `WARN ... Node has no connected peers; gossip and tx propagation are unavailable` + +**Severity: Urgent.** Your node has been at zero peers for several consecutive peer-manager heartbeats. It repeats about once a minute until peers return, then logs one info line when connectivity is restored. While peerless the node will not propose checkpoints, will not file data-withholding offenses, and rejects incoming transactions. New in v5.2.0. + +**Action:** check that your P2P port is reachable (`nc -zv <your-external-IP> 40400`) and that your advertised IP matches your external address. See [Sequencer troubleshooting](/operate/operators/concepts/sequencer-troubleshooting). + +### `WARN ... Skipping checkpoint proposal for slot N since we have too few connected peers` + +**Severity: Urgent.** You were the proposer for that slot but had fewer than `SEQ_MIN_PEERS_TO_PROPOSE` peers (default 1), so the node deliberately skipped building. New in v5.2.0. + +**Action:** same as above, fix peer connectivity. Setting `SEQ_MIN_PEERS_TO_PROPOSE=0` disables the gate but does not fix the underlying isolation. + +## Sequencer + +### `AttestationTimeoutError: Timeout collecting attestations for slot N: X/Y` + +**Severity: Investigate.** As the slot's proposer, you did not collect two-thirds of the committee's attestations in time, so that slot was missed. `X/Y` is how many attestations you collected versus how many were needed. The usual cause is that not enough of the committee's attesters were online that round. + +**Action:** every timed-out slot is a block you did not produce, so you earn no reward for it. Check your own peer count and connectivity first, since a low peer count or a slow connection can keep you from collecting attestations in time. If your node is healthy and this happens across many slots, it reflects a wider network liveness condition (not enough of the committee online) rather than a problem you can fix locally. A single miss is not slashable on its own, but persistent misses cost rewards, and missing nearly all of your duties for a full epoch contributes to inactivity. + +### `WARN ... Dropping N txs from mempool due to failures during block building for slot S` + +**Severity: Investigate.** Since v5.2.0 every transaction the block builder removes from the local mempool is logged at warn with its per-tx `reason`, alongside `slot` and `checkpointNumber`. Before this, these drops were only visible at verbose or debug level, so transactions disappeared with no recorded cause. + +**Action:** read the `failures` field for the reason. A burst of the same reason right after a reorg is expected and self-correcting. A steady trickle on a healthy chain points at transactions your node consistently rejects. + +### `WARN ... Own validator 0x... targeted by slashing vote (N of Q votes needed to slash)` + +**Severity: Urgent.** The network is voting to slash one of your validators, and `N` of the `Q` votes needed for quorum have been cast in this round. New in v5.2.0. A follow-up `Own validator 0x... was slashed for ...` is logged if the round executes. + +**Action:** react while the round is open. Confirm your node is attesting and proposing (this is almost always inactivity), then check [Slashing](/operate/operators/concepts/slashing). The same signal is available as `aztec_slasher_own_validator_current_round_votes_max` versus `aztec_slasher_quorum_size`. + +## Archiver + +### `WARN archiver:validate_trace debug_traceTransaction failed for old blocks ... historical state is not available` + +**Severity: Ignore for old blocks, but check your RPC.** At startup the node tries `debug_traceTransaction` against historical blocks your RPC has already pruned. This is routine for *old* blocks and the node proceeds. + +This call is not optional, though: the node uses `debug_traceTransaction` to read block data when a proposal's L1 transaction has a non-standard structure, and without it the node can fail to follow the chain. So you must run a debug-capable L1 execution RPC that keeps recent history (about 12 hours, roughly 3,600 blocks). If this warning appears for *recent* blocks, your RPC cannot trace and your node is at risk of getting stuck. + +**Action:** confirm your L1 execution RPC supports `debug_traceTransaction` and retains enough history. See [L1 RPC requirements](/operate/operators/concepts/l1-rpc#debug-traces) for the per-client configuration. + +## Prover + +### `PayloadTooLargeError: request entity too large limit: 1048576 (1 MB)` + +**Severity: Investigate.** A proof payload exceeded the node's request body limit, so it was rejected. Seen on prover broker and agent setups. + +**Action:** raise the body-size limit with the `RPC_MAX_BODY_SIZE` environment variable. See [Claiming prover rewards](/operate/operators/prover/claiming-rewards) and the [prover setup guide](/operate/operators/prover/setup). + +## See also + +- [L1 RPC requirements](/operate/operators/concepts/l1-rpc): execution, consensus, and debug endpoints your node needs +- [Monitoring](/operate/operators/monitoring): metrics and alerts so you see problems before the logs do +- [Slashing](/operate/operators/concepts/slashing): what actually puts stake at risk diff --git a/docs/lines/v6/docs-operate/operators/sequencer-management/claiming-rewards.md b/docs/lines/v6/docs-operate/operators/sequencer-management/claiming-rewards.md new file mode 100644 index 00000000000..f45479a8352 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/sequencer-management/claiming-rewards.md @@ -0,0 +1,217 @@ +--- +displayed_sidebar: operatorsSidebar +title: Claiming Rewards +description: Learn how to claim your sequencer rewards from the Aztec Rollup contract using cast commands. +--- + +## Overview + +Sequencer rewards accumulate in the Rollup contract but are not automatically distributed. You must manually claim them by calling the Rollup contract. This guide shows you how to check pending rewards and claim them using Foundry's `cast` command. + +## Prerequisites + +Before proceeding, you should: + +- Have a running sequencer that earned rewards (see [Sequencer Setup Guide](../setup/sequencer-setup.md)) +- Have Foundry installed with the `cast` command available ([installation guide](https://book.getfoundry.sh/getting-started/installation)) +- Know your Rollup contract address (see [Useful Commands](./useful-commands.md#get-the-rollup-contract-address)) +- Have your sequencer's coinbase address +- Have an Ethereum RPC endpoint for the network you're querying + +## Understanding Reward Claiming + +### How Rewards Accumulate + +When your sequencer proposes blocks and participates in consensus, rewards accumulate in the Rollup contract under your coinbase address. These rewards come from: + +- Block rewards distributed by the protocol +- Transaction fees from processed transactions + +Rewards are tracked per coinbase address in the Rollup contract's storage but remain in the contract until you claim them. + +### Manual vs Automatic + +Rewards are not automatically sent to your coinbase address. You must explicitly claim them by calling the `claimSequencerRewards` function on the Rollup contract. + +### Claim Requirements + +Before claiming, verify these conditions: + +1. **Rewards have accumulated**: Query your pending rewards before attempting to claim. +2. **Sufficient gas**: Ensure you have ETH to pay transaction gas costs. + +## Checking Reward Status + +### Set Up Your Environment + +For convenience, set your RPC URL as an environment variable: + +```bash +export RPC_URL="https://your-ethereum-rpc-endpoint.com" +export ROLLUP_ADDRESS="[YOUR_ROLLUP_CONTRACT_ADDRESS]" +``` + +Replace `[YOUR_ROLLUP_CONTRACT_ADDRESS]` with your actual Rollup contract address. + +### Query Your Pending Rewards + +Check accumulated rewards: + +```bash +cast call $ROLLUP_ADDRESS "getSequencerRewards(address)" [COINBASE_ADDRESS] --rpc-url $RPC_URL +``` + +Replace `[COINBASE_ADDRESS]` with your sequencer's coinbase address. + +**Example:** +```bash +# Query and convert to decimal tokens (assuming 18 decimals) +cast call $ROLLUP_ADDRESS "getSequencerRewards(address)" 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb --rpc-url $RPC_URL | cast --to-dec | cast --from-wei +# Output: 0.1 +``` + +## Claiming Your Rewards + +The `claimSequencerRewards` function is permissionless - anyone can call it for any address. Rewards are always sent to the `coinbase` address, regardless of who submits the transaction. + +### Basic Claim Command + +Use `cast send` to claim rewards: + +```bash +cast send $ROLLUP_ADDRESS \ + "claimSequencerRewards(address)" \ + [COINBASE_ADDRESS] \ + --rpc-url $RPC_URL \ + --private-key [YOUR_PRIVATE_KEY] +``` + +Replace: +- `[COINBASE_ADDRESS]` - The coinbase address whose rewards you want to claim +- `[YOUR_PRIVATE_KEY]` - The private key of the account paying for gas + +**Example:** +```bash +cast send $ROLLUP_ADDRESS \ + "claimSequencerRewards(address)" \ + 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb \ + --rpc-url $RPC_URL \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +### Using a Keystore File + +For better security, use a keystore file instead of exposing your private key: + +```bash +cast send $ROLLUP_ADDRESS \ + "claimSequencerRewards(address)" \ + [COINBASE_ADDRESS] \ + --rpc-url $RPC_URL \ + --keystore [PATH_TO_KEYSTORE] \ + --password [KEYSTORE_PASSWORD] +``` + +### Using a Hardware Wallet + +If you're using a Ledger wallet: + +```bash +cast send $ROLLUP_ADDRESS \ + "claimSequencerRewards(address)" \ + [COINBASE_ADDRESS] \ + --rpc-url $RPC_URL \ + --ledger +``` + +This will prompt you to confirm the transaction on your Ledger device. + +## Verifying Your Claim + +Check that the transaction succeeded and your pending rewards were reset to zero: + +```bash +# Check transaction succeeded (look for status: 1) +cast receipt [TRANSACTION_HASH] --rpc-url $RPC_URL + +# Verify pending rewards are now zero +cast call $ROLLUP_ADDRESS "getSequencerRewards(address)" [COINBASE_ADDRESS] --rpc-url $RPC_URL +``` + +## Troubleshooting + +### No Pending Rewards + +**Symptom**: `getSequencerRewards()` returns zero. + +**Possible causes**: +1. Your sequencer has not proposed any blocks yet +2. You already claimed all available rewards +3. Your coinbase address is configured incorrectly + +**Solutions**: +1. Verify your sequencer is active and proposing blocks (check [monitoring](../monitoring/index.md)) +2. Check your sequencer logs for block proposals +3. Verify the coinbase address in your sequencer configuration matches the address you're querying +4. Check if blocks you proposed have been proven (rewards are distributed after proof submission) + +### Transaction Fails with "Out of Gas" + +**Symptom**: Transaction reverts due to insufficient gas. + +**Solution**: +1. Increase the gas limit when sending the transaction using `--gas-limit`: + ```bash + cast send $ROLLUP_ADDRESS \ + "claimSequencerRewards(address)" \ + [COINBASE_ADDRESS] \ + --rpc-url $RPC_URL \ + --private-key [YOUR_PRIVATE_KEY] \ + --gas-limit 200000 + ``` +2. Ensure your account has sufficient ETH to cover gas costs + +### Insufficient Funds for Gas + +**Symptom**: Transaction fails because the sending account has insufficient ETH. + +**Solution**: +1. Check your account balance: + ```bash + cast balance [YOUR_ADDRESS] --rpc-url $RPC_URL + ``` +2. Send ETH to your account to cover gas costs (recommended: at least 0.005 ETH) + +### Wrong Network + +**Symptom**: Transaction fails or contract calls return unexpected results. + +**Solution**: +#if(testnet) +1. Verify your RPC URL points to the correct network (Sepolia for testnet) +#else +1. Verify your RPC URL points to the correct network (Ethereum mainnet) +#endif +2. Verify the Rollup contract address matches your target network +3. Check your account has ETH on the correct network + +## Best Practices + +**Claim Regularly**: Claim rewards periodically to reduce accumulated balances in the Rollup contract. This minimizes risk and simplifies accounting. + +**Monitor Pending Rewards**: Set up automated scripts to query pending rewards and alert you when they exceed a threshold. + +**Use Keystore Files**: Avoid exposing private keys in command history. Use keystore files or hardware wallets for production operations. + +**Verify Before Claiming**: Check pending rewards before claiming to ensure the transaction justifies the gas cost. + +**Track Claim History**: Keep records of claim transactions for accounting purposes using transaction hashes on blockchain explorers. + +**Coordinate with Delegators**: If operating with delegated stake, communicate with delegators about claiming and distribution schedules. + +## Next Steps + +- Set up [monitoring](../monitoring/index.md) to track reward accumulation automatically +- Learn about [becoming a staking provider](../setup/staking-provider.md) if operating with delegators +- Review [useful commands](./useful-commands.md) for other sequencer queries +- Join the [Aztec Discord](https://discord.gg/aztec) for operator support and community discussions diff --git a/docs/lines/v6/docs-operate/operators/sequencer-management/exiting-a-sequencer.md b/docs/lines/v6/docs-operate/operators/sequencer-management/exiting-a-sequencer.md new file mode 100644 index 00000000000..086e0a51f40 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/sequencer-management/exiting-a-sequencer.md @@ -0,0 +1,263 @@ +--- +displayed_sidebar: operatorsSidebar +title: Exiting a sequencer +description: Exit a sequencer position and withdraw its stake, either from the withdrawer or from the attester, including batched exits for many positions. +references: ["labs/yarn-project/ethereum/src/contracts/rollup.ts", "labs/yarn-project/cli/src/cmds/l1/index.ts", "labs/yarn-project/cli/src/cmds/l1/update_l1_validators.ts"] +--- + +## Overview + +Exiting removes a sequencer position from the active set and returns its stake to the position's withdrawer after a delay. There are two ways to start an exit: + +| | Withdrawer-initiated | Attester-initiated (new in v6) | +| --- | --- | --- | +| Who starts it | The withdrawer | The attester, or a relayer carrying the attester's signature | +| Batching | One transaction per position | Many positions in one relayed transaction | +| Rate limit | None | About 5% of the sequencer set per rolling window | +| Recipient | Named when the exit starts | Named by the withdrawer afterwards | +| Where the stake goes | The recipient the withdrawer names | The recipient the withdrawer names | + +Attester-initiated exits let whoever runs the node exit positions without the withdrawer's keys. This matters most for staking providers, who hold the attester keys for many delegations but not the withdrawer keys. The stake never goes to the attester: the withdrawer still decides where it goes. + +## Prerequisites + +- The aztec CLI at v6.0.0 or later (see [installation](/operate/operators/setup/running_a_node)), or Foundry's `cast`. +- The Rollup contract address for your network (see [Useful Commands](./useful-commands.md#get-the-rollup-contract-address)) and its L1 chain ID. +- An L1 RPC endpoint. +- For an attester-initiated exit: the attester's ETH private key (the ECDSA key in your keystore, not the BLS key). +- To receive the stake: access to the position's withdrawer. + +The examples use these variables: + +```bash +export ROLLUP=[ROLLUP_ADDRESS] +export L1_RPC=[L1_RPC_URL] +export L1_CHAIN_ID=[L1_CHAIN_ID] # 1 for Mainnet, 11155111 for Sepolia +export ATTESTER=[ATTESTER_ADDRESS] +``` + +## How an exit works + +Every exit goes through the same stages: + +1. **Initiate.** The position leaves the active set immediately and its stake moves into an exit record. The node can stop attesting for that key. +2. **Name the recipient.** A withdrawer-initiated exit names it in step 1. After an attester-initiated exit, the withdrawer must call `initiateWithdraw(attester, recipient)`. This does not restart any delay, and the recipient cannot be changed once set. +3. **Wait.** The stake unlocks once two delays have both passed, counted from step 1: the rollup's exit delay and the governance withdrawal delay. On Mainnet these are 4 days and about 9.6 days, so the stake is claimable about 9.6 days after the exit starts. The position can still be slashed until the rollup's exit delay ends; for the rest of the wait the stake is locked but no longer slashable. +4. **Finalize.** Anyone can call `finalizeWithdraw(attester)`. The stake is sent to the recipient. + +Read the current delays for your network: + +```bash +# Rollup exit delay, in seconds +cast call $ROLLUP "getExitDelay()(uint256)" --rpc-url $L1_RPC + +# Attester exit window, in seconds (the same value as the governance withdrawal delay) +cast call $ROLLUP "getAttesterExitWindow()(uint256)" --rpc-url $L1_RPC +``` + +## Withdrawer-initiated exit + +The withdrawer starts the exit and names the recipient in one call, sent from the withdrawer key: + +```bash +aztec remove-l1-validator --validator $ATTESTER --recipient [RECIPIENT_ADDRESS] \ + --rollup $ROLLUP --l1-rpc-urls $L1_RPC --l1-chain-id $L1_CHAIN_ID \ + --private-key [WITHDRAWER_PRIVATE_KEY] +``` + +Or with `cast`: + +```bash +cast send $ROLLUP "initiateWithdraw(address,address)" $ATTESTER [RECIPIENT_ADDRESS] \ + --private-key [WITHDRAWER_PRIVATE_KEY] --rpc-url $L1_RPC +``` + +Choose the recipient carefully: it cannot be changed once set. Then [finalize](#finalize-the-withdrawal) once the delays have passed. + +:::note Older CLI versions +Before v6.0.0, `aztec remove-l1-validator` had no `--recipient` flag and always named the attester's own address as the recipient. Do not use an older CLI to exit. +::: + +## Attester-initiated exit + +### Check the rate limit + +Attester-initiated exits are limited so that the sequencer set cannot shrink too fast: + +- Within any rolling window (`getAttesterExitWindow`, about 9.6 days on Mainnet), at most `floor((N - 1) / 20)` attester-initiated exits can happen, where `N` is the current number of sequencers. That is about 5% of the set. +- No exit may leave fewer sequencers than the target committee size (48 on Mainnet). +- Exits only work on the rollup that is both canonical and the latest in the GSE. On Mainnet, that means the v6 rollup after the v6 upgrade executes. They are not available on v5. + +Withdrawer-initiated exits do not count against the limit, but they lower `N`, so they shrink the allowance. + +Check the current state before you exit: + +```bash +cast call $ROLLUP \ + "getAttesterExitLimitState()((uint256,uint256,uint256,uint256,uint256,bool))" \ + --rpc-url $L1_RPC +``` + +The fields are `(window, validatorCount, committeeSize, used, allowance, canExit)`. `canExit` is `true` when at least one attester-initiated exit fits right now. `allowance - used` is only an upper bound on how many: each exit lowers `validatorCount`, which can lower `allowance`, and no exit may take `validatorCount` below `committeeSize`. Run the batch with `--dry-run --up-to-limit` to see how many of its authorizations fit right now. + +### Exit one position + +Send the exit from the attester key itself. The attester address needs a little ETH for gas. + +```bash +aztec initiate-withdraw-by-attester \ + --attester $ATTESTER --rollup $ROLLUP \ + --l1-rpc-urls $L1_RPC --l1-chain-id $L1_CHAIN_ID \ + --private-key [ATTESTER_PRIVATE_KEY] +``` + +`--mnemonic` with `--account-index` and `--address-index` works in place of `--private-key`. + +### Exit many positions in one transaction + +Each attester signs an exit authorization offline. One relayer then submits them all and pays the gas, so the attester addresses do not need ETH. + +1. **Pick a deadline.** Authorizations expire at a Unix timestamp you choose. Leave enough time to collect every signature and submit: + + ```bash + export DEADLINE=$(( $(date +%s) + 86400 )) # 24 hours from now + ``` + +2. **Sign one authorization per attester.** Run these one after another, not in parallel, so `--append` does not lose entries: + + ```bash + aztec sign-attester-exit --private-key [ATTESTER_0_KEY] --attester [ATTESTER_0] \ + --rollup $ROLLUP --l1-chain-id $L1_CHAIN_ID --deadline $DEADLINE \ + --output exits.json --append + + aztec sign-attester-exit --mnemonic "[VALIDATOR_MNEMONIC]" --account-index 0 --address-index 1 \ + --attester [ATTESTER_1] \ + --rollup $ROLLUP --l1-chain-id $L1_CHAIN_ID --deadline $DEADLINE \ + --output exits.json --append + ``` + + Signing needs no network access. Pass `--l1-rpc-urls` to have the CLI check the chain ID against the endpoint. With `--network mainnet` (or `testnet`) and `--l1-rpc-urls`, the CLI uses the network's current canonical rollup when you omit `--rollup`. On Mainnet, before the v6 upgrade executes, that is still the v5 rollup, so pass the v6 rollup with `--rollup` explicitly. + +3. **Simulate the batch.** A dry run needs no relayer key and reports how many authorizations would be processed: + + ```bash + aztec initiate-withdraw-by-attester-batch --dry-run --authorizations exits.json \ + --rollup $ROLLUP --l1-chain-id $L1_CHAIN_ID --l1-rpc-urls $L1_RPC + ``` + +4. **Submit it** from any funded key: + + ```bash + aztec initiate-withdraw-by-attester-batch --authorizations exits.json \ + --rollup $ROLLUP --l1-chain-id $L1_CHAIN_ID --l1-rpc-urls $L1_RPC \ + --private-key [RELAYER_PRIVATE_KEY] + ``` + + The command prints how many authorizations were processed and how many remain. + +#### Batches larger than the rate limit + +By default the batch is all or nothing: if the rate limit cannot fit every authorization, the transaction reverts. Add `--up-to-limit` to process as many authorizations as the limit allows, starting from the top of the file, and leave the rest. The command prints the zero-based indices of the entries it did not process, but it does not edit `exits.json`. Before you resubmit, remove the processed entries from the file (or write the unprocessed ones to a new file): an entry whose attester is already exiting reverts the whole batch. Submit the remainder once the window has room again and before the deadline. + +Both modes revert the whole transaction if any authorization they process is invalid: an expired deadline, a bad signature, a duplicate attester, or an attester that is already exiting or has no stake. With `--up-to-limit` the contract does not check the entries it leaves for later; the CLI still checks every signature in the file first, but not their deadlines or duplicates. Always run `--dry-run` first. + +### Name the recipient + +After an attester-initiated exit, the stake waits for the withdrawer. The withdrawer names the recipient with the same command as a withdrawer-initiated exit: + +```bash +aztec remove-l1-validator --validator $ATTESTER --recipient [RECIPIENT_ADDRESS] \ + --rollup $ROLLUP --l1-rpc-urls $L1_RPC --l1-chain-id $L1_CHAIN_ID \ + --private-key [WITHDRAWER_PRIVATE_KEY] +``` + +Or with `cast`: + +```bash +cast send $ROLLUP "initiateWithdraw(address,address)" $ATTESTER [RECIPIENT_ADDRESS] \ + --private-key [WITHDRAWER_PRIVATE_KEY] --rpc-url $L1_RPC +``` + +Do this for each exited position. It can happen any time before you finalize, it does not restart the delay, and the recipient cannot be changed once set. + +If you are a staking provider exiting delegated positions, tell your delegators: they control the withdrawer, so they must complete this step. + +## Finalize the withdrawal + +Once both delays have passed, anyone can send the stake to its recipient: + +```bash +cast send $ROLLUP "finalizeWithdraw(address)" $ATTESTER \ + --private-key [ANY_FUNDED_KEY] --rpc-url $L1_RPC +``` + +## Check an exit's status + +```bash +cast call $ROLLUP \ + "getExit(address)((uint256,uint256,uint256,address,bool,bool))" $ATTESTER \ + --rpc-url $L1_RPC +``` + +The fields are `(withdrawalId, amount, exitableAt, recipientOrWithdrawer, isRecipient, exists)`: + +- `exists` is `false`: no exit is in progress (never started, or already finalized). +- `isRecipient` is `false`: the exit was attester-initiated and `recipientOrWithdrawer` is the withdrawer, who still has to name the recipient. +- `isRecipient` is `true`: `recipientOrWithdrawer` is the recipient. Finalize after `exitableAt` (a Unix timestamp) and the governance withdrawal delay have both passed. + +## Using `cast` instead of the CLI + +The CLI commands wrap these rollup functions: + +| Function | What it does | +| --- | --- | +| `initiateWithdrawByAttester(address)` | Exit one position. Caller must be the attester. | +| `initiateWithdrawByAttesterBatch(...)` | Relay many attester-signed exits. Reverts if the rate limit cannot fit the whole batch. | +| `initiateWithdrawByAttesterBatchUpToLimit(...)` | Relay many attester-signed exits, processing only as many from the start of the list as the rate limit allows. | + +Exit a single position, signed and submitted by the attester: + +```bash +cast send $ROLLUP "initiateWithdrawByAttester(address)" $ATTESTER \ + --private-key [ATTESTER_PRIVATE_KEY] --rpc-url $L1_RPC +``` + +For a batch, sign one EIP-712 message per attester. The domain is `{ name: "Aztec Rollup", version: "1", chainId: <L1 chain id>, verifyingContract: <rollup> }` and the type is `AttesterExit(address attester,uint256 deadline)`: + +```bash +cast wallet sign --private-key [ATTESTER_PRIVATE_KEY] --data '{ + "domain":{"name":"Aztec Rollup","version":"1","chainId":'$L1_CHAIN_ID',"verifyingContract":"'$ROLLUP'"}, + "types":{"AttesterExit":[{"name":"attester","type":"address"},{"name":"deadline","type":"uint256"}]}, + "primaryType":"AttesterExit", + "message":{"attester":"'$ATTESTER'","deadline":"'$DEADLINE'"} +}' +``` + +Split each signature into `v`, `r` and `s`, then relay the batch. Each entry is `(attester, deadline, (v, r, s))`: + +```bash +cast send $ROLLUP \ + "initiateWithdrawByAttesterBatchUpToLimit((address,uint256,(uint8,bytes32,bytes32))[])" \ + "[($ATTESTER_0,$DEADLINE,($V0,$R0,$S0)),($ATTESTER_1,$DEADLINE,($V1,$R1,$S1))]" \ + --private-key [RELAYER_PRIVATE_KEY] --rpc-url $L1_RPC +``` + +## Troubleshooting + +| Error | Cause | Fix | +| --- | --- | --- | +| `Staking__AttesterExitLimitExceeded` | The rolling window has no room for this many attester-initiated exits. | Check `getAttesterExitLimitState`, then exit fewer positions, use `--up-to-limit`, or wait. | +| `Staking__AttesterExitPoolTooSmall` | The exit would leave fewer sequencers than the committee size. | Exit fewer positions, or use a withdrawer-initiated exit. | +| `Staking__NotCanonical` or `Staking__NotLatestRollup` | You are targeting a rollup that is not the canonical, latest one. | Check `--rollup`. On Mainnet, attester exits work only on the v6 rollup after the upgrade executes. | +| `Staking__AttesterExitAuthorizationExpired` | An authorization's deadline has passed. | Sign that attester again with a later deadline. | +| `Staking__AlreadyExiting` | The position already has an exit in progress. | Remove it from the batch. Check it with `getExit`. | +| `Staking__NothingToExit` | The position has no stake on this rollup. | Remove it from the batch, and check the attester address and rollup. | +| `Staking__NotAttester` | `initiate-withdraw-by-attester` was sent from a key that is not the attester. | Sign with the attester's ETH key, or use the batch flow with a relayer. | +| `Staking__InitiateWithdrawNeeded` | `finalizeWithdraw` was called before the withdrawer named a recipient. | Have the withdrawer [name the recipient](#name-the-recipient). | +| `Staking__WithdrawalNotUnlockedYet` or `Governance__WithdrawalNotUnlockedYet` | One of the two delays has not passed yet. | Wait, then finalize again. | + +## Next steps + +- [Useful Commands](./useful-commands.md) for querying your sequencer's status +- [Provider operations](../provider/provider-operations.mdx) for staking provider workflows +- [v6.0.0 changelog](../reference/changelog/v6.md) for the other changes in this release diff --git a/docs/lines/v6/docs-operate/operators/sequencer-management/governance-participation.md b/docs/lines/v6/docs-operate/operators/sequencer-management/governance-participation.md new file mode 100644 index 00000000000..9fb03dff01e --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/sequencer-management/governance-participation.md @@ -0,0 +1,421 @@ +--- +id: creating_and_voting_on_proposals +displayed_sidebar: operatorsSidebar +title: Governance and Proposal Process +description: Learn how to participate in protocol governance as a sequencer, including signaling support, creating proposals, and voting +references: ["labs/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts"] +--- + +## Overview + +This guide shows you how to participate in protocol governance as a sequencer. You'll learn how to signal support for protocol upgrades, create proposals, and vote on governance decisions that shape the Aztec network. + +:::info Conceptual Background +Before diving into the practical steps, you may want to understand the underlying concepts: +- [Governance Overview](/participate/governance) - How the governance system works +- [Proposal Lifecycle](/participate/governance/proposal-lifecycle) - The stages from signaling to execution +- [Voting](/participate/governance/voting) - How voting power and delegation work +- [GSE and Stake Mobility](/participate/governance/gse) - How your stake moves during upgrades +::: + +## Prerequisites + +Before proceeding, you should: + +- Have a running sequencer node (see [Sequencer Setup Guide](../setup/sequencer-setup.md)) +- Understand [how governance works](/participate/governance) + +## Understanding Governance Components + +### Payloads + +Protocol upgrades consist of a series of commands that execute on protocol contracts or replace contract references. You define these steps in a contract called a **payload** that you deploy on Ethereum. + +This guide assumes the payload already exists at a known address. You'll participate in the payload's journey through signaling, proposal creation, voting, and execution. + +:::warning Always Verify Payloads +Before signaling support or voting, always: +1. Verify the payload address on Etherscan or your preferred block explorer +2. Review the `getActions()` function to understand what changes the payload will make +3. Check if the payload has been audited (if applicable) +4. Discuss the proposal with the community on [Aztec Discord](https://discord.gg/aztec) + +Never signal or vote for a payload you haven't personally verified. +::: + +Here's an example payload structure: + +```solidity +contract UpgradePayload is IPayload { + IRegistry public immutable REGISTRY; + address public NEW_ROLLUP = address(new FakeRollup()); + + constructor(IRegistry _registry) { + REGISTRY = _registry; + } + + function getActions() external view override(IPayload) returns (IPayload.Action[] memory) { + IPayload.Action[] memory res = new IPayload.Action[](1); + + res[0] = Action({ + target: address(REGISTRY), + data: abi.encodeWithSelector(REGISTRY.addRollup.selector, NEW_ROLLUP) + }); + + return res; + } + + function getURI() external pure override(IPayload) returns (string memory) { + return "UpgradePayload"; + } +} +``` + +If this payload's proposal passes governance voting, the governance contract executes `addRollup` on the `Registry` contract. + +### Contract Addresses + +Key contracts you'll use: +- **Governance Proposer**: Handles payload signaling and proposal creation +- **Governance Staking Escrow (GSE)**: Manages stake delegation and voting +- **Governance**: Executes approved proposals +- **Rollup**: Your sequencer stakes here and defaults to delegating voting power here + +**To obtain these contract addresses:** Check your sequencer logs at startup for the line beginning with `INFO: node Aztec Node started on chain...` + +### Governance Lifecycle Overview + +The governance process follows these stages: + +1. **Signaling**: Sequencers signal support for a payload when proposing blocks. A payload needs a quorum of support to be promoted to a proposal. Signaling can start any time from the moment a payload is deployed. +2. **Proposal Creation**: After reaching quorum, anyone can submit the payload as an official proposal. +#if(testnet) +3. **Voting Delay** (2 minutes): A mandatory waiting period before voting opens (allows time for community review). +4. **Voting Period** (6 minutes): Users who hold stake in the network vote on the proposal using their staked tokens. +5. **Execution Delay** (2 minutes): After passing the vote, another mandatory delay before execution (allows time for node upgrades). +6. **Execution**: Anyone can execute the proposal, which applies the changes. + +**Note:** These timeline values are specific to testnet and are subject to change for future network phases. +#else +3. **Voting Delay** (3 days): A mandatory waiting period before voting opens (allows time for community review). +4. **Voting Period** (7 days): Users who hold stake in the network vote on the proposal using their staked tokens. A proposal passes if at least 20% of the total voting power votes, more than 66.5% of the votes cast are "yea", and the total voting power is at least 500 validators' worth. +5. **Execution Delay** (2 days): After passing the vote, another mandatory delay before execution (allows time for node upgrades). +6. **Execution**: Anyone can execute the proposal, which applies the changes. There is a 7-day grace period after the execution delay during which the proposal can still be executed. +#endif + +## Signaling Support for a Payload + +As a sequencer, you initiate proposals through signaling. When you propose a block, you can automatically signal support for a specific payload. Once enough sequencers signal support within a round, the payload qualifies to become an official proposal. + +### How Signaling Works + +- Only you can signal during slots when you're the block proposer +- Your sequencer node automatically calls `signal` on the `GovernanceProposer` contract when proposing a block (if you've configured a payload address) +#if(testnet) +- Rounds consist of 100 slots each (2 hours at 72 seconds per slot). At every 100-slot boundary, the system checks if any payload has received 60 or more signals (the quorum threshold, which is 60% of the round size) +- Payloads that reach quorum can be submitted as official proposals by anyone + +**Note:** These round size and quorum values apply to testnet only. +#else +- Rounds consist of 1000 slots each (20 hours at 72 seconds per slot). At every 1000-slot boundary, the system checks if any payload has received 600 or more signals (the quorum threshold, which is 60% of the round size) +- Payloads that reach quorum can be submitted as official proposals by anyone +#endif + +### Configure Your Signaling Preference + +Use the `setConfig` method on your node's admin interface to specify which payload address you want to signal support for. + +The admin API requires an API key by default. The node generates one at first startup and prints it once to the logs. See [admin API key authentication](../reference/changelog/v4.md#admin-api-key-authentication). + +#if(testnet) +**CLI Method**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{ + "jsonrpc":"2.0", + "method":"aztecAdmin_setConfig", + "params":[{"governanceProposerPayload":"0x1234567890abcdef1234567890abcdef12345678"}], + "id":1 + }' +``` + +**Docker Method**: +#endif + +```bash +docker exec -it aztec-sequencer curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{ + "jsonrpc":"2.0", + "method":"aztecAdmin_setConfig", + "params":[{"governanceProposerPayload":"0x1234567890abcdef1234567890abcdef12345678"}], + "id":1 + }' +``` + +Replace `0x1234567890abcdef1234567890abcdef12345678` with your actual payload contract address and `aztec-sequencer` with your container name. + +Expected response: +```json +{"jsonrpc":"2.0","id":1} +``` + +### Verify Your Configuration + +Use the `getConfig` method to verify the payload address: + +#if(testnet) +**CLI Method**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{ + "jsonrpc":"2.0", + "method":"aztecAdmin_getConfig", + "id":1 + }' +``` + +**Docker Method**: +#endif + +```bash +docker exec -it aztec-sequencer curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{ + "jsonrpc":"2.0", + "method":"aztecAdmin_getConfig", + "id":1 + }' +``` + +Search for `governanceProposerPayload` in the response to confirm it matches your configured address. + +Once configured, your sequencer automatically signals support for this payload each time you propose a block. Each signal counts toward the quorum requirement. + +Since v5.2.0 the node stops signalling on its own once the payload's proposal goes live or has already been executed, instead of burning roughly 100k gas per slot on a signal that can no longer do anything. You will see `Payload 0x... has a live governance proposal, stopping signals` or `Payload 0x... was executed by governance within lookback, stopping signals` in the logs. A proposal that was merely rejected, dropped, or expired is still re-signalled. For a payload deliberately designed to be executed more than once, set `GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE=true` to keep signalling anyway. + +## Creating a Proposal + +#if(testnet) +Once a payload receives the required quorum (60 signals in a 100-slot round), you or any user can call `submitRoundWinner` on the `GovernanceProposer` contract to officially create the proposal. +#else +Once a payload receives the required quorum (600 signals in a 1000-slot round), you or any user can call `submitRoundWinner` on the `GovernanceProposer` contract to officially create the proposal. +#endif + +### Submit the Payload + +```bash +cast send [GOVERNANCE_PROPOSER_ADDRESS] \ + "submitRoundWinner(uint256)" [ROUND_NUMBER] \ + --rpc-url [YOUR_RPC_URL] \ + --private-key [YOUR_PRIVATE_KEY] +``` + +To find the current round number: +```bash +# Get the current round from the GovernanceProposer contract +cast call [GOVERNANCE_PROPOSER_ADDRESS] \ + "getCurrentRound()" \ + --rpc-url [YOUR_RPC_URL] +``` + +### Verify the Created Proposal + +After creation, you can query the proposal in the governance contract: + +```bash +# Get the total proposal count +cast call [GOVERNANCE_CONTRACT_ADDRESS] \ + "proposalCount()" \ + --rpc-url [YOUR_RPC_URL] + +# Query the latest proposal (count - 1, since proposals are zero-indexed) +cast call [GOVERNANCE_CONTRACT_ADDRESS] \ + "getProposal(uint256)" $((PROPOSAL_COUNT - 1)) \ + --rpc-url [YOUR_RPC_URL] +``` + +This returns the `Proposal` struct data, which includes: +- The payload address +- Creation timestamp +- Voting start and end times +- Current vote tallies + +## Voting on Proposals + +Once a payload becomes a proposal, there's a mandatory waiting period before voting opens. You can vote in two ways: through default delegation to the rollup contract, or by delegating to an address you control for custom voting. + +### Default Voting Through the Rollup + +By default, when you stake as a sequencer, you delegate your voting power to the rollup contract through the GSE (Governance Staking Escrow). The rollup automatically votes "yea" on proposals created through the `GovernanceProposer` using **all** delegated stake from **all** sequencers in that rollup. + +**Key points:** +- If you signaled for a payload, your stake votes "yea" automatically—no additional action needed +- If you didn't signal but other sequencers did, your stake still votes "yea" when the rollup votes +- To vote differently, you must change your delegation before voting opens (see Custom Voting below) + +Anyone can trigger the rollup vote: + +```bash +cast send [ROLLUP_ADDRESS] \ + "vote(uint256)" [PROPOSAL_ID] \ + --rpc-url [YOUR_RPC_URL] \ + --private-key [YOUR_PRIVATE_KEY] +``` + +### Custom Voting: Delegating to Your Own Address + +If you want to vote differently on a proposal (for example, to vote "nay" or to split your voting power), you can delegate your stake to an address you control. This removes your stake's voting power from the rollup's control and gives it to your chosen address. + +:::warning Voting Power Timestamp +Voting power is timestamped at the moment a proposal becomes "active" (when the voting period opens). You must complete delegation **before** the voting period begins to use your voting power for that proposal. + +Check the proposal's voting start time and delegate well in advance. +::: + +#### Step 1: Delegate Your Stake + +Use the GSE contract to delegate to an address you control: + +```bash +cast send [GSE_ADDRESS] \ + "delegate(address,address,address)" \ + [ROLLUP_ADDRESS] \ + [YOUR_ATTESTER_ADDRESS] \ + [YOUR_DELEGATEE_ADDRESS] \ + --rpc-url [YOUR_RPC_URL] \ + --private-key [YOUR_WITHDRAWER_PRIVATE_KEY] +``` + +- `[ROLLUP_ADDRESS]`: The rollup contract where you staked +- `[YOUR_ATTESTER_ADDRESS]`: Your sequencer's attester address +- `[YOUR_DELEGATEE_ADDRESS]`: The address that will vote (often the same as your attester address, or another address you control) +- You must sign this transaction with your **withdrawer** private key (the withdrawer that you specified when you initially deposited to the rollup) + +#### Step 2: Vote Through GSE + +Once you've delegated to an address you control, that address can vote directly on proposals: + +```bash +# Vote "yea" with your voting power +cast send [GSE_ADDRESS] \ + "vote(uint256,uint256,bool)" \ + [PROPOSAL_ID] \ + [AMOUNT] \ + true \ + --rpc-url [YOUR_RPC_URL] \ + --private-key [YOUR_DELEGATEE_PRIVATE_KEY] +``` + +- `[AMOUNT]`: The amount of voting power to use (can be your full stake or a partial amount) +- You can vote multiple times with different amounts to split your voting power between "yea" and "nay" if desired +- To vote "nay" with your voting power, set the boolean in the code above to false + +#### Step 3: Verify Your Vote + +Check that your vote was recorded: + +```bash +# Check vote counts for a proposal +# Note: This returns the proposal's vote tallies from the Governance contract, not GSE +cast call [GOVERNANCE_CONTRACT_ADDRESS] \ + "getProposal(uint256)" [PROPOSAL_ID] \ + --rpc-url [YOUR_RPC_URL] +``` + +This returns the current "yea" and "nay" vote tallies. + +## Executing Proposals + +When a proposal receives sufficient support, it passes. After passing, there's another mandatory delay before the proposal becomes executable. Once executable, anyone can trigger execution. + +### Execute the Proposal + +Once the proposal state is Executable, anyone can execute it: + +```bash +cast send [GOVERNANCE_CONTRACT_ADDRESS] \ + "execute(uint256)" [PROPOSAL_ID] \ + --rpc-url [YOUR_RPC_URL] \ + --private-key [YOUR_PRIVATE_KEY] +``` + +After execution, the governance contract performs all actions defined in the payload. The protocol changes become effective immediately. + +### Upgrade Your Node + +**Critical**: Once a proposal executes, you must upgrade your node software to track the protocol changes. + +Monitor proposals closely from the signaling stage through execution. When a vote passes, prepare to upgrade your node software during the execution delay period, so you're ready when the proposal becomes effective. In practice, this often means running multiple nodes, with one node being on the version upgraded from, and one being on the version being upgraded to. + +:::tip Stake Mobility +If you deposited with `moveWithLatestRollup = true`, your stake automatically becomes available to the new rollup after an upgrade. If you used `false`, you'll need to manually exit and re-enter. See [GSE and Stake Mobility](/participate/governance/gse) for details. +::: + +## Troubleshooting + +### My Signal Isn't Being Recorded + +**Symptoms**: You configured a payload address, but the signal count isn't increasing. + +**Solutions**: +1. Verify you're actually proposing blocks in slots assigned to you +2. Check your node logs for errors related to governance signaling +3. Verify the payload address is correct and matches the format (0x...) +4. Confirm the `GovernanceProposer` contract address is correct for your network + +### I Can't Delegate My Voting Power + +**Symptoms**: Delegation transaction fails or reverts. + +**Solutions**: +1. Verify you're using your **withdrawer** private key, not your attester key +2. Confirm you have stake deposited in the rollup +3. Check that the addresses are correct (rollup, attester, delegatee) +4. Ensure the rollup address matches where you actually staked + +### My Vote Transaction Fails + +**Symptoms**: Vote transaction reverts or fails. + +**Solutions**: +1. Check the proposal is in the "Active" state (voting period is open) +2. Verify you delegated before the voting period started (voting power is timestamped) +3. Confirm you have sufficient voting power (check your stake amount) +4. Ensure you're not trying to vote with more power than you have +5. Check you're using the correct private key (delegatee key, not withdrawer) + +### How Do I Check When Voting Opens? + +Query the proposal to see the voting timeline: + +```bash +cast call [GOVERNANCE_CONTRACT_ADDRESS] \ + "getProposal(uint256)" [PROPOSAL_ID] \ + --rpc-url [YOUR_RPC_URL] +``` + +The returned data includes timestamps for: +- Voting start time +- Voting end time + +## Summary + +As a sequencer participating in governance: + +1. **Signal support**: Configure your node with a payload address. Your node automatically signals when proposing blocks. +2. **Vote**: Your delegated stake automatically votes "yea" on proposals created through sequencer signaling. You don't need to take additional action if you support the proposal. To vote differently, delegate your stake to an address you control before voting opens, then vote directly through the GSE contract. +3. **Upgrade promptly**: Monitor proposals and upgrade your node software after execution to stay in sync with protocol changes. + +## Next Steps + +- Learn about [sequencer setup](../setup/sequencer-setup.md) for operating your node +- Join the [Aztec Discord](https://discord.gg/aztec) to participate in governance discussions and stay informed about upcoming proposals diff --git a/docs/lines/v6/docs-operate/operators/sequencer-management/index.md b/docs/lines/v6/docs-operate/operators/sequencer-management/index.md new file mode 100644 index 00000000000..7efbd3f2220 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/sequencer-management/index.md @@ -0,0 +1,120 @@ +--- +displayed_sidebar: operatorsSidebar +id: sequencer_management_overview +title: Sequencer Management +description: Learn how to manage your sequencer operations including governance participation, delegated stake, and contract queries. +--- + +## Overview + +Once your sequencer is running, you need to manage its ongoing operations. This guide covers sequencer management tasks including participating in governance, running with delegated stake, and querying contract state to monitor your sequencer's health and performance. + +## Prerequisites + +Before proceeding, you should: + +- Have a running sequencer node (see [Sequencer Setup Guide](../setup/sequencer-setup.md)) +- Be familiar with basic sequencer operations +- Have access to Foundry's `cast` tool for contract queries +- Understand your sequencer's role in the network + +## Understanding sequencer operations + +As a sequencer operator, your responsibilities extend beyond simply running a node. You participate in network governance, manage your stake (whether self-funded or delegated), and monitor your sequencer's performance and status on the network. + +### Key management areas + +**Governance Participation**: Sequencers play a crucial role in protocol governance. You signal support for protocol upgrades, vote on proposals, and help shape the network's evolution. Active participation ensures your voice is heard in decisions that affect the protocol. + +**Stake Management**: Whether you're using your own stake or operating with delegated stake from others, you need to understand how staking works, monitor your balances, and ensure you maintain sufficient funds for operations. + +**Operational Monitoring**: Regular monitoring of your sequencer's status, performance metrics, and onchain state helps you catch issues early and maintain optimal operations. + +## What this guide covers + +This guide walks you through sequencer management in five parts: + +### 1. Governance and proposal process + +Learn how to participate in protocol governance: + +- Understanding payloads and the governance lifecycle +- Signaling support for protocol upgrades +- Creating and voting on proposals +- Executing approved changes +- Upgrading your node after governance changes + +See [Governance Participation](./governance-participation.md) for detailed instructions. + +### 2. Running as a staking provider + +If you're operating a sequencer with delegated stake: + +- Understanding the delegated stake model +- Registering as a provider with the Staking Registry +- Managing sequencer identities for delegation +- Updating provider configuration and commission rates +- Monitoring delegator relationships + +See [Becoming a Staking Provider](../setup/staking-provider.md) for setup instructions. + +### 3. Claiming rewards + +Learn how to claim your sequencer rewards: + +- Understanding how rewards accumulate in the Rollup contract +- Checking reward claimability status and pending rewards +- Claiming rewards to your coinbase address +- Troubleshooting common claiming issues + +See [Claiming Rewards](./claiming-rewards.md) for detailed instructions. + +### 4. Exiting a sequencer + +Learn how to exit a position and withdraw its stake: + +- Withdrawer-initiated and attester-initiated exits +- Batching many attester-signed exits into one transaction +- The attester exit rate limit and the withdrawal delays +- Naming the recipient and finalizing the withdrawal + +See [Exiting a sequencer](./exiting-a-sequencer.md) for detailed instructions. + +### 5. Useful commands + +Essential contract query commands for operators: + +- Finding contract addresses (Registry, Rollup, Governance) +- Querying the sequencer set and individual sequencer status +- Checking governance signals and proposal counts +- Monitoring stake balances and voting power +- Troubleshooting common query issues + +See [Useful Commands](./useful-commands.md) for a complete reference. + +## Getting started + +Start with the [Useful Commands](./useful-commands.md) guide to learn how to query your sequencer's status and verify it's operating correctly. This helps you establish a baseline for monitoring. + +If you're participating in governance, review the [Governance Participation](./governance-participation.md) guide to understand how to signal, vote, and execute proposals. + +For operators running with delegated stake, the [Becoming a Staking Provider](../setup/staking-provider.md) guide walks you through provider registration and management. + +## Best practices + +**Monitor Regularly**: Check your sequencer's status, balance, and attestation activity regularly. Set up alerts for critical thresholds like low balances or missed attestations. + +**Participate in Governance**: Stay informed about governance proposals and participate in votes that affect your operations. Join the community discussions on Discord to understand proposed changes. + +**Maintain Adequate Balances**: Ensure your publisher account always has sufficient ETH (at least 0.1 ETH) to avoid being slashed. Monitor balances and set up automated top-ups if possible. + +**Keep Your Node Updated**: When governance proposals pass that require node upgrades, prepare during the execution delay period. Have a plan for coordinated upgrades to minimize downtime. + +**Communicate with Delegators**: If you're running with delegated stake, maintain open communication with your delegators about performance, commission changes, and planned maintenance. + +## Next steps + +- Query your sequencer status using the [Useful Commands](./useful-commands.md) +- Learn about [governance participation](./governance-participation.md) to vote on protocol changes +- Set up [monitoring](../monitoring/index.md) to track your sequencer's performance +- Join the [Aztec Discord](https://discord.gg/aztec) for operator support and community discussions diff --git a/docs/lines/v6/docs-operate/operators/sequencer-management/slashing-configuration.md b/docs/lines/v6/docs-operate/operators/sequencer-management/slashing-configuration.md new file mode 100644 index 00000000000..92a5d7d6e80 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/sequencer-management/slashing-configuration.md @@ -0,0 +1,378 @@ +--- +id: slashing_and_offenses +displayed_sidebar: operatorsSidebar +title: Slashing and Offenses +description: Learn how the slashing mechanism works, what offenses are detected, and how to configure your sequencer to participate in consensus-based slashing +references: ["labs/yarn-project/slasher/src/*", "labs/yarn-project/aztec-node/src/sentinel/*", "labs/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts"] +--- + +## Overview + +This guide explains how the Aztec network's slashing mechanism works and how your sequencer automatically participates in detecting and voting on validator offenses. You'll learn about the types of offenses that are automatically detected and how to configure your sequencer's slashing behavior. + +## Prerequisites + +Before proceeding, you should: + +- Have a running sequencer node (see [Sequencer Setup Guide](../setup/sequencer-setup.md)) +- Understand that slashing actions are executed automatically when you propose blocks +- Have the Sentinel enabled if you want to detect inactivity offenses + +## Understanding the Slashing Model + +The Aztec network uses a consensus-based slashing mechanism where validators vote on individual validator offenses during block proposal. + +### How Slashing Works + +**Automatic Detection**: Your sequencer runs watchers that continuously monitor the network and automatically detect slashable offenses committed by other validators. + +**Voting Through Proposals**: Time is divided into slashing rounds (typically 128 L2 slots per round). When you propose a block during round N, your sequencer automatically votes on which validators from round N-2 should be slashed. This 2-round offset gives the network time to detect offenses before voting. + +**Vote Encoding**: Votes are encoded as bytes where each validator's vote is represented by 2 bits indicating the slash amount (0-3 slash units). The L1 contract tallies these votes and slashes validators that reach quorum. + +**Execution**: After a round ends, there's an execution delay period (approximately 3 days on mainnet, 5 hours on testnet) during which the slashing vetoer can pause execution if needed. Once the delay passes, anyone can execute the round to apply the slashing. + +### Slashing Rounds and Offsets + +``` +Round 1 (Grace Period): No voting happens +Round 2 (Grace Period): No voting happens +Round 3: Proposers vote on offenses from Round 1 (which are typically forgiven due to grace period) +Round 4: Proposers vote on offenses from Round 2 +Round N: Proposers vote on offenses from Round N-2 +``` + +**Key parameters**: +- **Round Size**: 128 L2 slots on mainnet and testnet (approximately 2.6 hours at 72 seconds per slot) +- **Slashing Offset**: 2 rounds (proposers in round N vote on offenses from round N-2) +- **Execution Delay**: 28 rounds on mainnet (~3 days), 2 rounds on testnet (~5 hours) +- **Grace Period**: First 8,400 slots on mainnet (~7 days) and 64 slots on testnet (~77 minutes) after the rollup becomes canonical (configurable per node via `SLASH_GRACE_PERIOD_L2_SLOTS`) + +### Slashing Amounts + +The L1 contract defines three fixed slashing tiers that can be configured for different offenses. These amounts are set on L1 deployment and can only be changed via governance. + +:::info Network Configuration +On mainnet, **offenses are currently configured to slash 2,000 tokens for small offenses and 5,000 tokens for medium and large offenses (1% and 2.5% of the Activation Threshold - the minimum stake required to join the validator set)**. A validator is ejected when a slash would drop its stake below the rollup's local ejection threshold (190,000 tokens on mainnet, 95% of the Activation Threshold). A validator that joined at exactly the 200,000 token Activation Threshold is therefore ejected by the first slash that would push its total slashed amount above 10,000 tokens (5% of its stake). See [Ejection from the validator set](#ejection-from-the-validator-set) for details. + +On testnet, offenses are currently configured to slash 100,000 tokens for small offenses and 250,000 tokens for medium and large offenses. With testnet's local ejection threshold of 199,000 tokens, the first slash ejects a validator that joined at the 200,000 token Activation Threshold. +::: + +## Slashable offenses + +Your sequencer automatically detects and votes to slash the following offenses. The set of offenses, and the rationale behind them, is specified in [AZIP-7](https://github.com/AztecProtocol/governance/blob/main/AZIPs/azip-7-update_slashing.md). + +### 1. Inactivity + +**What it is**: A validator fails to attest to checkpoint proposals when selected for committee duty, or fails to produce checkpoints and block proposals when selected as proposer. + +**Detection criteria**: +- Measured per epoch by the Sentinel for validators on the committee during that epoch. +- Evaluated at the end of each epoch (plus a small buffer) without waiting for the epoch to be proven on L1, so inactive validators can be slashed regardless of prover availability. +- Block re-execution is used to attribute fault between proposers and attestors based on what actually happened in each slot, rather than using attestation count as a proxy. +- A validator is considered inactive for an epoch if their failure ratio meets or exceeds `SLASH_INACTIVITY_TARGET_PERCENTAGE`. +- Requires consecutive committee participation with inactivity: must be inactive for N consecutive epochs where they were on the committee (configured via `SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD`). Epochs where the validator was not on the committee are not counted, so a validator inactive in epochs 1, 3, and 5 meets the threshold for 3 consecutive inactive epochs even though epochs 2 and 4 are skipped. + +**Note**: Requires the Sentinel to be enabled (`SENTINEL_ENABLED=true`). + +### 2. Data withholding + +**What it is**: After a checkpoint is published, the transactions it contains were not made available on the P2P network within the tolerance window. + +**Detection criteria**: +- Once `SLASH_DATA_WITHHOLDING_TOLERANCE_SLOTS` full L2 slots have elapsed past the checkpoint's slot, your node checks whether it has all the transactions for that checkpoint in its local mempool. +- If any are missing, the validators who attested to the checkpoint are flagged. +- The check runs regardless of whether the epoch is eventually proven. Slashing still applies if the data was withheld, to prevent committees from striking side deals with specific provers by releasing data only to them. + +**Responsibility**: Validators who attested to the checkpoint. + +### 3. Broadcasted invalid block proposal + +**What it is**: A proposer broadcast an invalid block proposal over the P2P network. + +**Detection criteria**: Detected by validators during proposal validation, for example when a transaction in the proposal fails validation or the proposed block header is structurally invalid. + +**Responsibility**: The proposer who broadcast the invalid block. + +### 4. Broadcasted invalid checkpoint proposal + +**What it is**: A proposer broadcast an invalid checkpoint proposal over the P2P network. This includes the AZIP-7 "submitting a block proposal after the checkpoint" case, because a later block signed by the same proposer in the same slot makes the prior checkpoint retroactively invalid. + +**Detection criteria**: Detected when the checkpoint terminates before a higher-index block proposal signed by the same proposer in the same slot, when the signed header does not match deterministic validator recomputation, or when the fee asset price modifier is malformed. + +**Responsibility**: The proposer who broadcast the invalid checkpoint. + +### 5. Proposed insufficient attestations + +**What it is**: A proposer submitted a block to L1 without collecting enough valid committee attestations. + +**Detection criteria**: +- Block published to L1 has fewer than 2/3 + 1 attestations from the committee. +- Your node detects this through L1 block validation. + +**Responsibility**: The proposer who published the block. + +### 6. Proposed incorrect attestations + +**What it is**: A proposer submitted a block with invalid signatures, or signatures from non-committee members. + +**Detection criteria**: +- Block contains attestations with invalid ECDSA signatures. +- Block contains signatures from addresses not in the committee. + +**Responsibility**: The proposer who published the block. + +### 7. Proposed descendant of checkpoint with invalid attestations + +**What it is**: A proposer published a checkpoint to L1 that builds on an earlier checkpoint with invalid or insufficient attestations. + +**Detection criteria**: +- Your node has previously identified a checkpoint as having invalid or insufficient attestations. +- A later proposer publishes a descendant checkpoint to L1 on top of it. + +**Responsibility**: The proposer of the descendant checkpoint. Under pipelining, the next proposer may have started building optimistically before the prior checkpoint's signatures were submitted to L1, so only the proposer who actually publishes the descendant checkpoint to L1 is slashed. + +### 8. Attested to invalid checkpoint proposal + +**What it is**: A committee member attested to a checkpoint proposal in a slot where your node detected a slashable invalid block proposal. + +**Detection criteria**: +- Your node detected an invalid block proposal for the slot via re-execution. +- A committee member subsequently attested to a checkpoint covering that slot. + +**Responsibility**: Committee members who attested in the invalid proposal slot. + +### 9. Duplicate proposal + +**What it is**: A proposer broadcast multiple block or checkpoint proposals for the same position with different content (equivocation). + +**Detection criteria**: Detected at the P2P layer by the AttestationPool, which tracks proposals by position (slot plus `indexWithinCheckpoint` for blocks, or slot for checkpoints). A second proposal for the same position with a different archive flags the duplicate. + +**Responsibility**: The proposer who broadcast the duplicate proposal. + +### 10. Duplicate attestation + +**What it is**: A validator signed attestations for different proposals at the same slot (equivocation). + +**Detection criteria**: Detected at the P2P layer when conflicting attestations are observed from the same signer for the same slot. + +**Responsibility**: The attestor. + +## Configuring Your Sequencer for Slashing + +The slashing module runs automatically when your sequencer is enabled. + +### Excluding Validators from Slashing + +You can configure your node to always or never slash specific validators: + +```bash +# Always slash these validators (regardless of detected offenses) +SLASH_VALIDATORS_ALWAYS=0x1234...,0x5678... + +# Never slash these validators (even if offenses are detected) +SLASH_VALIDATORS_NEVER=0xabcd...,0xef01... +``` + +**Note**: Validators in `SLASH_VALIDATORS_NEVER` take priority. If a validator appears in both lists, they won't be slashed. + +**Automatic protection**: Your own validator addresses (from your keystore) are automatically added to `SLASH_VALIDATORS_NEVER` unless you set `slashSelfAllowed=true` via the node admin API. + +### Verify Your Configuration + +Check your current slashing configuration: + +The admin API requires an API key by default. The node generates one at first startup and prints it once to the logs; pass it with the `x-api-key` header (or `Authorization: Bearer [ADMIN_API_KEY]`). See [admin API key authentication](../reference/changelog/v4.md#admin-api-key-authentication). + +**CLI Method**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{ + "jsonrpc":"2.0", + "method":"aztecAdmin_getConfig", + "id":1 + }' +``` + +**Docker Method**: + +```bash +docker exec -it aztec-sequencer curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{ + "jsonrpc":"2.0", + "method":"aztecAdmin_getConfig", + "id":1 + }' +``` + +## Automatic Slashing + +Your sequencer handles slashing automatically: + +### 1. Continuous Offense Detection + +Watchers run in the background, monitoring: +- Validator attestations and proposals via the Sentinel (when enabled) +- Invalid block and checkpoint proposals from the P2P network +- Transaction data availability after each checkpoint +- L1 block data for attestation validation +- Equivocation (duplicate proposals and attestations) on the P2P network + +### 2. Offense Storage + +When a watcher detects an offense, it's automatically stored with: +- Validator address +- Offense type +- Epoch or slot number +- Penalty amount + +Offenses are kept until they're voted on or expire after the configured number of rounds. + +### 3. Automatic Voting + +When you're selected as a block proposer: +1. Your sequencer retrieves offenses from 2 rounds ago (the slashing offset) +2. It filters out validators in your `SLASH_VALIDATORS_NEVER` list +3. It adds synthetic offenses for validators in your `SLASH_VALIDATORS_ALWAYS` list +4. Votes are encoded as a byte array, with each validator's vote represented by two bits specifying the proposed slash amount (0–3 units) +5. The votes are submitted to L1 as part of your proposal transaction + +**You don't need to take any manual action** - this happens automatically during block proposal. + +### 4. Round Execution + +When slashing rounds become executable (after the execution delay): +- Your sequencer checks if there are rounds ready to execute +- If you're the proposer and a round is ready, your node includes the execution call in your proposal +- This triggers the L1 contract to tally votes and slash validators that reached quorum + +## Understanding the Slashing Vetoer + +The slashing vetoer is an independent security group that can pause slashing to protect validators from unfair slashing due to software bugs. + +**Execution Delay**: All slashing proposals have an execution delay (28 rounds, about 3 days, on mainnet; 2 rounds, about 5 hours, on testnet) during which the vetoer can review and potentially block execution. + +**Temporary Disable**: The vetoer can disable all slashing for up to 3 days on mainnet (5 days on testnet) if needed, with the ability to extend this period. + +**Purpose**: This failsafe protects sequencers from being unfairly slashed due to client software bugs or network issues that might cause false positives in offense detection. + +## Ejection from the Validator Set + +If a slash would drop a validator's stake below the rollup's **local ejection threshold**, the validator's entire remaining stake is withdrawn instead of just the slashed amount: the slash is burned and the remainder is sent to their registered withdrawer address after the exit delay. + +**Local Ejection Threshold**: 190,000 tokens on mainnet (95% of the 200,000 token Activation Threshold). This is a per-rollup parameter, and other networks use different values (199,000 tokens on testnet). + +With mainnet's current slash amounts (2,000 tokens for small offenses, 5,000 for medium and large), a validator that joined at exactly the Activation Threshold is ejected by the first slash that would push its total slashed amount above 10,000 tokens (5% of its stake). With testnet's slash amounts (100,000 tokens for small offenses, 250,000 for medium and large), the first slash ejects such a validator. A separate protocol-level ejection threshold (100,000 tokens, 50% of the Activation Threshold) applies to all stake withdrawals at the GSE level, but with current parameters the local ejection threshold is the one that triggers ejection from slashing. + +## Monitoring Slashing Activity + +### Check Pending Offenses + +Monitor offenses your node has detected but not yet voted on by checking your node logs: + +```bash +# Look for these log messages +grep "Adding pending offense" /path/to/node/logs +grep "Voting to slash" /path/to/node/logs +``` + +### View Executed Slashing Rounds + +Your node logs when slashing rounds are executed: + +```bash +grep "Slashing round.*has been executed" /path/to/node/logs +``` + +### Query L1 Contract State + +You can query the SlashingProposer contract to see voting activity: + +```bash +# Get current round information +cast call [SLASHING_PROPOSER_ADDRESS] \ + "getCurrentRound()" \ + --rpc-url [YOUR_RPC_URL] + +# Check a specific round's vote count +cast call [SLASHING_PROPOSER_ADDRESS] \ + "getRound(uint256)" [ROUND_NUMBER] \ + --rpc-url [YOUR_RPC_URL] +``` + +## Troubleshooting + +### Slashing Module Not Running + +**Symptom**: No slashing-related logs appear in your node output. + +**Solutions**: +1. Verify your node is running as a validator (not just an observer) +2. Check that `disableValidator` is not set to `true` in your config +3. Confirm the rollup contract has a slashing proposer configured +4. Restart your node and check for errors during slasher initialization + +### Inactivity Offenses Not Detected + +**Symptom**: Your node doesn't detect inactivity offenses even when validators miss attestations. + +**Solutions**: +1. Enable the Sentinel: Set `SENTINEL_ENABLED=true` +2. Verify Sentinel is tracking data: Check logs for "Sentinel" messages +3. Ensure `SLASH_INACTIVITY_PENALTY` is greater than 0 +4. Check that `SENTINEL_HISTORY_LENGTH_IN_EPOCHS` is configured appropriately (see configuration section) +5. Remember: Validators need to be inactive for consecutive epochs (threshold: 2 by default on mainnet, 1 on testnet) + +### Own Validators Being Slashed + +**Symptom**: Your node is voting to slash your own validators. + +**Solutions**: +1. Verify that `slashSelfAllowed` is not set to `true` +2. Check that your validator addresses from the keystore are being automatically added to `SLASH_VALIDATORS_NEVER` +3. Manually add your addresses to `SLASH_VALIDATORS_NEVER` as a safeguard: + ```bash + SLASH_VALIDATORS_NEVER=0xYourAddress1,0xYourAddress2 + ``` + +### Penalty Amounts Not Matching L1 + +**Symptom**: Your configured penalties don't result in slashing on L1. + +**Solutions**: +1. Your node sums each validator's penalties per epoch and votes for the highest L1 slash amount the sum reaches, or for no slash below the smallest one. The amounts are 2,000 and 5,000 tokens on mainnet (`2000000000000000000000` and `5000000000000000000000`) and 100,000 and 250,000 tokens on testnet (`100000000000000000000000` and `250000000000000000000000`). +2. Check the penalties your node uses: the `slash...Penalty` fields of the `aztecAdmin_getConfig` response (see [Verify Your Configuration](#verify-your-configuration)). The `--network` flag sets them from the network configuration, and the node refuses to start with a conflicting `SLASH_*_PENALTY` environment variable unless `ALLOW_OVERRIDING_NETWORK_CONFIG` is `1` or `true`. + +## Best Practices + +**Enable the Sentinel**: If you want to participate in inactivity slashing, make sure `SENTINEL_ENABLED=true`. This is the only way to detect validators who go offline. + +**Use Grace Periods**: Set `SLASH_GRACE_PERIOD_L2_SLOTS` to avoid slashing validators during the initial network bootstrap period when issues are more likely. + +**Monitor Your Offenses**: Regularly check your logs to see what offenses your node is detecting and voting on. This helps you verify your slashing configuration is working as expected. + +**Don't Disable Default Protections**: Unless you explicitly want to slash your own validators, keep `slashSelfAllowed` at its default (`false`) to avoid accidentally voting against yourself. + +**Understand the Impact**: Remember that slashing is permanent and affects validators' stake. Only configure `SLASH_VALIDATORS_ALWAYS` for validators you have strong evidence of malicious behavior. + +**Stay Updated**: Monitor Aztec Discord and governance proposals for changes to slashing parameters or new offense types being added to the protocol. + +## Summary + +As a sequencer operator: + +1. **Slashing is automatic**: Your sequencer detects offenses and votes during block proposals without manual intervention +2. **Configuration is flexible**: Use environment variables or runtime API calls to adjust penalties and behavior +3. **Safety mechanisms exist**: Grace periods, vetoer controls, and automatic self-protection prevent unfair slashing +4. **Monitoring is important**: Check logs and L1 state to ensure your slasher is operating as expected + +## Next Steps + +- Review [Governance and Proposal Process](./governance-participation.md) to understand how slashing parameters can be changed +- Set up [monitoring](../monitoring/index.md) to track your sequencer's slashing activity +- Join the [Aztec Discord](https://discord.gg/aztec) to discuss slashing behavior and network health with other operators diff --git a/docs/lines/v6/docs-operate/operators/sequencer-management/useful-commands.md b/docs/lines/v6/docs-operate/operators/sequencer-management/useful-commands.md new file mode 100644 index 00000000000..9d3fa33b13b --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/sequencer-management/useful-commands.md @@ -0,0 +1,352 @@ +--- +displayed_sidebar: operatorsSidebar +title: Useful Commands +description: Essential cast commands for querying Registry, Rollup, and Governance contracts as a sequencer operator. +--- + +## Overview + +This reference provides commands for common sequencer operator tasks. You'll use Foundry's `cast` command to query onchain contract state, check sequencer status, and monitor governance processes. + +If you need help with something not covered here, visit the [Aztec Discord](https://discord.gg/aztec) in the `#operator-faq` channel. + +## Prerequisites + +Before using these commands, ensure you have: + +- **Foundry installed** with the `cast` command available ([installation guide](https://book.getfoundry.sh/getting-started/installation)) +- **Aztec CLI tool** installed (see [prerequisites guide](../prerequisites.md#aztec-toolchain)) +- **Ethereum RPC endpoint** (execution layer) for the network you're querying +- **Contract addresses** for your deployment (Registry, Rollup, Governance) + +## Getting Started + +### Set Up Your Environment + +For convenience, set your RPC URL as an environment variable: + +```bash +export RPC_URL="https://your-ethereum-rpc-endpoint.com" +``` + +All examples below use `--rpc-url $RPC_URL`. In production, always include this flag with your actual RPC endpoint. + +### Understanding Deployments + +#if(testnet) +Assume there are multiple deployments of Aztec, such as `testnet` and `ignition-testnet`. Each deployment has a unique Registry contract address that remains constant across upgrades. If a governance upgrade deploys a new rollup contract, the Registry contract address stays the same. +#else +Assume there are multiple deployments of Aztec, such as `mainnet` and `ignition-mainnet`. Each deployment has a unique Registry contract address that remains constant across upgrades. If a governance upgrade deploys a new rollup contract, the Registry contract address stays the same. +#endif + +<!-- The Registry contract for a particular deployment can be retrieved from the [Chain Info](../..link) page. --> + +### Find the Registry Contract Address + +The Registry contract is your entrypoint to all other contracts for a specific deployment. You'll need this address to discover other contract addresses. + +#if(testnet) +Contact the Aztec team or check the documentation for the Registry contract address for your target network (testnet, ignition-testnet, etc.). +#else +Contact the Aztec team or check the documentation for the Registry contract address for your target network (mainnet, ignition-mainnet, etc.). +#endif + +### Get the Rollup Contract Address + +Once you have the Registry address, retrieve the Rollup contract: + +```bash +cast call [REGISTRY_CONTRACT_ADDRESS] "getCanonicalRollup()" --rpc-url $RPC_URL +``` + +Replace `[REGISTRY_CONTRACT_ADDRESS]` with your actual Registry contract address. + +**Example:** +```bash +cast call 0x1234567890abcdef1234567890abcdef12345678 "getCanonicalRollup()" --rpc-url $RPC_URL +``` + +This returns the Rollup contract address in hexadecimal format. + +## Query the Sequencer Set + +### Get the GSE Contract Address + +The GSE (Governance Staking Escrow) contract manages sequencer registrations and balances. Get its address from the Rollup contract: + +```bash +cast call [ROLLUP_ADDRESS] "getGSE()" --rpc-url $RPC_URL +``` + +This returns the GSE contract address, which you'll need for some queries below. + +### Count Active Sequencers + +Get the total number of active sequencers in the set: + +```bash +cast call [ROLLUP_ADDRESS] "getActiveAttesterCount()" --rpc-url $RPC_URL +``` + +This returns the count of currently active sequencers as a hexadecimal number. + +### List Sequencers by Index + +Retrieve individual sequencer addresses by their index (0-based): + +```bash +cast call [ROLLUP_ADDRESS] "getAttesterAtIndex(uint256)" [INDEX] --rpc-url $RPC_URL +``` + +Replace: +- `[ROLLUP_ADDRESS]` - Your Rollup contract address +- `[INDEX]` - The index of the sequencer (starting from 0) + +**Example:** +```bash +# Get the first sequencer (index 0) +cast call 0xabcdef1234567890abcdef1234567890abcdef12 "getAttesterAtIndex(uint256)" 0 --rpc-url $RPC_URL + +# Get the second sequencer (index 1) +cast call 0xabcdef1234567890abcdef1234567890abcdef12 "getAttesterAtIndex(uint256)" 1 --rpc-url $RPC_URL +``` + +### Check Sequencer Status + +Query the complete status and information for a specific sequencer: + +```bash +cast call [ROLLUP_ADDRESS] "getAttesterView(address)" [ATTESTER_ADDRESS] --rpc-url $RPC_URL +``` + +Replace: +- `[ROLLUP_ADDRESS]` - Your Rollup contract address +- `[ATTESTER_ADDRESS]` - The sequencer's attester address you want to check + +**Example:** +```bash +cast call 0xabcdef1234567890abcdef1234567890abcdef12 "getAttesterView(address)" 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb --rpc-url $RPC_URL +``` + +### Interpret the Response + +The `getAttesterView` command returns an `AttesterView` struct containing: + +1. **status** - The sequencer's current status code (see Status Codes below) +2. **effectiveBalance** - The sequencer's effective stake balance +3. **exit** - Exit information struct (if the sequencer is exiting): + - `withdrawalId` - Withdrawal ID in the GSE contract + - `amount` - Amount being withdrawn + - `exitableAt` - Timestamp when withdrawal can be finalized + - `recipientOrWithdrawer` - Address that receives funds or can initiate withdrawal + - `isRecipient` - Whether the exit has a recipient set + - `exists` - Whether an exit exists +4. **config** - Attester configuration struct: + - `publicKey` - BLS public key (G1 point with x and y coordinates) + - `withdrawer` - Address authorized to withdraw stake + +### Get Individual Sequencer Information + +Query specific pieces of information using the GSE contract: + +```bash +# Check if a sequencer is registered +cast call [GSE_ADDRESS] "isRegistered(address,address)" [ROLLUP_ADDRESS] [ATTESTER_ADDRESS] --rpc-url $RPC_URL + +# Get sequencer's balance on this rollup instance +cast call [GSE_ADDRESS] "balanceOf(address,address)" [ROLLUP_ADDRESS] [ATTESTER_ADDRESS] --rpc-url $RPC_URL + +# Get sequencer's effective balance (includes bonus if latest rollup) +cast call [GSE_ADDRESS] "effectiveBalanceOf(address,address)" [ROLLUP_ADDRESS] [ATTESTER_ADDRESS] --rpc-url $RPC_URL + +# Get sequencer's configuration (withdrawer and public key) +cast call [ROLLUP_ADDRESS] "getConfig(address)" [ATTESTER_ADDRESS] --rpc-url $RPC_URL + +# Get only the status +cast call [ROLLUP_ADDRESS] "getStatus(address)" [ATTESTER_ADDRESS] --rpc-url $RPC_URL +``` + +### Status Codes + +| Status | Name | Meaning | +| ------ | ---- | ------- | +| 0 | NONE | The sequencer does not exist in the sequencer set | +| 1 | VALIDATING | The sequencer is currently active and participating in consensus | +| 2 | ZOMBIE | The sequencer is not active (balance fell below ejection threshold, possibly due to slashing) but still has funds in the system | +| 3 | EXITING | The sequencer has initiated withdrawal and is in the exit delay period | + +## Governance Operations + +### Get Governance Contract Addresses + +First, get the Governance contract from the Registry, then query it for the GovernanceProposer contract: + +```bash +# Get the Governance contract +cast call [REGISTRY_ADDRESS] "getGovernance()" --rpc-url $RPC_URL + +# Get the GovernanceProposer contract +cast call [GOVERNANCE_ADDRESS] "governanceProposer()" --rpc-url $RPC_URL +``` + +Replace `[REGISTRY_ADDRESS]` and `[GOVERNANCE_ADDRESS]` with your actual addresses. + +### Check Governance Quorum Requirements + +Query the quorum parameters for the governance system: + +```bash +# Get the signaling round size (in L2 slots) +cast call [GOVERNANCE_PROPOSER_ADDRESS] "ROUND_SIZE()" --rpc-url $RPC_URL + +# Get the number of signals required for quorum in any single round +cast call [GOVERNANCE_PROPOSER_ADDRESS] "QUORUM_SIZE()" --rpc-url $RPC_URL +``` + +**What these values mean:** +- **ROUND_SIZE()** - The size of any signaling round, measured in L2 slots (e.g., 1000 slots on mainnet) +- **QUORUM_SIZE()** - The number of signals needed within a round for a payload to reach quorum (e.g., 600 signals on mainnet, which is 60% of ROUND_SIZE) + +### Find the Current Round Number + +Calculate which governance round corresponds to a specific L2 slot: + +```bash +cast call [GOVERNANCE_PROPOSER_ADDRESS] "computeRound(uint256)" [SLOT_NUMBER] --rpc-url $RPC_URL +``` + +Replace: +- `[GOVERNANCE_PROPOSER_ADDRESS]` - Your GovernanceProposer contract address +- `[SLOT_NUMBER]` - The L2 slot number you want to check + +This returns the round number in hexadecimal format. Convert it to decimal for use in the next command. + +**Example:** +```bash +# Check which round slot 5000 belongs to +cast call 0x9876543210abcdef9876543210abcdef98765432 "computeRound(uint256)" 5000 --rpc-url $RPC_URL + +# Output: 0x0000000000000000000000000000000000000000000000000000000000000005 (round 5) +``` + +### Check Signal Count for a Payload + +Check how many sequencers have signaled support for a specific payload in a given round: + +```bash +cast call [GOVERNANCE_PROPOSER_ADDRESS] "signalCount(address,uint256,address)" [ROLLUP_ADDRESS] [ROUND_NUMBER] [PAYLOAD_ADDRESS] --rpc-url $RPC_URL +``` + +Replace: +- `[GOVERNANCE_PROPOSER_ADDRESS]` - Your GovernanceProposer contract address +- `[ROLLUP_ADDRESS]` - Your Rollup contract address +- `[ROUND_NUMBER]` - The round number as a decimal integer (not hex) +- `[PAYLOAD_ADDRESS]` - The address of the payload contract you're checking + +**Example:** +```bash +cast call 0x9876543210abcdef9876543210abcdef98765432 "signalCount(address,uint256,address)" 0xabcdef1234567890abcdef1234567890abcdef12 5 0x1111111111111111111111111111111111111111 --rpc-url $RPC_URL +``` + +This returns the number of signals the payload has received in that round. Compare this to the quorum threshold (QUORUM_SIZE) to determine if the payload can be promoted to a proposal. + +### Get Current Proposal Count + +Check how many governance proposals exist: + +```bash +cast call [GOVERNANCE_CONTRACT_ADDRESS] "proposalCount()" --rpc-url $RPC_URL +``` + +### Query a Specific Proposal + +Get details about a specific proposal: + +```bash +cast call [GOVERNANCE_CONTRACT_ADDRESS] "getProposal(uint256)" [PROPOSAL_ID] --rpc-url $RPC_URL +``` + +Replace: +- `[GOVERNANCE_CONTRACT_ADDRESS]` - Your Governance contract address +- `[PROPOSAL_ID]` - The proposal ID (zero-indexed, so the first proposal is 0) + +This returns the proposal struct containing: +- Payload address +- Creation timestamp +- Voting start and end times +- Current vote tallies + +## Tips and Best Practices + +### Using Etherscan + +You can also query these contracts through Etherscan's "Read Contract" interface: + +1. Navigate to the contract address on Etherscan +2. Go to the "Contract" tab +3. Click "Read Contract" or "Read as Proxy" +4. Find the function you want to call and enter parameters + +This provides a user-friendly interface without requiring command-line tools. + +### Monitoring Automation + +Consider creating scripts that regularly query sequencer status and governance signals. This helps you: +- Track your sequencer's health +- Monitor governance proposals you care about +- Receive alerts when action is needed + +### Decoding Hex Output + +Some commands return hexadecimal values. Use `cast` to convert them: + +```bash +# Convert hex to decimal +cast --to-dec 0x03e8 + +# Convert hex to address format +cast --to-address 0x000000000000000000000000742d35Cc6634C0532925a3b844Bc9e7595f0bEb +``` + +## Troubleshooting + +### "Invalid JSON RPC response" + +**Issue**: Command fails with JSON RPC error. + +**Solutions**: +- Verify your RPC endpoint is accessible and correct +#if(testnet) +- Check that you're connected to the right network (Sepolia for testnet) +#else +- Check that you're connected to the right network (Ethereum mainnet) +#endif +- Ensure your RPC provider supports the `eth_call` method +- Try a different RPC endpoint + +### "Reverted" or "Execution reverted" + +**Issue**: Contract call reverts. + +**Solutions**: +- Verify the contract address is correct +- Check that the function signature matches the contract's ABI +- Ensure you're passing the correct parameter types +- Verify the contract is deployed on the network you're querying + +### "Could not find function" + +**Issue**: Function not found in contract. + +**Solutions**: +- Verify the function name spelling and capitalization +- Check that you're querying the correct contract +- Ensure the contract version matches the function you're calling +- Try querying through Etherscan to verify the contract ABI + +## Next Steps + +- [Learn about sequencer setup](../setup/sequencer-setup.md) to operate your sequencer node +- [Participate in governance](./governance-participation.md) by signaling, voting, and creating proposals +- [Monitor your node](../monitoring/index.md) with metrics and observability tools +- Join the [Aztec Discord](https://discord.gg/aztec) for operator support and community discussions diff --git a/docs/lines/v6/docs-operate/operators/setup/blob_storage.md b/docs/lines/v6/docs-operate/operators/setup/blob_storage.md new file mode 100644 index 00000000000..5772ce4f415 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/blob_storage.md @@ -0,0 +1,213 @@ +--- +id: blob_storage +sidebar_position: 4 +title: Blob retrieval +description: Learn how Aztec nodes retrieve blob data for L1 transactions. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/blob-client/src/client/config.ts", "labs/yarn-project/blob-client/src/client/factory.ts", "labs/yarn-project/blob-client/src/filestore/*", "labs/yarn-project/blob-client/src/archive/*"] +--- + +## Overview + +Aztec uses EIP-4844 blobs to publish transaction data to Ethereum Layer 1. Since blob data is only available on L1 for a limited period (~18 days / 4,096 epochs), nodes need reliable ways to store and retrieve blob data for synchronization and historical access. + +Aztec nodes can be configured to retrieve blobs from L1 consensus (beacon nodes), file stores (S3, GCS, R2), and archive services. + +:::tip Automatic Configuration +When using `--network [NETWORK_NAME]`, blob file stores are automatically configured for you. Most users don't need to manually configure blob storage. +::: + +:::warning Override Behavior +Setting the `BLOB_FILE_STORE_URLS` environment variable overrides the file store configuration from the network config. +::: + +## Understanding blob sources + +The blob client can retrieve blobs from multiple sources, tried in order: + +1. **File Store**: Fast retrieval from configured storage (S3, GCS, R2, local files, HTTPS) +2. **L1 Consensus**: Beacon node API to a (semi-)supernode for recent blobs (within ~18 days) +3. **Archive API**: Services like Blobscan for historical blob data + +For near-tip synchronization, the client will retry file stores with backoff to handle eventual consistency when blobs are still being uploaded by other validators. + +### L1 consensus and blob availability + +If your beacon node has access to [supernodes or semi-supernodes](https://ethereum.org/roadmap/fusaka/peerdas/), L1 consensus alone may be sufficient for retrieving blobs within the ~18 day retention period. With the Fusaka upgrade and [PeerDAS (Peer Data Availability Sampling)](https://eips.ethereum.org/EIPS/eip-7594), Ethereum uses erasure coding to split blobs into 128 columns, enabling robust data availability: + +- **Supernodes** (validators with ≥4,096 ETH staked): Custody all 128 columns and all blob data for the full ~18 day retention period. These nodes form the backbone of the network and continuously heal data gaps. +- **Semi-supernodes** (validators with ≥1,824 ETH / 57 validators): Handle at least 64 columns, enabling reconstruction of complete blob data. +- **Regular nodes**: Only download 1/8th of the data (8 of 128 columns) to verify availability. This is **not sufficient** to serve complete blob data. + +:::warning Supernodes +If L1 consensus is your only blob source, your beacon node must be a supernode or semi-supernode (or connected to one) to retrieve complete blobs. A regular node cannot reconstruct full blob data from its partial columns alone. +::: + +This means that for recent blobs, configuring `L1_CONSENSUS_HOST_URLS` pointing to a well-connected supernode or semi-supernode may be all you need. However, file stores and archive APIs are still recommended for: +- Faster retrieval (file stores are typically faster than L1 consensus queries) +- Historical access (blobs older than ~18 days are pruned from L1) +- Redundancy (multiple sources improve reliability) + +## Configuring blob sources + +### Environment variables + +Configure blob sources using environment variables: + +| Variable | Description | Example | +|----------|-------------|---------| +| `BLOB_FILE_STORE_URLS` | Comma-separated URLs to read blobs from | `gs://bucket/,s3://bucket/` | +| `L1_CONSENSUS_HOST_URLS` | Beacon node URLs (comma-separated) | `https://beacon.example.com` | +| `L1_CONSENSUS_HOST_API_KEYS` | API keys for beacon nodes | `key1,key2` | +| `L1_CONSENSUS_HOST_API_KEY_HEADERS` | Header names for API keys | `Authorization` | +| `BLOB_ARCHIVE_API_URL` | Archive API URL (e.g., Blobscan) | `https://api.blobscan.com` | +| `BLOB_ALLOW_EMPTY_SOURCES` | Allow no blob sources (default: false) | `false` | +| `BLOB_PREFER_FILESTORES` | Try file stores before consensus clients (default: false) | `false` | +| `BLOB_FILE_STORE_TIMEOUT_MS` | Timeout for HTTP requests to blob file stores in ms (default: 10000) | `10000` | + +:::tip +If you want to contribute to the network by hosting a blob file store, see the [Blob upload guide](./blob_upload.md). +::: + +### Supported storage backends + +The blob client supports the same storage backends as snapshots: + +- **Google Cloud Storage** - `gs://bucket-name/path/` +- **Amazon S3** - `s3://bucket-name/path/` +- **Cloudflare R2** - `s3://bucket-name/path/?endpoint=https://[ACCOUNT_ID].r2.cloudflarestorage.com` +- **HTTP/HTTPS** (read-only) - `https://host/path` +- **Local filesystem** - `file:///absolute/path` + +### Storage path format + +Blobs are stored using the following path structure: + +``` +{base_url}/aztec-{l1ChainId}-{rollupVersion}-{rollupAddress}/blobs/{versionedBlobHash}.data +``` + +For example: +``` +gs://my-bucket/aztec-1-1-0x1234abcd.../blobs/0x01abc123...def.data +``` + +## Configuration examples + +### Basic file store configuration + +```bash +# Read blobs from GCS +BLOB_FILE_STORE_URLS=gs://my-snapshots/ +``` + +### Multiple read sources with L1 fallback + +```bash +# Try multiple sources in order +BLOB_FILE_STORE_URLS=gs://primary-bucket/,s3://backup-bucket/ + +# L1 consensus fallback +L1_CONSENSUS_HOST_URLS=https://beacon1.example.com,https://beacon2.example.com + +# Archive fallback for historical blobs +BLOB_ARCHIVE_API_URL=https://api.blobscan.com +``` + +### Cloudflare R2 configuration + +```bash +BLOB_FILE_STORE_URLS=s3://my-bucket/?endpoint=https://[ACCOUNT_ID].r2.cloudflarestorage.com +``` + +Replace `[ACCOUNT_ID]` with your Cloudflare account ID. + +### Local filesystem (for testing) + +```bash +BLOB_FILE_STORE_URLS=file:///data/blobs +``` + +## Authentication + +### Google Cloud Storage + +Set up [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials): + +```bash +gcloud auth application-default login +``` + +Or use a service account key: + +```bash +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json +``` + +### Amazon S3 / Cloudflare R2 + +Set AWS credentials as environment variables: + +```bash +export AWS_ACCESS_KEY_ID=your-access-key +export AWS_SECRET_ACCESS_KEY=your-secret-key +``` + +For R2, these credentials come from your Cloudflare R2 API tokens. + +## How blob retrieval works + +When a node needs blobs for a block, the blob client alternates between two primary sources in a retry loop, then falls back to the archive: + +1. **Primary source A** (default: L1 Consensus) - Query supernode beacon nodes using slot number +2. **Primary source B** (default: File Store) - Quick lookup in configured file stores +3. If blobs are still missing, retry with backoff (handles eventual consistency) +4. **Archive API** - Final fallback (e.g., Blobscan) + +The default order is consensus first, then file stores. Set `BLOB_PREFER_FILESTORES=true` to reverse this order if your file stores are more reliable or faster than your consensus clients. + +Non-supernode consensus hosts are automatically detected at startup and skipped during blob fetching. + +## Troubleshooting + +### No blob sources configured + +**Issue**: Node starts with warning about no blob sources. + +**Solutions**: +- Configure at least one of: `BLOB_FILE_STORE_URLS`, `L1_CONSENSUS_HOST_URLS`, or `BLOB_ARCHIVE_API_URL` +- Set `BLOB_ALLOW_EMPTY_SOURCES=true` only if you understand the implications (node may fail to sync) + +### Blob retrieval fails + +**Issue**: Node cannot retrieve blobs for a block. + +**Solutions**: +- Verify your file store URLs are accessible +- Check L1 consensus host connectivity +- Ensure authentication credentials are configured +- Try using multiple file store URLs for redundancy + +### L1 consensus host errors + +**Issue**: Cannot connect to beacon nodes. + +**Solutions**: +- Verify beacon node URLs are correct and accessible +- Check if API keys are required and correctly configured +- Ensure the beacon node is synced +- Try multiple beacon node URLs for redundancy + +## Best practices + +- **Configure multiple sources**: Use multiple file store URLs and L1 consensus hosts for redundancy +- **Use file stores for production**: File stores provide faster, more reliable blob retrieval than L1 consensus +- **Use archive API for historical access**: Configure `BLOB_ARCHIVE_API_URL` for accessing blobs older than ~18 days. Even with PeerDAS supernodes providing robust data availability, blob data is pruned from L1 after 4,096 epochs. Archive services like [Blobscan](https://blobscan.com/) store historical blob data indefinitely + +## Next Steps + +- Learn how to [host a blob file store](./blob_upload.md) to contribute to the network +- Learn about [using snapshots](./syncing_best_practices) for faster node synchronization +- Set up [monitoring](../monitoring/index.md) to track your node's blob retrieval +- Check the [CLI reference](../reference/cli-reference.md) for additional blob-related options +- Join the [Aztec Discord](https://discord.gg/aztec) for support diff --git a/docs/lines/v6/docs-operate/operators/setup/blob_upload.md b/docs/lines/v6/docs-operate/operators/setup/blob_upload.md new file mode 100644 index 00000000000..09fb814abb8 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/blob_upload.md @@ -0,0 +1,201 @@ +--- +id: blob_upload +sidebar_position: 5 +title: Blob upload +description: Learn how to host a blob file store to contribute to the Aztec network. +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/blob-client/src/client/config.ts", "labs/yarn-project/blob-client/src/filestore/filestore_blob_client.ts", "labs/yarn-project/blob-client/src/filestore/healthcheck.ts"] +--- + +## Overview + +While most nodes only need to retrieve blobs, you can contribute to the network by hosting a blob file store. When configured with an upload URL, your node will automatically upload blobs it retrieves to your file store, making them available for other nodes to download. + +:::note Upload is Optional +Configuring blob upload is optional. You can still download blobs from file stores without uploading them yourself — other network participants (such as sequencers and validators) upload blobs to shared storage, making them available for all nodes to retrieve. +::: + +## Prerequisites + +Before configuring blob upload, you should: + +- Have access to cloud storage (Google Cloud Storage, Amazon S3, or Cloudflare R2) with **write permissions** +- Understand the [blob retrieval](./blob_storage.md) configuration + +## Configuring blob upload + +### Environment variable + +Configure blob upload using the following environment variable in your `.env` file: + +| Variable | Description | Example | +|----------|-------------|---------| +| `BLOB_FILE_STORE_UPLOAD_URL` | URL for uploading blobs | `s3://my-bucket/blobs/` | + +### Supported storage backends + +The blob client supports the following storage backends for upload: + +- **Google Cloud Storage** - `gs://bucket-name/path/` +- **Amazon S3** - `s3://bucket-name/path/` +- **Cloudflare R2** - `s3://bucket-name/path/?endpoint=https://[ACCOUNT_ID].r2.cloudflarestorage.com` +- **Local filesystem** - `file:///absolute/path` + +:::warning +HTTPS URLs are read-only and cannot be used for uploads. +::: + +### Storage path format + +Blobs are stored using the following path structure: + +``` +{base_url}/aztec-{l1ChainId}-{rollupVersion}-{rollupAddress}/blobs/{versionedBlobHash}.data +``` + +For example: +``` +gs://my-bucket/aztec-1-1-0x1234abcd.../blobs/0x01abc123...def.data +``` + +## Healthcheck file + +When blob upload is configured, your node uploads a `.healthcheck` file to the storage path on startup and periodically thereafter. Other nodes use this file to verify connectivity to your file store before attempting to download blobs. + +:::warning Exclude from pruning +If you configure lifecycle rules or pruning policies on your storage bucket, ensure the `.healthcheck` file is excluded. Deleting this file will cause connectivity checks to fail on other nodes. +::: + +## Configuration examples + +### Google Cloud Storage + +```bash +BLOB_FILE_STORE_UPLOAD_URL=gs://my-bucket/blobs/ +``` + +### Amazon S3 + +```bash +BLOB_FILE_STORE_UPLOAD_URL=s3://my-bucket/blobs/ +``` + +### Cloudflare R2 + +```bash +BLOB_FILE_STORE_UPLOAD_URL=s3://my-bucket/blobs/?endpoint=https://[ACCOUNT_ID].r2.cloudflarestorage.com +``` + +Replace `[ACCOUNT_ID]` with your Cloudflare account ID. + +### Local filesystem (for testing) + +```bash +BLOB_FILE_STORE_UPLOAD_URL=file:///data/blobs +``` + +## Authentication + +Upload requires write permissions to your storage bucket. + +### Google Cloud Storage + +Set up [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials): + +```bash +gcloud auth application-default login +``` + +Or use a service account key with write permissions: + +```bash +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json +``` + +### Amazon S3 / Cloudflare R2 + +Set AWS credentials as environment variables: + +```bash +export AWS_ACCESS_KEY_ID=your-access-key +export AWS_SECRET_ACCESS_KEY=your-secret-key +``` + +For R2, these credentials come from your Cloudflare R2 API tokens. Ensure the token has write permissions. + +## Exposing a public HTTP endpoint + +While you upload blobs using SDK URLs (`gs://`, `s3://`), you should configure a public HTTP endpoint so other nodes can download blobs without needing cloud credentials. This allows anyone to add your file store as a read source using a simple HTTPS URL. + +### Google Cloud Storage + +GCS buckets can be accessed publicly at `https://storage.googleapis.com/BUCKET_NAME/path/to/object`. + +To enable public access: +1. Go to your bucket in the [Google Cloud Console](https://console.cloud.google.com/storage/browser) +2. Select the **Permissions** tab +3. Click **Grant Access** +4. Add `allUsers` as a principal with the **Storage Object Viewer** role + +See [Making data public](https://cloud.google.com/storage/docs/access-control/making-data-public) for detailed instructions. + +Once configured, other nodes can use: +```bash +BLOB_FILE_STORE_URLS=https://storage.googleapis.com/my-bucket/blobs/ +``` + +### Amazon S3 + +S3 buckets can be accessed publicly via static website hosting at `http://BUCKET_NAME.s3-website.REGION.amazonaws.com`. + +To enable public access: +1. Go to your bucket in the [AWS S3 Console](https://console.aws.amazon.com/s3/) +2. Disable **Block Public Access** settings +3. Add a bucket policy granting public read access +4. Enable **Static website hosting** in the bucket properties + +See [Hosting a static website on S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html) for detailed instructions. + +:::note +S3 website endpoints only support HTTP. For HTTPS, use [CloudFront](https://docs.aws.amazon.com/AmazonS3/latest/userguide/website-hosting-cloudfront-walkthrough.html) as a CDN in front of your bucket. +::: + +### Cloudflare R2 + +R2 buckets can expose a public HTTP endpoint via a custom domain or the managed `r2.dev` subdomain. + +To enable public access: +1. Go to your bucket in the [Cloudflare Dashboard](https://dash.cloudflare.com/) +2. Select **Settings** > **Public Access** +3. Either enable the `r2.dev` subdomain or connect a custom domain + +See [Public buckets](https://developers.cloudflare.com/r2/buckets/public-buckets/) for detailed instructions. + +Once configured, other nodes can use: +```bash +BLOB_FILE_STORE_URLS=https://pub-[ID].r2.dev/ +# or with custom domain: +BLOB_FILE_STORE_URLS=https://blobs.yourdomain.com/ +``` + +:::tip +R2 offers free egress, making it cost-effective for public blob distribution. +::: + +## Troubleshooting + +### Upload fails + +**Issue**: Blobs are not being uploaded to file store. + +**Solutions**: +- Verify `BLOB_FILE_STORE_UPLOAD_URL` is set +- Check write permissions on the storage bucket +- Ensure credentials are configured (AWS/GCP) +- Note: HTTPS URLs are read-only and cannot be used for uploads + +## Next Steps + +- Learn about [blob retrieval](./blob_storage.md) configuration +- Learn about [using snapshots](./syncing_best_practices) for faster node synchronization +- Join the [Aztec Discord](https://discord.gg/aztec) for support diff --git a/docs/lines/v6/docs-operate/operators/setup/bootnode-operation.md b/docs/lines/v6/docs-operate/operators/setup/bootnode-operation.md new file mode 100644 index 00000000000..e3a0859c743 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/bootnode-operation.md @@ -0,0 +1,238 @@ +--- +id: bootnode_operation +displayed_sidebar: operatorsSidebar +title: Using and running a bootnode +description: Learn how to connect to and operate bootnodes for peer discovery in the Aztec network. +references: ["labs/yarn-project/aztec/src/cli/cmds/start_p2p_bootstrap.ts"] +--- + +## Overview + +Bootnodes facilitate peer discovery in the Aztec network by maintaining a list of active peers that new nodes can connect to. This guide covers how to connect your node to a bootnode and how to run your own bootnode. + +## What is a bootnode? + +Nodes in the Aztec network must connect to peers to gossip transactions and propagate them across the network. Bootnodes help new nodes discover and connect to these peers, enabling them to join the peer-to-peer layer. + +## Prerequisites + +Before proceeding, you should: + +- Have the Aztec node software installed +- Understand basic command-line operations +- For running a bootnode: Have the necessary network infrastructure and port access + +## Connecting to a bootnode + +To connect your node to a bootnode for peer discovery: + +1. Obtain the bootnode's ENR (Ethereum Node Record) +#if(testnet) +2. Pass the ENR to your node at startup using the `--p2p.bootstrapNodes` flag + +The flag accepts a comma-separated list of bootstrap node ENRs: + +```bash +aztec start --node --p2p.bootstrapNodes [ENR] +``` + +For multiple bootnodes: + +```bash +aztec start --node --p2p.bootstrapNodes [ENR1],[ENR2],[ENR3] +``` +#else +2. Add the ENR to your node's `.env` file using the `BOOTSTRAP_NODES` environment variable + +The variable accepts a comma-separated list of bootstrap node ENRs: + +```bash +BOOTSTRAP_NODES=[ENR] +``` + +For multiple bootnodes: + +```bash +BOOTSTRAP_NODES=[ENR1],[ENR2],[ENR3] +``` + +Then add the environment variable to your `docker-compose.yml`: + +```yaml +environment: + # ... other environment variables + BOOTSTRAP_NODES: ${BOOTSTRAP_NODES} +``` +#endif + +## Running a bootnode + +#if(testnet) +To run your own bootnode, use the `--p2p-bootstrap` flag: + +```bash +aztec start --p2p-bootstrap +``` + +### Configuring the bootnode port + +By default, the bootnode uses the `P2P_PORT` value. To customize the port: + +```bash +aztec start --p2p-bootstrap --p2pBootstrap.p2pBroadcastPort [PORT] +``` + +### Persisting bootnode identity + +To maintain a consistent bootnode identity across restarts, use the `--p2pBootstrap.peerIdPrivateKeyPath` flag to specify a private key location: + +```bash +aztec start --p2p-bootstrap --p2pBootstrap.peerIdPrivateKeyPath [path] +``` + +**How it works:** + +- If a private key exists at `[path]`, the bootnode will use it for its identity +- If no private key exists, a new one will be generated and saved to `[path]` +- This ensures your bootnode maintains the same ENR across restarts +#else +To run your own bootnode, create a dedicated Docker Compose configuration. + +Create a `docker-compose.yml` file for your bootnode: + +```yaml +services: + aztec-bootnode: + image: "azteclabs/aztec:#release_version" + container_name: "aztec-bootnode" + ports: + - ${P2P_PORT}:${P2P_PORT} + - ${P2P_PORT}:${P2P_PORT}/udp + volumes: + - ${DATA_DIRECTORY}:/var/lib/data + environment: + P2P_PORT: ${P2P_PORT} + P2P_BROADCAST_PORT: ${P2P_BROADCAST_PORT} + PEER_ID_PRIVATE_KEY_PATH: ${PEER_ID_PRIVATE_KEY_PATH} + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --p2p-bootstrap + networks: + - aztec + restart: always + +networks: + aztec: + name: aztec +``` + +### Configuring the bootnode port + +By default, the bootnode uses the `P2P_PORT` value. To customize the port, add to your `.env` file: + +```bash +P2P_PORT=40400 +P2P_BROADCAST_PORT=[PORT] +``` + +### Persisting bootnode identity + +To maintain a consistent bootnode identity across restarts, specify a private key location in your `.env` file: + +```bash +DATA_DIRECTORY=./data +PEER_ID_PRIVATE_KEY_PATH=/var/lib/data/bootnode-peer-id +``` + +**How it works:** + +- If a private key exists at the path, the bootnode will use it for its identity +- If no private key exists, a new one will be generated and saved to that location +- This ensures your bootnode maintains the same ENR across restarts +#endif + +### Obtaining your bootnode's ENR + +After starting your bootnode, obtain its ENR from the startup logs. You can share this ENR with node operators who want to connect to your bootnode. + +### Adding your bootnode to the default set + +:::info + +The process for adding bootnodes to Aztec's default bootnode list is currently being finalized. For now, share your bootnode ENR directly with node operators who +want to connect. + +::: + +## Verification + +To verify your bootnode setup: + +### For nodes connecting to a bootnode + +1. **Check logs**: Look for messages indicating successful peer discovery +2. **Verify peer count**: Confirm your node has connected to peers from the bootnode +3. **Monitor network activity**: Ensure transactions are being gossiped correctly + +### For bootnode operators + +1. **Confirm bootnode is running**: Check that the process started successfully +2. **Verify port accessibility**: Ensure the configured port is open and accessible +3. **Monitor peer connections**: Check logs for incoming peer connection requests +4. **Validate ENR generation**: Confirm your bootnode's ENR is displayed in the logs + +## Troubleshooting + +### Cannot connect to bootnode + +**Issue**: Your node fails to connect to the specified bootnode. + +**Solutions**: +- Verify the ENR is correct and properly formatted +- Check network connectivity to the bootnode's address +- Ensure the bootnode is running and accessible +- Confirm firewall rules allow P2P connections + +### Bootnode not discovering peers + +**Issue**: Your bootnode isn't discovering or storing peers. + +**Solutions**: +#if(testnet) +- Verify the bootnode process is running with the correct flags +#else +- Verify the bootnode container is running with the correct configuration +#endif +- Check that the P2P port is properly configured and accessible +- Review logs for error messages or connection issues +- Ensure sufficient system resources are available + +### Private key path errors + +#if(testnet) +**Issue**: Errors occur when specifying `--p2pBootstrap.peerIdPrivateKeyPath`. + +**Solutions**: +- Verify the path exists and is writable +- Check file permissions for the directory and file +- Ensure the path doesn't contain invalid characters +- Confirm the private key file format is correct (if reusing an existing key) +#else +**Issue**: Errors occur when specifying the peer ID private key path. + +**Solutions**: + +- Verify the path exists and is writable within the container +- Check file permissions for the directory and file +- Ensure the volume mount is correctly configured in docker-compose.yml +- Confirm the private key file format is correct (if reusing an existing key) +#endif + +## Next Steps + +- Monitor your bootnode or node connections regularly +- Consider running multiple bootnodes for redundancy +- Join the Aztec community to share your bootnode ENR with other operators diff --git a/docs/lines/v6/docs-operate/operators/setup/building-from-source.md b/docs/lines/v6/docs-operate/operators/setup/building-from-source.md new file mode 100644 index 00000000000..3992da4a22b --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/building-from-source.md @@ -0,0 +1,325 @@ +--- +id: building_from_source +title: Building Node Software from Source +description: Build the Aztec node Docker image from source code for development, testing, or running a specific version. +displayed_sidebar: operatorsSidebar +references: ["labs/build-images/src/Dockerfile", "labs/release-image/Dockerfile", "labs/release-image/Dockerfile.base", "labs/release-image/bootstrap.sh", "labs/bootstrap.sh"] +--- + +## Overview + +This guide shows you how to build the Aztec node Docker image from source, including all build tools and dependencies. + +Building from source allows you to: + +- Run a specific tagged version +- Verify the build process matches the official CI pipeline +- Customize the software for development or testing +- Audit the complete build chain + +### Requirements + +**Hardware:** +- 4 core / 8 vCPU +- 16 GB RAM for Docker +- 150 GB free disk space +- Stable internet connection + +**Software:** +- Git to clone the repository +- Docker version 20.10 or later with at least 16 GB RAM allocated + +This guide assumes you're using a standard Linux distribution such as Debian or Ubuntu. While other operating systems may work, these instructions are tested and optimized for Linux environments. + +These requirements are for building the software. Running a node has different requirements—see [Running a Full Node](./running-a-node.md). + +## Build Steps + +### Step 1: Clone the Repository + +Clone the Aztec node repository: + +```bash +git clone https://github.com/aztec-labs-eng/aztec-node.git +cd aztec-node +``` + +### Step 2: Check Out a Version Tag + +Check out the version tag you want to build. For example, to build version #release_version: + +```bash +git checkout v#release_version +``` + +:::tip +View all available release tags with: +```bash +git tag | grep "^v[0-9]" +``` +::: + +### Step 3: Build the Container with Build Tools + +Build the container image with all necessary compilation tools: + +```bash +cd build-images/src +docker build --target build -t aztec-build-local:3.2 . +cd ../.. +``` + +:::tip +The tag `aztec-build-local:3.2` avoids conflicts with the official Docker Hub image and clearly indicates this is a locally-built version. +::: + +**What this does:** +- Builds the `build` stage from `build-images/src/Dockerfile` +- Installs Node.js 26.12.0 from NodeSource repository +- Installs Clang 16, 18, and 20 from LLVM +- Installs Rust 1.85.0 using the Rust toolchain installer with wasm32 targets +- Downloads and installs WASI SDK 34 from GitHub releases +- Installs Foundry v1.8.5 with foundryup +- Installs CMake, Ninja, and other build essentials + +:::note +This step builds all compilation tooling from scratch. The Dockerfile uses multi-stage builds—you only need the `build` target. Other targets (`devbox` and `sysbox`) are for development environments. +::: + +:::tip Verifying the Build Image +After the build completes, inspect the image to verify its contents: + +```bash +# Run a shell in the container to explore +docker run -it --rm aztec-build-local:3.2 /bin/bash + +# Check specific versions once inside: +node --version # Should show v26.12.0 +rustc --version # Should show Rust 1.85.0 +clang-20 --version # Should show clang 20.x +forge --version # Should show v1.8.5 +cmake --version # Should show cmake 3.24+ +``` + +You can review the Dockerfile at `build-images/src/Dockerfile` to see exactly what's installed and verify each step. +::: + +### Step 4: Compile the Source Code + +Run the bootstrap script inside the build container to compile all source code: + +```bash +docker run --rm \ + -v $(pwd):/workspaces/aztec-node \ + -w /workspaces/aztec-node \ + aztec-build-local:3.2 \ + ./bootstrap.sh full +``` + +**What this does:** +- Mounts your local repository into the container +- Compiles C++ code (Barretenberg proving system) +- Compiles Rust code (Noir compiler and ACVM) +- Builds TypeScript/JavaScript packages +- Writes compiled artifacts to your local filesystem (persist after container exits) +- Runs tests to verify the build + +:::note +The bootstrap process is incremental—if interrupted, restart it to resume from where it left off. Git submodules for L1 contract dependencies are initialized automatically during the build. +::: + +### Step 5: Build the Runtime Base Image + +Build the runtime base image with Node.js dependencies. This image contains only runtime requirements—no build tools or compiled code: + +```bash +docker build -f release-image/Dockerfile.base -t azteclabs/release-image-base . +``` + +:::note +The tag `azteclabs/release-image-base` must match exactly—the Dockerfile in Step 6 references this specific tag. This image is not published to Docker Hub; it exists only locally. +::: + +**What this does:** +- Installs production Node.js dependencies (no dev dependencies) +- Includes Node.js 24 runtime and system utilities +- Copies Foundry tools (anvil, cast) from the build container +- Creates a slim Ubuntu-based runtime environment without build tools + +### Step 6: Build the Final Release Image + +Build the final node image, combining the runtime environment (Step 5) with your compiled code (Step 4): + +```bash +docker build -f release-image/Dockerfile --build-arg VERSION=#release_version -t aztec-local:#release_version . +``` + +:::tip +The tag `aztec-local:#release_version` avoids conflicts with the official Docker Hub image and clearly indicates this is a locally-built version. +::: + +**Build arguments:** +- `VERSION` - Sets the version string that appears in `aztec --version` + +**What this does:** +- Starts from the `azteclabs/release-image-base` image (Step 5) +- Copies compiled source code from your local filesystem (Step 4) +- Sets up environment variables for the Barretenberg and noir-execute binaries +- Configures the entrypoint to run the Aztec node + +## Verification + +Verify your build completed successfully: + +### Check Image Exists + +```bash +docker images aztec-local +``` + +You should see your image listed: + +``` +REPOSITORY TAG IMAGE ID CREATED SIZE +aztec-local #release_version abc123def456 2 minutes ago 2.5GB +``` + +### Verify Version + +```bash +docker run --rm aztec-local:#release_version --version +``` + +Should display version #release_version. + +### Test Basic Functionality + +```bash +docker run --rm aztec-local:#release_version --help +``` + +Should display CLI help information without errors. + +If all checks pass, your image is ready to use. + +## Troubleshooting + +### Build fails with "no space left on device" + +**Issue**: Insufficient disk space. + +**Solutions**: +- Clean up unused Docker images and build cache: `docker system prune -a` +- Free up at least 150 GB of disk space +- Ensure adequate storage for intermediate build artifacts + +### Build image fails + +**Issue**: Errors during Step 3 when building `aztec-build-local:3.2`. + +**Solutions**: +- Verify you're in the `build-images/src` directory +- Ensure the `--target build` flag is specified +- Retry the build if network issues occur while downloading Rust, LLVM, or WASI SDK +- Review `build-images/src/Dockerfile` to identify the failing stage + +### Build fails with "failed to solve with frontend dockerfile.v0: failed to create LLB definition" + +**Issue**: The release image build cannot find the base image. + +**Solutions**: +- Ensure you completed Step 5 and built the base image with the exact tag: `azteclabs/release-image-base` +- Verify the base image exists locally: `docker images azteclabs/release-image-base` +- If missing, return to Step 5 and rebuild the base image + +### Bootstrap compilation fails + +**Issue**: Errors during `./bootstrap.sh` in Step 4. + +**Solutions**: +- Verify you're using the correct build image: `aztec-build-local:3.2` +- Confirm you checked out a valid release tag (not a branch) +- Retry the build—the bootstrap script is incremental and resumes where it left off +- Review error messages for specifics—missing dependencies should not occur in the build container + +### Docker runs out of memory + +**Issue**: Build crashes due to insufficient memory. + +**Solutions**: +- Increase Docker's memory limit to at least 16 GB (Docker Desktop: Settings → Resources → Memory) +- Close other applications to free system memory +- Build on a machine with more RAM if possible + +### Wrong version shows in `aztec --version` + +**Issue**: Version argument not passed correctly. + +**Solutions**: +- Ensure you used `--build-arg VERSION=X.Y.Z` when building the release image +- The version should match the git tag without the 'v' prefix (e.g., `#release_version` not `v#release_version`) + +## Using Your Custom Build + +### Running a Node + +Use your locally-built image with any node setup method. For Docker Compose, update your `docker-compose.yml`: + +```yaml +services: + aztec-node: + image: "aztec-local:#release_version" + # ... rest of configuration +``` + +See [Running a Full Node](./running-a-node.md) for complete setup instructions. + +### Using the CLI + +Run the Aztec CLI directly from your custom image: + +```bash +docker run --rm aztec-local:#release_version --version +``` + +## Alternative Approaches + +### Using Pre-built Build Image + +To save time, skip Step 3 and pull the pre-built image from Docker Hub, then tag it locally: + +```bash +docker pull aztecprotocol/build:3.2 +docker tag aztecprotocol/build:3.2 aztec-build-local:3.2 +``` + +This approach is faster but requires trusting the published image. The official image is built from the same `build-images/src/Dockerfile`. + +### Building Without Docker + +To build without Docker, install all build dependencies locally and run `./bootstrap.sh` directly: + +- Install all toolchains from the build image (Node.js 26, Rust 1.85.0, Clang 20, CMake, wasi-sdk) +- Run `bootstrap.sh check` to verify your environment +- See `build-images/README.md` for details + +Using the build container is strongly recommended to ensure a consistent, tested environment. + +## Understanding the Build Process + +The build process uses these key files in the repository: + +- **`build-images/src/Dockerfile`** - Defines the build container with all compilation tools +- **`bootstrap.sh`** - Main build script that compiles all source code (C++, Rust, TypeScript) +- **`release-image/Dockerfile.base`** - Multi-stage Dockerfile that creates a slim runtime base image +- **`release-image/Dockerfile`** - Final release image with compiled Aztec software +- **`release-image/bootstrap.sh`** - Build script used in CI for Docker images + +The official CI pipeline follows a similar process. See `.github/workflows/release.yml` for how production images are built and published. + +## Next Steps + +- Use your custom build to [run a full node](./running-a-node.md) +- Set up [monitoring](../monitoring/index.md) for your node +- Review the [CLI reference](../reference/cli-reference.md) for configuration options +- Join the [Aztec Discord](https://discord.gg/aztec) to discuss development and customization diff --git a/docs/lines/v6/docs-operate/operators/setup/high-availability.md b/docs/lines/v6/docs-operate/operators/setup/high-availability.md new file mode 100644 index 00000000000..a87c0147dfe --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/high-availability.md @@ -0,0 +1,771 @@ +--- +id: high_availability_sequencers +displayed_sidebar: operatorsSidebar +title: High Availability Sequencers +description: Learn how to run highly available sequencers across multiple nodes with database-backed coordination to prevent double-signing and ensure redundancy. +references: ["labs/yarn-project/validator-ha-signer/src/*", "labs/yarn-project/stdlib/src/ha-signing/*", "labs/yarn-project/validator-client/src/key_store/ha_key_store.ts", "labs/yarn-project/aztec/src/cli/cmds/migrate_ha_db.ts", "labs/yarn-project/cli/src/cmds/validator_keys/new.ts"] +--- + +## Overview + +This guide shows you how to set up high availability (HA) for your sequencer by running the same sequencer identity across multiple physical nodes with automatic coordination via a shared database. This configuration provides redundancy and resilience, ensuring your sequencer continues operating even if individual nodes fail. + +**What is High Availability for sequencers?** + +High availability means running multiple sequencer nodes that share the same attester identity but use different publisher addresses. The nodes coordinate through a shared PostgreSQL database to prevent double-signing across all validator duties. This allows your sequencer to: + +- Continue performing validator duties even if one node goes offline +- Maintain uptime during maintenance windows and upgrades +- Protect against infrastructure failures +- Ensure you don't miss any validator duties +- Automatically prevent double-signing & slashable actions through distributed locking + +## Prerequisites + +Before setting up HA sequencers, ensure you have: + +- Experience running a single sequencer node (see the [Sequencer Setup guide](./sequencer-setup.md)) +- Understanding of basic keystore structure and configuration +- Access to multiple servers or VMs for running separate nodes +- Ability to securely distribute keys across infrastructure +- A PostgreSQL database accessible by all HA nodes (for coordination and slashing protection) + +## How HA Signing Works + +The HA signer uses a shared PostgreSQL database to coordinate signing across multiple nodes, preventing double-signing through distributed locking: + +1. **Distributed Locking**: When a node needs to sign a duty (block proposal, checkpoint proposal, checkpoint attestation, governance vote, etc.), it first attempts to acquire a lock in the database for that specific duty (validator + slot + duty type) (+ block index within checkpoint for block proposals). + +2. **First Node Wins**: The first node to acquire the lock proceeds with signing. Other nodes receive a `DutyAlreadySignedError`, which is expected and normal in HA setups. + +3. **Slashing Protection**: If a node attempts to sign different data for the same duty, the database detects this and throws a `SlashingProtectionError`, preventing slashing conditions. + +4. **Automatic Retry**: If a node fails mid-signing (crashes, network issue), the lock is automatically cleaned up after a timeout, allowing other nodes to retry. + +5. **Background Cleanup**: The HA signer runs background tasks to clean up stuck duties (duties that were locked but never completed), ensuring the system remains healthy. + +This coordination happens automatically when `VALIDATOR_HA_SIGNING_ENABLED=true` - no manual intervention is required. + +:::warning Limitation: Post-Signature Failures +If a node successfully signs a duty but fails **after** signing (before broadcasting the signature to the network), the duty will be missed. HA signing cannot help in this scenario because the duty is already marked as "signed" in the database, preventing other nodes from retrying. This is why it's still important to have reliable infrastructure even with HA enabled - HA protects against double-signing, not against all failure modes. +::: + +## What Duties Are Protected? + +The HA signing system provides double-signing protection for all validator duties: + +### Block Production Duties + +1. **Block Proposals**: Individual block proposals built during your assigned slot. Each slot may contain multiple blocks, and each block proposal is tracked separately with its `blockIndexWithinCheckpoint` (0, 1, 2...). + +2. **Checkpoint Proposals**: The aggregated proposal submitted at the end of a slot that bundles all blocks from that slot. This is what gets submitted to L1 along with attestations. + +3. **Checkpoint Attestations**: Your validator's signature attesting to a checkpoint proposal. Validators attest to checkpoints after validating all blocks in a slot. This is the primary consensus mechanism. + +4. **Attestations and Signers**: Extended attestation format that includes additional signer information for consensus coordination. + +### Governance Duties + +5. **Governance Votes**: Signatures on governance proposals for protocol upgrades and parameter changes. HA protection ensures you don't accidentally vote twice on the same proposal. + +6. **Slashing Votes**: Signatures on votes to slash misbehaving validators. Critical for validator accountability without risking self-slashing from duplicate votes. + +## Why High Availability? + +### Benefits of HA Configuration + +**1. Redundancy and Fault Tolerance** + +If one node crashes, experiences network issues, or needs maintenance, the other node continues operating. You won't miss any validator duties during: + +- Hardware failures +- Network outages +- Planned maintenance +- Software upgrades +- Infrastructure provider issues + +**2. Improved Uptime** + +With properly configured HA, your sequencer can achieve near-perfect uptime. You can perform rolling upgrades, switching nodes in and out of service without missing duties. + +### The Core Concept + +In an HA setup: + +- **Attester identity is shared** across both nodes (same private key) +- **Publisher identity is unique** per node (different private keys) +- **Shared database coordinates signing** - prevents double-signing through distributed locking +- Both nodes run simultaneously and attempt to sign duties +- **First node wins** - the database ensures only one node signs each duty +- **Automatic failover** - if one node fails mid-signing, the other can retry +- Only one proposal is accepted per slot (enforced by L1) + +The validator client automatically integrates with the HA signer when enabled, providing distributed locking and slashing protection without manual coordination. + +## Setting Up High Availability Sequencers + +### Infrastructure Requirements + +**HA Setup (2 nodes):** + +- 2 separate servers/VMs +- Each meeting the minimum sequencer requirements (see [Sequencer Setup](./sequencer-setup.md)) +- Different physical locations or availability zones (recommended) +- Reliable network connectivity for both nodes +- Access to the same L1 infrastructure (or separate L1 endpoints) +- **PostgreSQL database** accessible by all nodes (for coordination) +- Monitoring and alerting for both nodes + +**Database Requirements:** + +- PostgreSQL 12 or later +- Network access from all validator nodes +- Sufficient connection pool capacity (default: 10 connections per node) +- Regular backups recommended for production + +### Key Management + +You'll need to generate: + +1. **One shared attester key** - Your sequencer's identity (used by both nodes) +2. **One unique publisher key per node** - For submitting proposals +3. **Secure distribution method** - For safely deploying the shared attester key + +:::warning Secure Key Distribution +The shared attester key must be distributed securely to both nodes. Consider using remote signers with: + +- Encrypted secrets management (HashiCorp Vault, AWS Secrets Manager, etc.) +- Hardware security modules (HSMs) for production deployments + +Never transmit private keys over unencrypted channels or store them in version control. +::: + +### Step 1: Generate Keys + +Generate a base keystore with multiple publishers using the Aztec CLI. This will create one attester identity with multiple publisher keys that can be distributed across your nodes. + +#if(testnet) + +```bash +# Generate base keystore with one attester and 3 publishers +aztec validator-keys new \ + --fee-recipient [YOUR_AZTEC_FEE_RECIPIENT_ADDRESS] \ + --gse-address 0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac \ + --l1-rpc-urls $ETH_RPC \ + --mnemonic "your shared mnemonic phrase for key derivation" \ + --address-index 0 \ + --publisher-count 3 \ + --data-dir ~/ha-keys-temp +``` + +#else + +```bash +# Generate base keystore with one attester and 2 publishers +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ + --gse-address 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f \ + --l1-rpc-urls $ETH_RPC \ + --mnemonic "your shared mnemonic phrase for key derivation" \ + --address-index 0 \ + --publisher-count 2 \ + --data-dir ~/ha-keys-temp +``` + +#endif + +This command generates: + +- **One attester** with both ETH and BLS keys (at derivation index 0) +- **Two publisher keys** (at derivation indices 1 and 2) +- All keys saved to `~/ha-keys-temp/key1.json` + +The output will show the complete keystore JSON with all generated keys. **Save this output securely** as you'll need to extract keys from it for each node. + +:::tip Managing Your Mnemonic +Store your mnemonic phrase securely in a password manager or hardware wallet. You'll need it to: + +- Regenerate keys if lost +- Add more publishers later +- Recover your sequencer setup + +Never commit mnemonics to version control or share them over insecure channels. +::: + +### Step 2: Fund Publisher Accounts + +Each publisher account needs ETH to pay for L1 gas when submitting proposals. You must maintain at least **0.1 ETH** in each publisher account. + +#if(testnet) +**Get Sepolia ETH from faucets:** + +- [Sepolia Faucet](https://sepoliafaucet.com/) +- [Infura Sepolia Faucet](https://www.infura.io/faucet/sepolia) +- [Alchemy Sepolia Faucet](https://sepoliafaucet.com/) + +#endif +**Check publisher balances:** + +```bash +# Check balance for Publisher 1 +cast balance [PUBLISHER_1_ADDRESS] --rpc-url $ETH_RPC + +# Check balance for Publisher 2 +cast balance [PUBLISHER_2_ADDRESS] --rpc-url $ETH_RPC +``` + +**Example:** + +```bash +cast balance 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb --rpc-url $ETH_RPC +# Output: 100000000000000000 (0.1 ETH in wei) +``` + +:::warning Balance Monitoring +Monitor these balances regularly to ensure they don't drop below 0.1 ETH. Falling below this threshold risks slashing. Consider setting up automated alerts when balances drop below 0.15 ETH. +::: + +### Step 3: Extract Keys from Generated Keystore + +Open the generated keystore file (`~/ha-keys-temp/key1.json`) and extract the keys. The file will look something like this: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xABC...123", // Shared attester ETH key + "bls": "0xDEF...456" // Shared attester BLS key + }, + "publisher": [ + "0x111...AAA", // Publisher 1 (for Node 1) + "0x222...BBB" // Publisher 2 (for Node 2) + ], + "feeRecipient": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ] +} +``` + +You'll use: + +- The **same attester keys** (both ETH and BLS) on both nodes +- A **different publisher key** for each node + +### Step 4: Create Node-Specific Keystores + +Create a separate keystore file for each node, using the same attester but different publishers: + +**Node 1 Keystore** (`~/node1/keys/keystore.json`): + +Use the same attester ETH and BLS keys, but only Publisher 1: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xABC...123", + "bls": "0xDEF...456" + }, + "publisher": ["0x111...AAA"], + "feeRecipient": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ] +} +``` + +**Node 2 Keystore** (`~/node2/keys/keystore.json`): + +Use the same attester ETH and BLS keys, but only Publisher 2: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0xABC...123", + "bls": "0xDEF...456" + }, + "publisher": ["0x222...BBB"], + "feeRecipient": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ] +} +``` + +:::warning Security Best Practice +After creating node-specific keystores, **securely delete** the base keystore file (`~/ha-keys-temp/key1.json`) that contains all publishers together. Each node should only have access to its own publisher key. +::: + +### Step 5: Deploy Keystores to Nodes + +Securely transfer each keystore to its respective node: + +```bash +# Example: Copy keystores to remote nodes via SCP +scp ~/node1/keys/keystore.json user@node1-server:~/aztec/keys/ +scp ~/node2/keys/keystore.json user@node2-server:~/aztec/keys/ +``` + +Ensure proper file permissions on each node: + +```bash +chmod 600 ~/aztec/keys/keystore.json +``` + +### Step 6: Set Up the HA Database + +Before starting your nodes, you need a PostgreSQL database that all HA nodes can access for coordination. + +**1. Provision a PostgreSQL database:** + +For production HA setups, use a managed, multi-zone database service with automatic failover. The coordination database is small (a few tables holding signing locks), so the entry tier of any managed service is enough; what matters is that the database survives the loss of a single zone or machine. + +Examples: + +- **Google Cloud SQL for PostgreSQL** with the high-availability (regional) configuration +- **DigitalOcean Managed PostgreSQL** with a standby node for automatic failover +- **AWS RDS PostgreSQL** with Multi-AZ, or **Azure Database for PostgreSQL** with zone redundancy +- **Self-hosted PostgreSQL** with streaming replication and automatic failover, only if you already operate database infrastructure across multiple sites + +Avoid running the coordination database on a single machine, including on one of the sequencer nodes themselves. If that machine goes down, both nodes stop signing and the HA pair fails together. The database must be more available than any single node it coordinates. + +:::danger Critical: All Nodes Must Connect to the Same Primary Database +The HA signing system relies on atomic database operations for distributed locking. **All validator nodes MUST connect to the SAME PRIMARY database instance**. The configurations above are safe because they use automatic failover to a single primary. + +**DO NOT use:** + +- Read replicas (replication lag breaks consistency) +- Multi-master or active-active configurations (breaks distributed locking) +- Different database instances per node (defeats the purpose of HA coordination) + +All validator nodes must use the same database connection string that points to the current primary. +::: + +The key requirements are: + +- PostgreSQL 12 or later +- **Single primary database** that all validator nodes connect to +- Network accessible from all validator nodes +- Sufficient connection pool capacity (default: 10 connections per node) +- A database created for the HA signer (e.g., `validator_ha`) +- Automatic failover is good (keeps high availability), but only one primary at a time + +**Example using psql** (if manually creating the database): + +```bash +# Connect to your PostgreSQL instance +psql -h your-db-host -U postgres + +# Create the database +CREATE DATABASE validator_ha; + +# Exit psql +\q +``` + +**2. Run database migrations:** + +The HA signer uses database migrations to set up the required tables. Run migrations **once** before starting your nodes: + +```bash +aztec migrate-ha-db up \ + --database-url postgresql://user:password@host:port/validator_ha +``` + +:::tip Migration Safety +Migrations are idempotent and safe to run concurrently, but for cleaner logs, run them once before starting nodes. You can also run migrations from an init container or separate migration job in Kubernetes. +::: + +**3. Verify the database setup:** + +Check that the required tables were created: + +```bash +# Using psql +psql postgresql://user:password@host:port/validator_ha -c "\dt" + +# Or using your cloud provider's database console +``` + +You should see tables like `validator_duties`, `schema_version` and `pmigrations`. + +### Step 7: Configure HA Signing + +Configure each node with HA signing enabled. Set these environment variables on **each node**: + +```bash +# Enable HA signing +export VALIDATOR_HA_SIGNING_ENABLED=true + +# PostgreSQL connection string (same database for all nodes) +export VALIDATOR_HA_DATABASE_URL=postgresql://user:password@host:port/validator_ha + +# Unique node identifier (different for each node) +export VALIDATOR_HA_NODE_ID=validator-node-1 # Use validator-node-2 for second node + +# Optional: Tune polling and timeout settings +export VALIDATOR_HA_POLLING_INTERVAL_MS=100 # Default: 100ms +export VALIDATOR_HA_SIGNING_TIMEOUT_MS=3000 # Default: 3000ms +``` + +**Required Environment Variables:** + +| Variable | Description | Example | +| ------------------------------ | ------------------------------------------ | ------------------------------------- | +| `VALIDATOR_HA_SIGNING_ENABLED` | Enable HA signing (required) | `true` | +| `VALIDATOR_HA_DATABASE_URL` | PostgreSQL connection string (required) | `postgresql://user:pass@host:5432/db` | +| `VALIDATOR_HA_NODE_ID` | Unique identifier for this node (required) | `validator-node-1` | + +**Optional Tuning Variables:** + +| Variable | Description | Default | +| -------------------------------------- | ------------------------------------------------ | ------------------ | +| `VALIDATOR_HA_POLLING_INTERVAL_MS` | How often to check duty status | `100` | +| `VALIDATOR_HA_SIGNING_TIMEOUT_MS` | Max wait for in-progress signing | `3000` | +| `VALIDATOR_HA_MAX_STUCK_DUTIES_AGE_MS` | Max age before cleanup | `2 * slotDuration` | +| `VALIDATOR_HA_POOL_MAX` | Max database connections | `10` | +| `VALIDATOR_HA_POOL_MIN` | Min database connections | `0` | +| `VALIDATOR_HA_OLD_DUTIES_MAX_AGE_H` | Duty retention from signing start, in hours (minimum two slots) | `24` | + + +When `VALIDATOR_HA_SIGNING_ENABLED=true`, the validator client automatically: + +- Creates an HA signer using the provided configuration +- Wraps the base keystore with `HAKeyStore` for HA-protected signing +- Coordinates signing across nodes via PostgreSQL to prevent double-signing +- Provides slashing protection to block conflicting signatures + +### Step 8: Start All Nodes + +Start each node (assuming you are using Docker Compose): + +```bash +# On each server +docker compose up -d +``` + +Ensure both nodes are configured with: + +- The same network (`--network #release_network`) +- Proper L1 endpoints +- Correct P2P configuration +- **HA signing enabled** with the same database URL +- **Unique node IDs** for each node +- Adequate resources + +## Verification and Monitoring + +### Verify Your HA Setup + +**1. Check that both nodes are running:** + +```bash +# On each server +curl http://localhost:8080/status + +# Or for Docker +docker compose logs -f aztec-sequencer +``` + +**2. Confirm nodes recognize the shared attester:** + +Check logs for messages indicating the attester address is loaded correctly. Both nodes should show the same attester address. + +**3. Verify HA signer is active:** + +Look for log messages indicating HA signer initialization: + +``` +HAKeyStore initialized { nodeId: 'validator-node-1' } +``` + +**4. Verify different publishers:** + +Each node's logs should show a different publisher address being used for submitting transactions. + +**5. Monitor attestations:** + +Watch L1 for attestations from your sequencer's attester address. You should see attestations being submitted even if one node goes offline. + +**6. Check database coordination:** + +Query the database to see which node signed recent duties: + +```sql +SELECT + validator_address, + slot, + duty_type, + node_id, + status, + started_at +FROM validator_duties +ORDER BY started_at DESC +LIMIT 10; +``` + +You should see duties distributed across both nodes, with only one node signing each duty. + +**7. Check duty type distribution:** + +View the distribution of different duty types across your nodes: + +```sql +SELECT + duty_type, + node_id, + COUNT(*) as duty_count, + COUNT(CASE WHEN status = 'signed' THEN 1 END) as signed_count +FROM validator_duties +WHERE started_at > NOW() - INTERVAL '1 hour' +GROUP BY duty_type, node_id +ORDER BY duty_type, node_id; +``` + +This helps verify that both nodes are handling all types of validator duties (block proposals, checkpoint proposals, attestations, votes, etc.). + +### Testing Failover + +To verify HA is working correctly: + +1. **Monitor baseline**: Note the duty completion rate with both nodes running +2. **Check database**: Verify both nodes are signing duties (query `validator_duties` table) +3. **Stop one node**: `docker compose down` on one server +4. **Verify continuity**: Check that the remaining node continues handling all validator duties +5. **Check logs**: The remaining node should show normal operation without errors +6. **Monitor database**: The remaining node should continue signing all duty types +7. **Restart the stopped node**: Verify it rejoins seamlessly and resumes signing + +If validator duties stop when you stop one node, check: + +- Database connectivity from the remaining node +- HA signing is enabled (`VALIDATOR_HA_SIGNING_ENABLED=true`) +- Node ID is correctly configured +- Database migrations were run successfully + +## Signing history and rollup upgrades + +Signing records are isolated by rollup address. With the signing-history retention fix, restarting a node preserves +records for other rollups sharing its PostgreSQL database. This allows replicas serving different rollups to retain +their own double-signing protection during an upgrade. + +:::warning Older replicas sharing the database +Older node versions delete signing records for other rollups on startup. Every replica sharing the database must +include the retention fix before you rely on this protection during a rollup transition. Updating only the replica +serving the new rollup does not prevent an older replica from deleting its records on restart. +::: + +By default, duties are retained for 24 hours, measured from when signing started. Retention is clamped to at least two +full slot durations, covering the rest of the current slot and the next slot. The validator reads the slot duration +from the rollup contract. + +Background cleanup runs at startup and approximately hourly, removing old records across all rollups in the shared +database, including unfinished duties abandoned by nodes that never return. A replica serving the new rollup can +therefore clean up records for a retired rollup. Cleanup of the node's own stuck, unfinished signing duties uses a +separate timeout. + +Set `VALIDATOR_HA_OLD_DUTIES_MAX_AGE_H` to a positive number of hours to override the default, using the same value on +every replica sharing the database. Changes apply to existing records as well as new ones. No additional database +migration is required for this retention policy. + +## Operational Best Practices + +### Load Balancing L1 Access + +If possible, configure each node with its own L1 infrastructure: + +- **Node 1**: L1 endpoints in Region A +- **Node 2**: L1 endpoints in Region B + +This protects against L1 provider outages affecting both nodes simultaneously. + +### Geographic Distribution + +For maximum resilience, distribute nodes across: + +- Multiple data centers +- Different cloud providers +- Different geographic regions +- Different network availability zones + +This protects against regional failures, provider outages, and network issues. + +### Regular Testing + +Periodically test your HA setup: + +- Simulate node failures (stop nodes intentionally) +- Test network partitions (firewall rules) +- Test database connectivity issues (temporarily block database access) +- Verify monitoring and alerting +- Practice recovery procedures +- Test rolling upgrades +- Verify database cleanup of stuck duties + +### Production Deployment Considerations + +**Database High Availability:** + +For production, your coordination database should also be highly available: + +- Use a managed PostgreSQL service (AWS RDS, Google Cloud SQL, Azure Database) with automatic failover +- Enable automatic failover to standby replicas (single primary with hot standby) +- **Do not use read replicas** for HA signing connections (all nodes must connect to primary) +- Configure connection pooling appropriately (`VALIDATOR_HA_POOL_MAX`) +- Monitor database performance and connection counts +- Set up database backups and point-in-time recovery +- Ensure all validator nodes use the same connection string pointing to the primary + +**Migration Strategy:** + +Run database migrations before deploying new validator nodes: + +```bash +# Option 1: Run migrations in CI/CD pipeline +aztec migrate-ha-db up --database-url $VALIDATOR_HA_DATABASE_URL + +# Option 2: Use Kubernetes init container (see validator-ha-signer README) +# Option 3: Use separate migration job +``` + +**Monitoring:** + +Monitor these key metrics: + +- Database connection pool usage +- Signing success/failure rates per node and per duty type +- `DutyAlreadySignedError` frequency (expected in HA) +- Database query latency +- Stuck duty cleanup frequency +- Distribution of duty types across nodes (should be relatively even over time) + +## Troubleshooting + +### Both Nodes Stopped Performing Duties + +**Issue**: No attestations, proposals, or other validator duties from either node. + +**Solutions**: + +- Verify both nodes aren't simultaneously offline +- Check L1 connectivity from each node +- Verify the shared attester key is correct in both keystores +- Check that the sequencer is still registered and active on L1 +- Review logs for errors on both nodes +- **Verify database connectivity** - check that both nodes can connect to PostgreSQL +- **Check HA signing is enabled** - verify `VALIDATOR_HA_SIGNING_ENABLED=true` on both nodes +- **Review database logs** - check for connection errors or timeouts +- **Query validator_duties table** - check if duties are being attempted but failing + +### Database Connection Issues + +**Issue**: Nodes can't connect to the database or signing fails with database errors. + +**Solutions**: + +- Verify database is running and accessible from both nodes +- Check network connectivity: `psql $VALIDATOR_HA_DATABASE_URL -c "SELECT 1;"` +- Verify connection string format: `postgresql://user:password@host:port/database` +- Check firewall rules allow connections from validator nodes +- Verify database credentials are correct +- Check connection pool limits (increase `VALIDATOR_HA_POOL_MAX` if needed) +- Review database logs for connection errors + +### Duplicate Signatures Appearing + +**Issue**: Seeing duplicate signatures for the same duty (proposals, attestations, votes) from your sequencer. + +**Solutions**: + +- Verify each node has a unique publisher key +- Check that publisher keys aren't duplicated across keystores +- Ensure nodes aren't sharing the same keystore file +- Review keystore configuration on each node +- **Verify HA signing is enabled** - duplicate signatures shouldn't occur with HA enabled +- **Check database configuration** - see "Incorrect Database Configuration" below +- **Check database** - query `validator_duties` to see if both nodes attempted to sign the same duty +- **Review logs** for `DutyAlreadySignedError` (expected) or `SlashingProtectionError` (indicates issue) +- **Check duty type** - different duty types (block proposals vs checkpoint proposals vs attestations) should be tracked separately + +### Incorrect Database Configuration + +**Issue**: Duplicate signatures despite HA being enabled, or inconsistent behavior across nodes. + +**Root Cause**: Nodes may be connecting to different database instances or read replicas instead of the same primary database. + +**Solutions**: + +- **Verify all nodes use the same connection string** - check `VALIDATOR_HA_DATABASE_URL` on all nodes +- **Confirm connecting to primary** - ensure connection string points to the primary database, not a read replica +- **Check for multi-master setup** - multi-master or active-active database configurations will break distributed locking +- **Test database connectivity** - from each node, run: + ```bash + psql $VALIDATOR_HA_DATABASE_URL -c "SELECT pg_is_in_recovery();" + ``` + Should return `f` (false) for all nodes, indicating connection to the primary +- **Review database failover events** - if using managed services, check if recent failover caused connection issues +- **Verify no load balancing to replicas** - ensure database connection pooling or load balancers don't route to read replicas + +:::danger Critical +If nodes connect to different database instances or read replicas, the distributed locking will fail and you **will** double-sign, leading to slashing. All nodes must connect to the same primary database. +::: + +### One Node Not Contributing + +**Issue**: One node running but not performing validator duties. + +**Solutions**: + +- Check that node's sync status +- Verify keystore is loaded correctly +- Check network connectivity to L1 +- Review logs for specific errors +- Confirm publisher account has sufficient ETH +- **Verify HA configuration** - check `VALIDATOR_HA_SIGNING_ENABLED`, `VALIDATOR_HA_DATABASE_URL`, and `VALIDATOR_HA_NODE_ID` +- **Check database** - query to see if the node is attempting to sign duties +- **Review logs for HA errors** - look for `DutyAlreadySignedError` (normal) or database connection errors +- **Verify node ID is unique** - both nodes must have different `VALIDATOR_HA_NODE_ID` values +- **Check duty distribution** - use the duty type distribution query from the verification section + +### Keystore Loading Failures + +**Issue**: Node fails to load the keystore. + +**Solutions**: + +- Verify keystore.json syntax is valid +- Check file permissions (readable by the node process) +- Ensure the keystore path is correct +- Validate all private keys are properly formatted +- Review the [Keystore Troubleshooting guide](../keystore/troubleshooting.md) + +### Database Migration Issues + +**Issue**: Migrations fail or nodes can't start due to missing tables. + +**Solutions**: + +- Verify migrations were run: `aztec migrate-ha-db up --database-url $VALIDATOR_HA_DATABASE_URL` +- Check database permissions - the user needs CREATE TABLE privileges +- Review migration logs for specific errors +- Verify database version is PostgreSQL 12 or later +- Check that the `validator_duties`, `schema_version` and `pmigrations` (created by node-pg-migrate) tables exist + +## Related Guides + +:::tip Running Multiple Sequencers Per Node +Want to run multiple sequencer identities on a **single node** instead? See the [Advanced Keystore Patterns guide](../keystore/advanced-patterns.md#multiple-sequencers)—that's a different use case from HA. +::: + +## Next Steps + +- Review the [Advanced Keystore Patterns guide](../keystore/advanced-patterns.md) for multiple sequencers per node +- Set up [monitoring and observability](../monitoring/index.md) for your HA infrastructure +- Learn about [governance participation](../sequencer-management/governance-participation.md) as a sequencer +- Join the [Aztec Discord](https://discord.gg/aztec) for operator support and best practices diff --git a/docs/lines/v6/docs-operate/operators/setup/registering-sequencer.md b/docs/lines/v6/docs-operate/operators/setup/registering-sequencer.md new file mode 100644 index 00000000000..e618f64f650 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/registering-sequencer.md @@ -0,0 +1,198 @@ +--- +id: registering_sequencer +displayed_sidebar: operatorsSidebar +title: Registering a Sequencer +description: Learn how to register your sequencer on the Aztec network using the staking dashboard for self-staking. +references: ["labs/yarn-project/cli/src/cmds/validator_keys/staker.ts", "labs/yarn-project/cli/src/cmds/validator_keys/new.ts"] +--- + +## Overview + +This guide covers registering your sequencer on the Aztec network through the staking dashboard for **self-staking**. This is one of two ways to participate as a sequencer: + +1. **Self-staking** (this guide): You provide your own stake via the staking dashboard +2. **Delegated staking**: You receive stake from delegators (see [Running as a Staking Provider](./staking-provider.md)) + +Before proceeding, ensure you have completed the [Sequencer Setup Guide](./sequencer-setup.md) and your node is running. + +## Prerequisites + +- Completed sequencer node setup with keystore generated +- Access to your **public keystore** file (`keyN_staker_output.json`) +- Sufficient ATP/ATV tokens for staking +- Wallet with ETH for gas fees +- Web browser for accessing the staking dashboard + +## Understanding Your Keystore + +When you generated your sequencer keys, two files were automatically created: + +1. **Private keystore** (`~/.aztec/keystore/keyN.json`) - Contains private keys, used by your sequencer node. Keep this secure and never share it. +2. **Public keystore** (`~/.aztec/keystore/keyN_staker_output.json`) - Contains only public information, used for registration via the staking dashboard. + +### Public Keystore Structure + +The public keystore contains the following information needed for registration: + +```json +[ + { + "attester": "0xYOUR_ATTESTER_ADDRESS", + "publicKeyG1": { + "x": "FIELD_ELEMENT_AS_DECIMAL_STRING", + "y": "FIELD_ELEMENT_AS_DECIMAL_STRING" + }, + "publicKeyG2": { + "x0": "FIELD_ELEMENT_AS_DECIMAL_STRING", + "x1": "FIELD_ELEMENT_AS_DECIMAL_STRING", + "y0": "FIELD_ELEMENT_AS_DECIMAL_STRING", + "y1": "FIELD_ELEMENT_AS_DECIMAL_STRING" + }, + "proofOfPossession": { + "x": "FIELD_ELEMENT_AS_DECIMAL_STRING", + "y": "FIELD_ELEMENT_AS_DECIMAL_STRING" + } + } +] +``` + +**Fields explained:** +- **`attester`**: Your Ethereum attester address (sequencer identifier) +- **`publicKeyG1`**: BLS public key on the G1 curve (x, y coordinates) +- **`publicKeyG2`**: BLS public key on the G2 curve (x0, x1, y0, y1 coordinates) +- **`proofOfPossession`**: Cryptographic proof to prevent rogue key attacks + +:::tip +The public keystore contains no private keys and is safe to share with the staking dashboard or other parties. +::: + +## Preparing Your Keystore File + +### Single Sequencer + +If you're registering one sequencer, simply use the `keyN_staker_output.json` file that was generated when you created your keys. + +### Multiple Sequencers + +If you're registering multiple sequencers in a single transaction, combine the individual keystore files into a single JSON array. Each object in the array represents one sequencer. + +**Example for two sequencers:** + +```json +[ + { + "attester": "0xATTESTER_ADDRESS_1", + "publicKeyG1": { + "x": "0x...", + "y": "0x..." + }, + "publicKeyG2": { + "x0": "0x...", + "x1": "0x...", + "y0": "0x...", + "y1": "0x..." + }, + "proofOfPossession": { + "x": "0x...", + "y": "0x..." + } + }, + { + "attester": "0xATTESTER_ADDRESS_2", + "publicKeyG1": { + "x": "0x...", + "y": "0x..." + }, + "publicKeyG2": { + "x0": "0x...", + "x1": "0x...", + "y0": "0x...", + "y1": "0x..." + }, + "proofOfPossession": { + "x": "0x...", + "y": "0x..." + } + } +] +``` + +Simply copy the contents of each `keyN_staker_output.json` file and combine them into a single array. + +## Registration Steps + +Follow these steps to register your sequencer(s) through the staking dashboard: + +1. **Navigate to the staking dashboard** at https://stake.aztec.network + +2. **Connect your wallet** with the account that holds your ATP/ATV tokens + +3. **Click "Stake"** + + ![Staking dashboard home](/img/staking_dashboard_1.png) + +4. **Select "Run your own Sequencer"** + + ![Select sequencer option](/img/staking_dashboard_2.png) + +5. **Click through "Start Registration"** after reviewing the requirements + +6. **Select the ATP/ATV tokens you want to stake** + +7. **Upload your keystore JSON file** (either single or combined multi-sequencer file) + + ![Upload keystore file](/img/staking_dashboard_3.png) + +8. **Confirm your attester/sequencer addresses** + + ![Confirm addresses](/img/staking_dashboard_4.png) + +9. **Approve token spend** in your wallet + + ![Approve tokens](/img/staking_dashboard_5.png) + +10. **Add staking for all sequencers to the queue** + + ![Add to queue](/img/staking_dashboard_6.png) + +11. **Execute transactions** in the dashboard + + ![Execute transactions](/img/staking_dashboard_7.png) + +12. **Confirm each transaction** in your wallet + +13. **Click "Complete"** when all transactions are confirmed + +14. **Verification**: Your sequencers have entered the queue. You can verify this at https://dashtec.xyz/queue + +## Verification + +After registration, verify your sequencer is properly registered: + +### Via Staking Dashboard + +Use the staking dashboard to: +- View your sequencer's registration status +- Monitor your stake amount +- Track sequencer performance metrics + +### Via Blockchain Explorer + +You can verify your sequencers are in the queue at https://dashtec.xyz/queue + +### Via Smart Contract + +You can also query the status directly using the Rollup contract. See [Useful Commands](../sequencer-management/useful-commands.md) for detailed instructions. + +## Next Steps + +After registering your sequencer: + +1. **Monitor performance**: Track your sequencer's attestation rate and block proposals via the staking dashboard +2. **Maintain uptime**: Keep your sequencer node running with high availability +3. **Monitor your stake**: Ensure your stake remains above the ejection threshold +4. **Stay informed**: Join the [Aztec Discord](https://discord.gg/aztec) for operator support and network updates + +## Alternative: Running with Delegated Stake + +If you prefer to run a sequencer backed by delegated stake instead of self-staking, see the [Becoming a Staking Provider](./staking-provider.md) guide. diff --git a/docs/lines/v6/docs-operate/operators/setup/running-a-node.md b/docs/lines/v6/docs-operate/operators/setup/running-a-node.md new file mode 100644 index 00000000000..f76b785181b --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/running-a-node.md @@ -0,0 +1,210 @@ +--- +id: running_a_node +displayed_sidebar: operatorsSidebar +title: Running a Full Node +description: A comprehensive guide on how to run a full node on the Aztec network using Docker Compose. +references: ["labs/yarn-project/aztec/src/cli/cmds/start_node.ts"] +--- + +import NodeHardware from "../_node_hardware.mdx"; + +## Overview + +This guide covers the steps required to run a full node on Aztec using Docker Compose. + +A full node allows you to connect and interact with the network, providing an interface to send and receive transactions and state updates without relying on third parties. + +You should run your own full node if you want to interact with the network in the most privacy-preserving way. It's also a great way to support the Aztec network and get involved with the community. + +### Minimum Hardware Requirements + +<NodeHardware /> + +These requirements are subject to change as the network throughput increases. + +**Before proceeding:** Ensure you've reviewed and completed the [prerequisites](../prerequisites.md). + +This setup includes only essential settings. The `--network #release_network` flag applies network-specific defaults—see the [CLI reference](../reference/cli-reference.md) for all available configuration options. + +## Setup + +### Step 1: Set Up Directory Structure + +Create the directory structure for node data: + +```bash +mkdir -p aztec-node/data +cd aztec-node +touch .env +``` + +### Step 2: Configure Environment Variables + +Add the following to your `.env` file: + +```bash +DATA_DIRECTORY=./data +LOG_LEVEL=info +ETHEREUM_HOSTS=[your L1 execution endpoint] +L1_CONSENSUS_HOST_URLS=[your L1 consensus endpoint] +ETHEREUM_DEBUG_HOSTS=[your trace capable L1 execution endpoint] +P2P_IP=[your external IP address] +P2P_PORT=40400 +AZTEC_PORT=8080 +AZTEC_ADMIN_PORT=8880 +``` + +:::tip +Find your public IP address with: `curl ipv4.icanhazip.com` +::: + +:::warning Trace-capable L1 endpoint required +The node needs an L1 execution endpoint that serves `trace_transaction` or `debug_traceTransaction`. A checkpoint published through a non-standard contract, rather than by a direct call to the rollup, cannot be read from the transaction calldata alone. The node traces that transaction to recover the checkpoint, and without a trace it stops syncing at that checkpoint. + +Set `ETHEREUM_DEBUG_HOSTS` to your trace-capable endpoint. If you leave it unset, the node uses `ETHEREUM_HOSTS` for trace calls. The node only uses `ETHEREUM_DEBUG_HOSTS` for traces, not for regular L1 interactions. + +At startup the node checks that it can trace a recent L1 transaction on that endpoint, and refuses to start if neither method works. To start anyway, set `ETHEREUM_ALLOW_NO_DEBUG_HOSTS=true`. The node then logs a warning and runs, but stalls on the first checkpoint it cannot read until you point it at a trace-capable endpoint and restart it. +::: + +### Step 3: Create Docker Compose File + +Create a `docker-compose.yml` file in your `aztec-node` directory: + +```yaml +services: + aztec-node: + image: "azteclabs/aztec:#release_version" + container_name: "aztec-node" + ports: + - ${AZTEC_PORT}:${AZTEC_PORT} + - ${P2P_PORT}:${P2P_PORT} + - ${P2P_PORT}:${P2P_PORT}/udp + volumes: + - ${DATA_DIRECTORY}:/var/lib/data + environment: + DATA_DIRECTORY: /var/lib/data + LOG_LEVEL: ${LOG_LEVEL} + ETHEREUM_HOSTS: ${ETHEREUM_HOSTS} + L1_CONSENSUS_HOST_URLS: ${L1_CONSENSUS_HOST_URLS} + ETHEREUM_DEBUG_HOSTS: ${ETHEREUM_DEBUG_HOSTS} + P2P_IP: ${P2P_IP} + P2P_PORT: ${P2P_PORT} + AZTEC_PORT: ${AZTEC_PORT} + AZTEC_ADMIN_PORT: ${AZTEC_ADMIN_PORT} + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --node + --network #release_network + networks: + - aztec + restart: always + +networks: + aztec: + name: aztec +``` + +:::warning Security: Admin Port Not Exposed +The admin port (8880) is intentionally **not exposed** to the host machine for security reasons. The admin API provides sensitive operations like configuration changes and database rollbacks that should never be accessible from outside the container. + +If you need to access admin endpoints, use `docker exec`: +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' +``` +::: + +### Step 4: Start the Node + +Start the node: + +```bash +docker compose up -d +``` + +## Verification + +Once your node is running, verify it's working correctly: + +### Check Node Sync Status + +Check the current sync status: + +```bash +curl -s -X POST -H 'Content-Type: application/json' \ +-d '{"jsonrpc":"2.0","method":"aztec_getChainTips","params":[],"id":67}' \ +http://localhost:8080 | jq -r ".result.proven.block.number" +``` + +Compare the output with block explorers (see [Networks page](/networks) for explorer links). + +### Check Node Status + +```bash +curl http://localhost:8080/status +``` + +### Verify Port Connectivity + +```bash +# Check TCP connectivity on port 40400 +nc -vz [YOUR_EXTERNAL_IP] 40400 +# Should return: "Connection to [YOUR_EXTERNAL_IP] 40400 port [tcp/*] succeeded!" + +# Check UDP connectivity on port 40400 +nc -vu [YOUR_EXTERNAL_IP] 40400 +# Should return: "Connection to [YOUR_EXTERNAL_IP] 40400 port [udp/*] succeeded!" +``` + +### View Logs + +```bash +docker compose logs -f aztec-node +``` + +If all checks pass, your node should be up, running, and connected to the network. + +## Troubleshooting + +### Port forwarding not working + +**Issue**: Your node cannot connect to peers. + +**Solutions**: + +- Verify your external IP address matches the `P2P_IP` setting +- Check firewall rules on your router and local machine +- Test connectivity using: `nc -zv [your-ip] 40400` + +### Node not syncing + +**Issue**: Your node is not synchronizing with the network. + +**Solutions**: + +- Check L1 endpoint connectivity +- Verify both execution and consensus clients are fully synced +- Review logs for specific error messages +- Ensure L1 endpoints support high throughput + +### Docker issues + +**Issue**: Container won't start or crashes. + +**Solutions**: + +- Ensure Docker and Docker Compose are up to date +- Check disk space availability +- Verify the `.env` file is properly formatted +- Review container logs: `docker compose logs aztec-node` + +## Next Steps + +- Review [syncing best practices](./syncing-best-practices.md) for faster synchronization +- Learn about [bootnode operation](./bootnode-operation.md) for peer discovery +- Check the [CLI reference](../reference/cli-reference.md) for advanced configuration options +- Join the [Aztec Discord](https://discord.gg/aztec) for support and community discussions diff --git a/docs/lines/v6/docs-operate/operators/setup/running-a-prover.md b/docs/lines/v6/docs-operate/operators/setup/running-a-prover.md new file mode 100644 index 00000000000..bc72baca005 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/running-a-prover.md @@ -0,0 +1,385 @@ +--- +id: running_a_prover +displayed_sidebar: operatorsSidebar +title: Running a Prover +description: A comprehensive guide on how to run an Aztec prover on the network using Docker Compose in a distributed configuration. +references: ["labs/yarn-project/aztec/src/cli/aztec_start_options.ts", "labs/yarn-project/aztec/src/cli/cmds/start_prover_agent.ts", "labs/yarn-project/aztec/src/cli/cmds/start_prover_broker.ts", "labs/yarn-project/prover-node/src/config.ts", "labs/yarn-project/prover-client/src/proving_broker/*"] +--- + +## Overview + +This guide covers the steps required to run a prover on the Aztec network. Operating a prover is a resource-intensive role typically undertaken by experienced engineers due to its technical complexity and hardware requirements. + +Aztec provers are critical infrastructure components. They generate cryptographic proofs attesting to transaction correctness, ultimately producing a single rollup proof submitted to Ethereum. + +:::tip Prerequisites +Before proceeding, ensure you've reviewed and completed the [prerequisites](../prerequisites.md). +::: + +## Prover Architecture + +The prover consists of three main components: + +1. **Prover node**: Polls L1 for unproven epochs, creates prover jobs, distributes them to the broker, and submits the final rollup proof to the rollup contract. + +2. **Prover broker**: Manages the job queue, distributing work to agents and collecting results. + +3. **Prover agent(s)**: Executes proof generation jobs in a stateless manner. + +## Minimum Requirements + +### Prover Node + +- 16 core / 32 vCPU (released in 2015 or later) +- 16 GB RAM +- 1 TB NVMe SSD +- 25 Mbps network connection + +### Prover Broker + +- 8 core / 16 vCPU (released in 2015 or later) +- 16 GB RAM +- 10 GB SSD + +### Prover Agents + +**For each agent:** +- 32 core / 64 vCPU (released in 2015 or later) +- 128 GB RAM +- 10 GB SSD + +These requirements are subject to change as the network throughput increases. Prover agents require high-performance hardware, typically data center-grade infrastructure. + +:::tip Running Multiple Agents +You can run multiple prover agents on a single machine by adjusting `PROVER_AGENT_COUNT`. Hardware requirements scale approximately linearly: +- **2 agents**: 64 cores, 256 GB RAM +- **3 agents**: 96 cores, 384 GB RAM +- **4 agents**: 128 cores, 512 GB RAM +::: + +## Generating Keys + +Before setting up your prover, you need to generate the required Ethereum private key for the prover publisher. + +### Prover Publisher Private Key + +The prover publisher key is used to submit proofs to L1. This account needs ETH funding to pay for L1 gas. + +Generate an Ethereum private key using Foundry's `cast` tool: + +```bash +# Generate a new wallet with a 24-word mnemonic +cast wallet new-mnemonic --words 24 + +# This outputs a mnemonic phrase, a derived address, and private key +# Save these securely - you'll need the private key for PROVER_PUBLISHER_PRIVATE_KEY +# and the address for PROVER_ID +``` + +**Important notes:** +- Save both the private key and the derived address securely +- The private key will be used for `PROVER_PUBLISHER_PRIVATE_KEY` +- The derived Ethereum address will be used for `PROVER_ID` + +:::warning Account Funding Required +The publisher account needs to be funded with ETH to post proofs to L1. Ensure the account holds sufficient ETH for gas costs during operation. +::: + +:::tip +If you don't have Foundry installed, follow the installation guide at [getfoundry.sh](https://getfoundry.sh/). +::: + +## Setup + +The prover components are distributed across multiple machines for better performance and resource utilization. This setup runs multiple prover agents on separate high-performance machines, isolates the broker for better job queue management, and separates network-facing components (prover node) from compute-intensive components (agents). + +### Architecture + +- **Prover Node**: Runs on a machine with network access and L1 connectivity +- **Prover Broker**: Can run on the same machine as the prover node or separately (must be accessible from prover agents) +- **Prover Agents**: Run on separate high-performance machines (32+ cores each, scalable with `PROVER_AGENT_COUNT`) + +:::warning Network Requirements +Prover agents must communicate with the prover broker over the network. Ensure that: + +- The broker machine's port 8080 is accessible from all agent machines +- Firewall rules allow traffic between agents and broker +- Network connectivity is stable and low-latency between components +::: + +### Prover Node and Broker Setup + +On the machine that will run the prover node and broker: + +#### Step 1: Set Up Directory Structure + +```bash +mkdir -p aztec-prover-node/prover-node-data aztec-prover-node/prover-broker-data +cd aztec-prover-node +touch .env +``` + +#### Step 2: Configure Environment Variables + +Add to your `.env` file: + +```bash +# Prover Node Configuration +DATA_DIRECTORY=./prover-node-data +P2P_IP=[your external IP address] +P2P_PORT=40400 +ETHEREUM_HOSTS=[your L1 execution endpoint] +L1_CONSENSUS_HOST_URLS=[your L1 consensus endpoint] +LOG_LEVEL=info +PROVER_BROKER_HOST=http://prover-broker:8080 +PROVER_PUBLISHER_PRIVATE_KEY=[your prover publisher private key, see prerequisites] +AZTEC_PORT=8080 +AZTEC_ADMIN_PORT=8880 + +# Prover Broker Configuration +PROVER_BROKER_DATA_DIRECTORY=./prover-broker-data +PROVER_BROKER_PORT=8080 +``` + +#### Step 3: Create Docker Compose File + +Create `docker-compose.yml`: + +```yaml +name: aztec-prover-node +services: + prover-node: + image: azteclabs/aztec:#release_version + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --prover-node + --network #release_network + depends_on: + prover-broker: + condition: service_started + required: true + environment: + DATA_DIRECTORY: /var/lib/data + ETHEREUM_HOSTS: ${ETHEREUM_HOSTS} + L1_CONSENSUS_HOST_URLS: ${L1_CONSENSUS_HOST_URLS} + LOG_LEVEL: ${LOG_LEVEL} + PROVER_BROKER_HOST: ${PROVER_BROKER_HOST} + PROVER_PUBLISHER_PRIVATE_KEY: ${PROVER_PUBLISHER_PRIVATE_KEY} + P2P_IP: ${P2P_IP} + P2P_PORT: ${P2P_PORT} + AZTEC_PORT: ${AZTEC_PORT} + AZTEC_ADMIN_PORT: ${AZTEC_ADMIN_PORT} + ports: + - ${AZTEC_PORT}:${AZTEC_PORT} + - ${P2P_PORT}:${P2P_PORT} + - ${P2P_PORT}:${P2P_PORT}/udp + volumes: + - ${DATA_DIRECTORY}:/var/lib/data + restart: unless-stopped + + prover-broker: + image: azteclabs/aztec:#release_version + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --prover-broker + --network #release_network + environment: + DATA_DIRECTORY: /var/lib/data + ETHEREUM_HOSTS: ${ETHEREUM_HOSTS} + P2P_IP: ${P2P_IP} + LOG_LEVEL: ${LOG_LEVEL} + ports: + - ${PROVER_BROKER_PORT}:8080 + volumes: + - ${PROVER_BROKER_DATA_DIRECTORY}:/var/lib/data + restart: unless-stopped +``` + +:::warning Security: Admin Port Not Exposed +The admin port (8880) is intentionally **not exposed** to the host machine for security reasons. The admin API provides sensitive operations like configuration changes and database rollbacks that should never be accessible from outside the container. + +If you need to access admin endpoints, use `docker exec`: +```bash +docker exec -it prover-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' +``` +::: + +**Important:** The broker exposes port 8080 via `ports: - ${PROVER_BROKER_PORT}:8080`, making it accessible to external prover agents. Ensure this port is reachable from your agent machines. + +This configuration includes only essential settings. The `--network #release_network` flag applies network-specific defaults—see the [CLI reference](../reference/cli-reference.md) for all available configuration options. + +#### Step 4: Start Node and Broker + +```bash +docker compose up -d +``` + +### Prover Agent Setup + +On each machine that will run prover agents: + +#### Step 1: Set Up Directory + +```bash +mkdir aztec-prover-agent +cd aztec-prover-agent +touch .env +``` + +#### Step 2: Configure Environment Variables + +Add to your `.env` file: + +```bash +PROVER_AGENT_COUNT=1 +PROVER_AGENT_POLL_INTERVAL_MS=10000 +PROVER_BROKER_HOST=http://[BROKER_MACHINE_IP]:8080 +PROVER_ID=[address corresponding to PROVER_PUBLISHER_PRIVATE_KEY] +``` + +Replace `[BROKER_MACHINE_IP]` with the IP address of the machine running the prover broker. + +**Agent configuration tips:** + +- Set `PROVER_AGENT_COUNT` based on your machine's hardware (e.g., 64 cores/256 GB RAM = 2 agents, 96 cores/384 GB RAM = 3 agents, 128 cores/512 GB RAM = 4 agents) +- Test connectivity before starting: `curl http://[BROKER_MACHINE_IP]:8080` +- If the curl test fails, check your network configuration, firewall rules, and ensure the broker is running + +#### Step 3: Create Docker Compose File + +Create `docker-compose.yml`: + +```yaml +name: aztec-prover-agent +services: + prover-agent: + image: azteclabs/aztec:#release_version + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --prover-agent + --network #release_network + environment: + PROVER_AGENT_COUNT: ${PROVER_AGENT_COUNT} + PROVER_AGENT_POLL_INTERVAL_MS: ${PROVER_AGENT_POLL_INTERVAL_MS} + PROVER_BROKER_HOST: ${PROVER_BROKER_HOST} + PROVER_ID: ${PROVER_ID} + stop_grace_period: 300s + restart: unless-stopped +``` + +Docker sends `SIGKILL` when a container has not stopped before its grace period expires. Docker defaults this period to 10 seconds, which may interrupt an in-flight proof before the agent can drain it. Keep `stop_grace_period` long enough for the agent to finish its current proof; this example uses 300 seconds, matching the prover agent shutdown allowance in the Spartan deployment. If a job exceeds the broker's timeout, the broker re-enqueues it for another attempt. + +#### Step 4: Start Agent + +```bash +docker compose up -d +``` + +**Scaling your prover capacity:** + +- **Horizontal scaling**: Add more agent machines by repeating the agent setup on additional high-performance machines +- **Vertical scaling**: Increase `PROVER_AGENT_COUNT` on existing machines (ensure adequate hardware) + +All agents, regardless of which machine they're on, must be able to communicate with the broker at the configured `PROVER_BROKER_HOST`. + +## Verification + +Once your prover is running, verify all components are working correctly: + +### Check Services + +On the prover node machine: +```bash +docker compose ps +``` + +On each agent machine: +```bash +docker compose ps +``` + +### View Logs + +On prover node machine: +```bash +# Prover node logs +docker compose logs -f prover-node + +# Broker logs +docker compose logs -f prover-broker +``` + +On agent machines: +```bash +# Agent logs +docker compose logs -f prover-agent +``` + +## Troubleshooting + +### Components not communicating + +**Issue**: Prover agent cannot connect to broker. + +**Solutions**: +- Verify the broker IP address in `PROVER_BROKER_HOST` is correct +- Ensure port 8080 on the broker machine is accessible from agent machines +- Check firewall rules between machines allow traffic on port 8080 +- Test connectivity from agent machine: `curl http://[BROKER_IP]:8080` +- Verify the broker container is running: `docker compose ps` +- Check if the broker port is exposed in docker-compose.yml +- Review broker logs for connection attempts: `docker compose logs prover-broker` + +### Insufficient resources + +**Issue**: Prover agent crashes or performs poorly. + +**Solutions**: +- Verify your hardware meets the minimum requirements (32 cores per agent, 128 GB RAM per agent) +- Check system resource usage: `docker stats` +- Reduce `PROVER_AGENT_COUNT` if running multiple agents per machine +- Ensure no other resource-intensive processes are running +- Monitor CPU and memory usage to verify resources match your configured agent count + +### Agent not picking up jobs + +**Issue**: Agent logs show no job activity. + +**Solutions**: +- Verify the broker is receiving jobs from the prover node +- Check broker logs for errors +- Confirm `PROVER_ID` matches your publisher address +- Ensure agent can reach the broker endpoint +- Test broker connectivity: `curl http://[BROKER_IP]:8080` + +### Docker issues + +**Issue**: Containers won't start or crash repeatedly. + +**Solutions**: +- Ensure Docker and Docker Compose are up to date +- Check disk space availability on all machines +- Verify `.env` files are properly formatted +- Review logs for specific error messages + +### Common Issues + +See the [Operator FAQ](../operator-faq.md) for additional common issues and resolutions. + +## Next Steps + +- Monitor your prover's performance and proof submission rate +- Consider adding more prover agents for increased capacity (either by increasing `PROVER_AGENT_COUNT` or adding more machines) +- Join the [Aztec Discord](https://discord.gg/aztec) for operator support +- Review [governance participation](../sequencer-management/governance-participation.md) for participating in governance diff --git a/docs/lines/v6/docs-operate/operators/setup/sequencer-setup.md b/docs/lines/v6/docs-operate/operators/setup/sequencer-setup.md new file mode 100644 index 00000000000..2928e12f7e0 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/sequencer-setup.md @@ -0,0 +1,628 @@ +--- +id: sequencer_management +displayed_sidebar: operatorsSidebar +title: Running a Sequencer +description: Learn how to manage your sequencer on the Aztec network, including registration, keystore configuration, stake management, and status monitoring. +references: ["labs/yarn-project/node-keystore/src/schemas.ts", "labs/yarn-project/cli/src/cmds/validator_keys/new.ts", "labs/yarn-project/aztec/src/cli/aztec_start_options.ts", "labs/yarn-project/sequencer-client/src/config.ts"] +--- + +import NodeHardware from "../_node_hardware.mdx"; + +## Overview + +This guide covers sequencer lifecycle management on the Aztec network: keystore configuration, node setup, registration, ongoing operations, and eventual exit. + +:::danger Minimum Stake Requirement +To participate as a sequencer on the Aztec network, you must stake a minimum of **200,000 AZTEC tokens**. Ensure you have sufficient tokens before proceeding with sequencer setup and registration. +::: + +Sequencer nodes are critical infrastructure responsible for ordering transactions and producing blocks. They perform three key actions: + +1. Assemble unprocessed transactions and propose the next block +2. Attest to correct execution of transactions in proposed blocks (when part of the sequencer committee) +3. Submit successfully attested blocks to L1 + +Before publication, blocks must be validated by a committee of sequencer nodes who re-execute public transactions and verify private function proofs. Committee members attest to validity by signing the block header. + +Once sufficient attestations are collected (two-thirds of the committee plus one), the block can be submitted to L1. + +### Minimum Hardware Requirements + +<NodeHardware /> + +These requirements are subject to change as the network throughput increases. + +**Before proceeding:** Ensure you've reviewed and completed the [prerequisites](../prerequisites.md). + +## Keystore Explanation + +Sequencers require private keys to identify themselves as valid proposers and attesters. These keys are configured through a private keystore file. + +### Private Keystore Structure + +The private keystore file (`keystore.json`) uses the following structure: + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "ETH_PRIVATE_KEY", + "bls": "BLS_PRIVATE_KEY" + }, + "publisher": ["PUBLISHER_PRIVATE_KEY"], // Optional: defaults to attester key + "feeRecipient": "0x0000000000000000000000000000000000000000000000000000000000000000", // Not currently used, set to all zeros + "coinbase": "ETH_ADDRESS" + } + ] +} +``` + +:::info +The attester field contains both Ethereum and BLS keys: +- **ETH key**: Derives the address that serves as your sequencer's unique identifier in the protocol +- **BLS key**: Used to sign proposals and attestations, as well as for staking operations +::: + +### Field Descriptions + +#### attester (required) + +**Your sequencer's identity.** Contains both Ethereum and BLS keys: + +- **Format**: Object with `eth` and `bls` fields +- **eth**: Ethereum private key - the derived address serves as your sequencer's unique identifier in the protocol +- **bls**: BLS private key - actually signs proposals and attestations, and is used for staking operations (validator registration and proof of possession) +- **Purpose**: The ETH address identifies your sequencer, while the BLS key performs the cryptographic signing of consensus messages + +#### publisher (optional) + +Separate private key(s) for submitting BLS-signed messages to L1. The publisher just pays gas to post already-signed proposals and attestations. + +- **Format**: Array of Ethereum private keys +- **Default**: Uses attester key if not specified +- **Purpose**: Posts signed messages to L1 and pays for gas (doesn't participate in signing) +- **Funding**: around 0.01 ETH per attester served is a comfortable opening top-up at current L1 prices. Use the [funding calculator](../solo-sequencer/fund-accounts#fund-the-publisher) to scan the real network with your RPC and size against your own gas-price assumptions. + +:::tip +If you're using the attester ETH key for publishing (no separate publisher keys), you can omit the `publisher` field entirely from your keystore, but you will still need to fund the attester account so it can pay L1 gas. +::: + +#### feeRecipient + +Aztec address that would receive L2 transaction fees. + +- **Format**: 32-byte Aztec address (64 hex characters) +- **Current status**: Not currently used by the protocol - set to `0x0000000000000000000000000000000000000000000000000000000000000000` +- **Purpose**: Reserved for future fee distribution mechanisms + +#### coinbase (optional) + +Ethereum address that receives all L1 block rewards and tx fees. + +- **Format**: Ethereum address +- **Default**: Uses attester address if not specified + +### Generating Keys + +Use the Aztec CLI's keystore utility to generate both your private and public keystores: + +#if(testnet) +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ + --gse-address 0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac \ + --l1-rpc-urls $ETH_RPC +``` + +**Relevant parameters:** +- `--fee-recipient`: Set to all zeros (not currently used by the protocol) +- `--staker-output`: Generate the public keystore for the staking dashboard +- `--gse-address`: The GSE (Governance Staking Escrow) contract address (`0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac` for Sepolia testnet) +- `--l1-rpc-urls`: Your Ethereum Sepolia L1 RPC endpoint + - Set `ETH_RPC` environment variable, or replace `$ETH_RPC` with your RPC URL (e.g., `https://sepolia.infura.io/v3/YOUR_API_KEY`) +#else +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ + --gse-address 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f \ + --l1-rpc-urls $ETH_RPC +``` + +**Relevant parameters:** +- `--fee-recipient`: Set to all zeros (not currently used by the protocol) +- `--staker-output`: Generate the public keystore for the staking dashboard +- `--gse-address`: The GSE (Governance Staking Escrow) contract address (`0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f` for mainnet) +- `--l1-rpc-urls`: Your Ethereum mainnet RPC endpoint + - Set `ETH_RPC` environment variable, or replace `$ETH_RPC` with your Ethereum mainnet RPC URL (e.g., `https://mainnet.infura.io/v3/YOUR_API_KEY`) +#endif +- `--count`: Number of validator identities to generate (default: 1) + - Use this to generate multiple attester identities in a single keystore + - Example: `--count 5` generates 5 validator identities with sequential addresses + - All identities are derived from the same mnemonic using different derivation paths + - Useful for operators running multiple sequencer identities or delegated staking providers +#if(testnet) +- `--publisher-count` Number of publisher accounts per validator (default 1) +#else +- `--publisher-count` Number of publisher accounts per validator (default 0) +#endif + + +**This command creates two JSON files:** +1. **Private keystore** (`~/.aztec/keystore/keyN.json`) - Contains your ETH and BLS private keys for running the node +2. **Public keystore** (`~/.aztec/keystore/keyN_staker_output.json`) - Contains only public information (public keys and proof of possession) for the staking dashboard + +Where `N` is an auto-incrementing number (e.g., `key1.json`, `key2.json`, etc.) + +**What gets generated:** +- Automatically generates a mnemonic for key derivation (or provide your own with `--mnemonic`) +- Creates an ETH key (for your sequencer identifier) and BLS key (for signing) +- Computes BLS public keys (G1 and G2) and proof of possession +- Outputs your attester address, publisher address and BLS public keys to the console + +**Example output (single validator):** +``` +No mnemonic provided, generating new one... +Using new mnemonic: + +word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12 + +Wrote validator keystore to /Users/aztec/.aztec/keystore/key1.json +Wrote staker output for 1 validator(s) to /Users/aztec/.aztec/keystore/key1_staker_output.json + +acc1: + attester: + eth: 0xA55aB561877E479361BA033c4ff7B516006CF547 + bls: 0xa931139040533679ff3990bfc4f40b63f50807815d77346e3c02919d71891dc1 +``` + +**Example output (multiple validators with `--count 3`):** +``` +No mnemonic provided, generating new one... +Using new mnemonic: + +word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12 + +Wrote validator keystore to /Users/aztec/.aztec/keystore/key1.json +Wrote staker output for 3 validator(s) to /Users/aztec/.aztec/keystore/key1_staker_output.json + +acc1: + attester: + eth: 0xA55aB561877E479361BA033c4ff7B516006CF547 + bls: 0xa931139040533679ff3990bfc4f40b63f50807815d77346e3c02919d71891dc1 +acc2: + attester: + eth: 0xB66bC672988F590472CA144e5D8d9F82307DA658 + bls: 0xb842240151644780ff4991cfd5f51c74f61918926e88457f4d13020e82902ed2 +acc3: + attester: + eth: 0xC77cD783999F601583DB255f6E9e0F93418EB769 + bls: 0xc953351262755891ff5aa2dfe6f62d85f72a29a37f99568f5e24131f93a13fe3 +``` + +**Critical: Save your mnemonic phrase!** +- The mnemonic is the **only thing you must save** - it can regenerate all your keys, addresses, and keystores +- Store it securely offline (not on the server running the node) + +**For convenience, note:** +- **Attester address** (eth): Your sequencer's identifier (e.g., `0xA55aB...F547`) - useful for registration and monitoring +- **File paths**: Where the keystores were saved + +All other information (BLS keys, public keys, addresses) can be re-derived from the mnemonic if needed. + +#if(testnet) +:::tip Provide Your Own Mnemonic +For deterministic key generation or to recreate keys later, provide your own mnemonic: +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ + --gse-address 0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac \ + --l1-rpc-urls $ETH_RPC \ + --mnemonic "your twelve word mnemonic phrase here" +``` +::: +#else +:::tip Provide Your Own Mnemonic +For deterministic key generation or to recreate keys later, provide your own mnemonic: +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ + --gse-address 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f \ + --l1-rpc-urls $ETH_RPC \ + --mnemonic "your twelve word mnemonic phrase here" +``` +::: +#endif + +#if(testnet) +:::tip Generate Multiple Validator Identities +To generate multiple validator identities (useful for delegated staking providers or operators running multiple sequencers): +```bash +# Generate 5 validator identities from the same mnemonic +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ + --gse-address 0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac \ + --l1-rpc-urls $ETH_RPC \ + --count 5 +``` +#else +:::tip Generate Multiple Validator Identities +To generate multiple validator identities (useful for delegated staking providers or operators running multiple sequencers): +```bash +# Generate 5 validator identities from the same mnemonic +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ + --gse-address 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f \ + --l1-rpc-urls $ETH_RPC \ + --count 5 +``` +#endif + +Each identity gets a unique attester address derived from sequential derivation paths. All identities are included in: +- The same private keystore file (`keyN.json`) +- The same public keystore file (`keyN_staker_output.json`) +::: + +For detailed instructions, advanced options, and complete examples, see the [Creating Sequencer Keystores guide](../keystore/creating-keystores.md). + +## Setup with Docker Compose + +### Step 1: Set Up Directory Structure + +Create the directory structure for sequencer data storage: + +```bash +mkdir -p aztec-sequencer/keys aztec-sequencer/data +cd aztec-sequencer +touch .env +``` + +### Step 2: Generate and Move Private Keystore to Docker Directory + +If you haven't already generated your private and public keystores, do so now (see [Generating Keys](#generating-keys) above). + +Move the private keystore (not the public keystore) into the Docker directory: + +```bash +# Move the private keystore to Docker directory (replace N with your key number) +cp ~/.aztec/keystore/keyN.json aztec-sequencer/keys/keystore.json + +# Keep the public keystore for later use with the staking dashboard +# It will be at ~/.aztec/keystore/keyN_staker_output.json +``` + +### Step 3: Fund Your Publisher Account + +Your sequencer needs ETH to pay for gas when submitting blocks to L1. Fund the account that will act as the publisher. + +**Determine which address to fund:** + +```bash +# Get your attester address (this will be your publisher if no separate publisher is configured) +jq -r '.[0].attester' ~/.aztec/keystore/keyN_staker_output.json + +# If you have a separate publisher configured: (Note this returns the publisher private key) +jq -r '.validators[0].publisher[0]' aztec-sequencer/keys/keystore.json +``` + +The publisher account pays L1 gas each time it submits a checkpoint. At current mainnet gas levels the cost is small (around 0.03 ETH per attester per year), but balance decays continuously and the system does not retry with a different publisher if a transaction fails due to insufficient funds. + +**Sizing the top-up:** +- 1 attester → 0.01 ETH gives several months of runway +- 10 attesters sharing one publisher → roughly 0.1 ETH for a similar window +- For exact runway estimates, use the [funding calculator](../solo-sequencer/fund-accounts#fund-the-publisher) with your L1 RPC to scan the last 30 days of network activity + +:::tip +Set a refill threshold and an alert on the publisher balance so you can top up before it dries up at the worst-case gas price you expect. +::: + +### Step 4: Configure Environment Variables + +Add the following to your `.env` file: + +#if(testnet) +```bash +DATA_DIRECTORY=./data +KEY_STORE_DIRECTORY=./keys +LOG_LEVEL=info +ETHEREUM_HOSTS=[your Ethereum Sepolia execution endpoint, or a comma separated list if you have multiple] +L1_CONSENSUS_HOST_URLS=[your Ethereum Sepolia consensus endpoint, or a comma separated list if you have multiple] +ETHEREUM_DEBUG_HOSTS=[your trace capable L1 execution endpoint] +P2P_IP=[your external IP address] +P2P_PORT=40400 +AZTEC_PORT=8080 +AZTEC_ADMIN_PORT=8880 +``` +#else +```bash +DATA_DIRECTORY=./data +KEY_STORE_DIRECTORY=./keys +LOG_LEVEL=info +ETHEREUM_HOSTS=[your Ethereum mainnet execution endpoint, or a comma separated list if you have multiple] +L1_CONSENSUS_HOST_URLS=[your Ethereum mainnet consensus endpoint, or a comma separated list if you have multiple] +ETHEREUM_DEBUG_HOSTS=[your trace capable L1 execution endpoint] +P2P_IP=[your external IP address] +P2P_PORT=40400 +AZTEC_PORT=8080 +AZTEC_ADMIN_PORT=8880 +``` +#endif + +:::tip +Find your public IP address with: `curl ipv4.icanhazip.com` +::: + +:::warning Nethermind Users (versions before v1.36.0) +If you are using Nethermind as your L1 execution client with a version before v1.36.0, you must add the following environment variable: +```bash +# Required for Nethermind versions before v1.36.0 +L1_FIXED_PRIORITY_FEE_PER_GAS=1 +``` +This issue was fixed in Nethermind v1.36.0, so users on that version or later do not need this setting. +::: + +:::warning Trace-capable L1 endpoint required +The node needs an L1 execution endpoint that serves `trace_transaction` or `debug_traceTransaction`. A checkpoint published through a non-standard contract, rather than by a direct call to the rollup, cannot be read from the transaction calldata alone. The node traces that transaction to recover the checkpoint, and without a trace it stops syncing at that checkpoint. + +Set `ETHEREUM_DEBUG_HOSTS` to your trace-capable endpoint. If you leave it unset, the node uses `ETHEREUM_HOSTS` for trace calls. The node only uses `ETHEREUM_DEBUG_HOSTS` for traces, not for regular L1 interactions. + +At startup the node checks that it can trace a recent L1 transaction on that endpoint, and refuses to start if neither method works. To start anyway, set `ETHEREUM_ALLOW_NO_DEBUG_HOSTS=true`. The node then logs a warning and runs, but stalls on the first checkpoint it cannot read until you point it at a trace-capable endpoint and restart it. +::: + +### Step 5: Create Docker Compose File + +Create a `docker-compose.yml` file in your `aztec-sequencer` directory: + +```yaml +services: + aztec-sequencer: + image: "azteclabs/aztec:#release_version" + container_name: "aztec-sequencer" + ports: + - ${AZTEC_PORT}:${AZTEC_PORT} + - ${P2P_PORT}:${P2P_PORT} + - ${P2P_PORT}:${P2P_PORT}/udp + volumes: + - ${DATA_DIRECTORY}:/var/lib/data + - ${KEY_STORE_DIRECTORY}:/var/lib/keystore + environment: + KEY_STORE_DIRECTORY: /var/lib/keystore + DATA_DIRECTORY: /var/lib/data + LOG_LEVEL: ${LOG_LEVEL} + ETHEREUM_HOSTS: ${ETHEREUM_HOSTS} + L1_CONSENSUS_HOST_URLS: ${L1_CONSENSUS_HOST_URLS} + ETHEREUM_DEBUG_HOSTS: ${ETHEREUM_DEBUG_HOSTS} + P2P_IP: ${P2P_IP} + P2P_PORT: ${P2P_PORT} + AZTEC_PORT: ${AZTEC_PORT} + AZTEC_ADMIN_PORT: ${AZTEC_ADMIN_PORT} + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --node + --sequencer + --network #release_network + networks: + - aztec + restart: always + +networks: + aztec: + name: aztec +``` + +:::warning Security: Admin Port Not Exposed +The admin port (8880) is intentionally **not exposed** to the host machine for security reasons. The admin API provides sensitive operations like configuration changes and database rollbacks that should never be accessible from outside the container. + +If you need to access admin endpoints, use `docker exec`: +```bash +docker exec -it aztec-sequencer curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -H 'x-api-key: [ADMIN_API_KEY]' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' +``` + +The admin API requires an API key by default. The node generates one at first startup and prints it once to the logs; pass it with the `x-api-key` header (or `Authorization: Bearer [ADMIN_API_KEY]`). See [admin API key authentication](../reference/changelog/v4.md#admin-api-key-authentication). +::: + +This configuration includes only essential settings. The `--network #release_network` flag applies network-specific defaults—see the [CLI reference](../reference/cli-reference.md) for all available configuration options. + +### Step 6: Start the Sequencer + +Start the sequencer: + +```bash +docker compose up -d +``` + +## Verification + +Once your sequencer is running, verify it's working correctly: + +### Check Sync Status + +Check the current sync status (this may take a few minutes): + +```bash +curl -s -X POST -H 'Content-Type: application/json' \ +-d '{"jsonrpc":"2.0","method":"aztec_getChainTips","params":[],"id":67}' \ +http://localhost:8080 | jq -r ".result.proven.block.number" +``` + +Compare the output with block explorers (see [Networks page](/networks) for explorer links). + +### Check Node Status + +```bash +curl http://localhost:8080/status +``` + +### View Logs + +```bash +docker compose logs -f --tail 100 aztec-sequencer +``` + +## Next Steps: Registering Your Sequencer + +Now that your sequencer node is set up and running, you need to register it with the network. There are two ways to participate as a sequencer: + +### Option 1: Self-Staking via Staking Dashboard + +Register your sequencer and provide your own stake through the staking dashboard. This is the most common approach for individual operators. + +**→ [Register Your Sequencer (Self-Staking)](./registering-sequencer.md)** + +You'll use the **public keystore** file (`keyN_staker_output.json`) that was generated when you created your keys. + +### Option 2: Running with Delegated Stake + +Operate sequencers backed by tokens from delegators. This non-custodial system allows you to run sequencer infrastructure while delegators provide the economic backing. + +**→ [Run as a Staking Provider](./staking-provider.md)** + +As a provider, you'll register with the Staking Registry and manage a queue of sequencer identities that activate when delegators stake to you. + +:::tip Which Option Should I Choose? +- **Self-staking**: You have tokens and want to run your own sequencer +- **Delegated staking**: You want to operate sequencer infrastructure and earn commission from delegators' stake + +Both options use the same node setup from this guide. +::: + +## Monitoring Sequencer Status + +You can query the status of any sequencer (attester) using the Rollup and GSE (Governance Staking Escrow) contracts on L1. + +### Prerequisites + +- Foundry installed (`cast` command) +- Ethereum RPC endpoint +- Registry contract address for your network + +### Get Contract Addresses + +First, get the canonical Rollup contract address from the Registry: + +```bash +# Get the canonical rollup address +cast call [REGISTRY_CONTRACT_ADDRESS] "getCanonicalRollup()" --rpc-url [YOUR_RPC_URL] +``` + +Then get the GSE contract address from the Rollup: + +```bash +# Get the GSE contract address +cast call [ROLLUP_ADDRESS] "getGSE()" --rpc-url [YOUR_RPC_URL] +``` + +### Query Sequencer Status + +Check the complete status and information for a specific sequencer: + +```bash +# Get full attester view (status, balance, exit info, config) +cast call [ROLLUP_ADDRESS] "getAttesterView(address)" [ATTESTER_ADDRESS] --rpc-url [YOUR_RPC_URL] +``` + +This returns an `AttesterView` struct containing: + +1. **status** - The sequencer's current status (see Status Codes below) +2. **effectiveBalance** - The sequencer's effective stake balance +3. **exit** - Exit information (if the sequencer is exiting) +4. **config** - Attester configuration (withdrawer address and public key) + +#### Status Codes + +| Status | Name | Meaning | +| ------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------- | +| 0 | NONE | The sequencer does not exist in the sequencer set | +| 1 | VALIDATING | The sequencer is currently active and participating in consensus | +| 2 | ZOMBIE | The sequencer is not active (balance fell below ejection threshold, possibly due to slashing) but still has funds in the system | +| 3 | EXITING | The sequencer has initiated withdrawal and is in the exit delay period | + +### Performance Monitoring + +Track your sequencer's performance by monitoring: + +- **Effective balance** - Should remain above the ejection threshold +- **Status** - Should be VALIDATING for active participation +- **Attestation rate** - How many attestations you've successfully submitted +- **Proposal success rate** - How many of your proposed blocks were accepted +- **Network participation metrics** - Overall participation in network consensus + +## Exiting a Sequencer + +:::warning +Information about the exit process will be added when the mechanism is finalized. Check the [Aztec Discord](https://discord.gg/aztec) for the latest information on exiting the sequencer set. +::: + +## Troubleshooting + +### Port forwarding not working + +**Issue**: Your node cannot connect to peers. + +**Solutions**: + +- Verify your external IP address matches the `P2P_IP` setting +- Check firewall rules on your router and local machine +- Test connectivity using: `nc -zv [your-ip] 40400` + +### Sequencer not syncing + +**Issue**: Your node is not synchronizing with the network. + +**Solutions**: + +- Check L1 endpoint connectivity +- Verify both execution and consensus clients are fully synced +- Review logs for specific error messages +- Ensure L1 endpoints support high throughput + +### Private keystore issues + +**Issue**: Private keystore not loading or errors about invalid keys. + +**Solutions**: + +- Ensure `keystore.json` is properly formatted +- Verify private keys are valid Ethereum private keys +- Check file permissions on the keys directory + +### Docker issues + +**Issue**: Container won't start or crashes. + +**Solutions**: + +- Ensure Docker and Docker Compose are up to date +- Check disk space availability +- Verify the `.env` file is properly formatted +- Review container logs: `docker compose logs aztec-sequencer` + +### Common Issues + +See the [Operator FAQ](../operator-faq.md) for additional common issues and resolutions. + +## Additional Resources + +After setting up and registering your sequencer: + +- **[Register Your Sequencer](./registering-sequencer.md)** - Complete registration via staking dashboard +- **[Monitor Sequencer Status](#monitoring-sequencer-status)** - Track performance and attestation rate +- **[Operator FAQ](../operator-faq.md)** - Common issues and resolutions +- **[Governance Participation](../sequencer-management/governance-participation.md)** - Participate in governance +- **[Advanced Keystore Patterns](../keystore/advanced-patterns.md)** - Manage multiple sequencer identities + +**Community support:** +- Join the [Aztec Discord](https://discord.gg/aztec) for operator support and network updates diff --git a/docs/lines/v6/docs-operate/operators/setup/staking-provider.md b/docs/lines/v6/docs-operate/operators/setup/staking-provider.md new file mode 100644 index 00000000000..08399f32faf --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/staking-provider.md @@ -0,0 +1,609 @@ +--- +id: become_a_staking_provider +displayed_sidebar: operatorsSidebar +title: Become a Staking Provider +description: Learn how to run a sequencer with delegated stake on the Aztec network, including provider registration and sequencer identity management. +references: ["labs/yarn-project/cli/src/cmds/validator_keys/new.ts", "labs/yarn-project/cli/src/cmds/validator_keys/staker.ts", "labs/yarn-project/node-keystore/src/schemas.ts"] +--- + +## Overview + +This guide covers running a sequencer with delegated stake on the Aztec network. Unlike conventional setups where you must have your own stake, delegated stake lets you (the "provider") operate sequencers backed by tokens from delegators. + +**This is a non-custodial system**: Delegators retain full control and ownership of their tokens at all times. You never take custody of the delegated tokens—they remain in the delegator's control while providing economic backing for your sequencer operations. + +## Prerequisites + +Before proceeding, ensure you have: + +- Knowledge of running a sequencer node (see [Sequencer Setup Guide](./sequencer-setup.md)) +- An Ethereum wallet with sufficient ETH for gas fees +- Understanding of basic Aztec staking mechanics +- Foundry installed for `cast` commands +- Aztec CLI v#release_version or later installed: + +```bash +VERSION=#release_version bash -i <(curl -sL https://install.aztec.network/#release_version) +``` + +### Contract Addresses + +For Staking Registry and GSE (Governance Staking Escrow) addresses, see the [Networks page](/networks#l1-contract-addresses). + +## How Delegated Stake Works + +You register with the StakingRegistry contract and add sequencer identities (keystores) to a queue. When delegators stake to your provider, the system: + +1. Dequeues one keystore from your provider queue +2. Creates a [Split contract](https://docs.splits.org/core/split) for reward distribution +3. Registers the sequencer into the staking queue using the dequeued keystore + +### Reward Distribution + +When a delegator stakes to your provider, a Split contract is automatically created to manage reward distribution. In the default flow, you configure your sequencer to use the Split contract address as the coinbase (see [After Delegation: Configure Sequencer Coinbase](#after-delegation-configure-sequencer-coinbase)). + +:::info Alternative: EOA coinbase with the payout script +The Split flow is the default, not the only option. A provider can set the coinbase to an address they control and run the [payout script](https://github.com/AztecProtocol/aztec-staking-payout) to calculate and distribute delegator rewards at a published commission and cadence. See [the configuration step](/operate/operators/provider/configure-environment#alternative-eoa-coinbase-with-the-payout-script) and the [forum announcement](https://forum.aztec.network/t/operator-commission-adjustments/8588). +::: + +Rewards are distributed according to your agreed commission rate: +- **Provider commission**: Your `providerRewardsRecipient` address receives your commission rate (e.g., 5% for 500 basis points) +- **Delegator rewards**: The delegator's Aztec Token Vault (ATV) receives the remaining percentage + +**Rewards flow:** +1. Rewards accumulate in the rollup under the coinbase address (the Split contract) +2. After governance unlocks rewards, anyone can release them from the rollup to the `coinbase` address. +3. Anyone can then disperse the rewards from the Split contract to both the ATV and your `providerRewardsRecipient` + +This design ensures delegators maintain control of their rewards while you earn commission for operating the sequencer infrastructure. + +## Setup Process + +Before starting these steps, ensure your sequencer node infrastructure is set up (see [Prerequisites](#prerequisites)). + +Follow these steps to set up delegated stake: + +1. Register your provider with the Staking Registry +2. Add sequencer identities to your provider queue +3. Set your metadata in the GitHub repo + +**After a delegator stakes:** Configure your sequencer's coinbase (see [After Delegation](#after-delegation-configure-sequencer-coinbase)) + +### Step 1: Register Your Provider + +Register with the `StakingRegistry` contract as a provider for delegated staking. Registration is permissionless and open to anyone. + +**Function signature:** + +```solidity +function registerProvider( + address _providerAdmin, + uint16 _providerTakeRate, + address _providerRewardsRecipient +) external returns (uint256); +``` + +**Parameters:** +- `_providerAdmin`: Address that can update provider configuration +- `_providerTakeRate`: Commission rate in basis points (500 = 5%) +- `_providerRewardsRecipient`: Address receiving commission payments + +**Returns:** Your unique `providerIdentifier`. Save this—you'll need it for all provider operations. + +**Example:** + +```bash +# Register a provider with 5% commission rate +cast send $STAKING_REGISTRY_ADDRESS \ + "registerProvider(address,uint16,address)" \ + $PROVIDER_ADMIN_ADDRESS \ + 500 \ + $REWARDS_RECIPIENT_ADDRESS \ + --rpc-url $RPC_URL \ + --private-key $YOUR_PRIVATE_KEY +``` + +### Extracting Your Provider ID + +Once the transaction is confirmed, you need to extract your `providerIdentifier` from the transaction logs. The provider ID is emitted as the second topic in the registration event log. + +**Method 1: Using cast receipt** + +```bash +cast receipt [TX_HASH] --rpc-url $RPC_URL | grep "return" | awk '{print $2}' | xargs cast to-dec +``` + +**Method 2: From transaction logs** + +The transaction receipt will contain one log where the second topic is your `providerId` in hex format: + +```bash +# Example log output +logs [{"address":"0xc3860c45e5f0b1ef3000dbf93149756f16928adb", + "topics":["0x43fe1b4477c9a580955f586c904f4670929e184ef4bef4936221c52d0a79a75b", + "0x0000000000000000000000000000000000000000000000000000000000000002", # This is your providerId + "0x000000000000000000000000efdb4c5f3a2f04e0cb393725bcae2dd675cc3718", + "0x00000000000000000000000000000000000000000000000000000000000001f4"], + ... + }] +``` + +Convert the hex value to decimal: + +```bash +cast to-dec 0x0000000000000000000000000000000000000000000000000000000000000002 +# Output: 2 +``` + +**Save your `providerIdentifier`**—you'll need it for all subsequent provider operations. + +### Step 2: Add Sequencer Identities + +Add sequencer identities (keystores) to your provider queue. Each keystore represents one sequencer that can be activated when a delegator stakes to you. + +**Function signature:** + +```solidity +function addKeysToProvider( + uint256 _providerIdentifier, + KeyStore[] calldata _keyStores +) external; +``` + +**Parameters:** +- `_providerIdentifier`: Your provider identifier from registration +- `_keyStores`: Array of keystore structures (max 100 per transaction) + +**KeyStore structure:** + +```solidity +struct KeyStore { + address attester; // Sequencer's address + BN254Lib.G1Point publicKeyG1; // BLS public key (G1) + BN254Lib.G2Point publicKeyG2; // BLS public key (G2) + BN254Lib.G1Point proofOfPossession; // BLS signature (prevents rogue key attacks) +} +``` + +:::warning Critical: Key Management for Delegated Staking + +**⚠️ If you run out of keys, users cannot delegate tokens to you.** + +The Staking Registry **DOES NOT** check for duplicate keys. Please take **EXTREME** care when registering keys: +- Duplicate keys will cause delegation failures when that duplicate is at the top of your queue +- The only way to fix this is by calling `dripProviderQueue(_providerIdentifier, _numberOfKeysToDrip)` to remove the duplicate +- Always verify keys before registration to avoid user experience issues +::: + +### Generating Keys for Registration + +Use the `aztec validator-keys` command with the `--staker-output` flag to automatically generate properly formatted registration data: + +#if(testnet) +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ + --gse-address 0xb6a38a51a6c1de9012f9d8ea9745ef957212eaac \ + --l1-rpc-urls $ETH_RPC \ + --l1-chain-id 11155111 +``` +#else +```bash +aztec validator-keys new \ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \ + --staker-output \ + --gse-address 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f \ + --l1-rpc-urls $ETH_RPC \ + --l1-chain-id 1 +``` +#endif + +This command automatically: +1. Generates the private keystore with ETH and BLS keys +2. Generates the public keystore with G1 and G2 public keys +3. Generates the proof of possession signature +4. Outputs the data in the correct format for the `addKeysToProvider` function + +The public keystore file (`keyN_staker_output.json`) contains the data you'll use for provider registration. + +:::warning Key security +The private keystore (`keyN.json`) and the mnemonic that generated it must stay secret. Anyone holding them can commit slashable offenses and redirect block rewards; they cannot steal the delegated stake itself. If you lose them, the keys cannot be rotated: the sequencer stays offline and is slashed for inactivity until it is ejected. Slashing can reduce the delegated stake below the ejection threshold, ending the delegation. Further slashes can still apply during the withdrawal delay. See [Slashing](../concepts/slashing.md) for penalty amounts and the activation and ejection thresholds. +::: + +For more details on keystore creation, see the [Sequencer Setup Guide](./sequencer-setup.md#generating-keys). + +### Building the Registration Command + +You have two options for constructing the `addKeysToProvider` command: + +**Option 1: Use the helper script (Recommended)** + +Use this helper script to automatically build the command from your `validator-keys` output: + +https://gist.github.com/koenmtb1/1b665d055fbc22581c288f90cdc60d88 + +The script reads the JSON output from `validator-keys staker` and constructs the properly formatted `cast send` command. + +**Option 2: Manual construction** + +If you need to manually construct the command, the function signature is: + +```solidity +addKeysToProvider(uint256,(address,(uint256,uint256),(uint256,uint256,uint256,uint256),(uint256,uint256))[]) +``` + +**Parameters:** +- First `uint256`: Your provider identifier (from registration in Step 1) +- Tuple array: `KeyStore[]` where each element contains: + - `address`: Sequencer address + - `(uint256,uint256)`: publicKeyG1 (x, y coordinates) + - `(uint256,uint256,uint256,uint256)`: publicKeyG2 (x0, x1, y0, y1 coordinates) + - `(uint256,uint256)`: proofOfPossession (x, y coordinates) + +Example with placeholder values: + +```bash +cast send $STAKING_REGISTRY_ADDRESS \ + "addKeysToProvider(uint256,(address,(uint256,uint256),(uint256,uint256,uint256,uint256),(uint256,uint256))[])" \ + $YOUR_PROVIDER_IDENTIFIER \ + "[(0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb,(12345,67890),(11111,22222,33333,44444),(98765,43210))]" \ + --rpc-url $RPC_URL \ + --private-key $ADMIN_PRIVATE_KEY +``` + +**Important:** +- Replace all values above with actual data from `aztec validator-keys new --staker-output` +- Add a maximum of 100 keystores per transaction to avoid gas limit issues +- Verify each keystore is unique before adding to prevent duplicate key issues + +### Step 3: Set Your Metadata + +To be featured on the staking dashboard, submit metadata about your provider. + +**Required metadata:** +- Provider name and description +- Contact email +- Logo image (PNG or SVG, recommended size: 256x256px) +- Website URL +- Discord username +- Your `providerIdentifier` + +**Submission process:** + +#if(testnet) +1. Copy [`_example.json`](https://github.com/AztecProtocol/staking-dashboard/blob/master/providers-testnet/_example.json) from the [`providers-testnet`](https://github.com/AztecProtocol/staking-dashboard/tree/master/providers-testnet) folder in the [staking-dashboard GitHub repository](https://github.com/AztecProtocol/staking-dashboard). +2. Rename it to `{providerId}-{your-provider-name}.json` (e.g. `42-my-provider.json`), where `providerId` matches your on chain registration. +3. Fill in your metadata and open a pull request adding the file to the `providers-testnet` folder. +#else +1. Copy [`_example.json`](https://github.com/AztecProtocol/staking-dashboard/blob/master/providers/_example.json) from the [`providers`](https://github.com/AztecProtocol/staking-dashboard/tree/master/providers) folder in the [staking-dashboard GitHub repository](https://github.com/AztecProtocol/staking-dashboard). +2. Rename it to `{providerId}-{your-provider-name}.json` (e.g. `42-my-provider.json`), where `providerId` matches your on chain registration. +3. Fill in your metadata and open a pull request adding the file to the `providers` folder. +#endif + +The JSON file should follow this format: + +```json +{ + "providerId": 0, + "providerName": "", + "providerDescription": "", + "providerEmail": "", + "providerWebsite": "", + "providerLogoUrl": "", + "discordUsername": "", + "providerSelfStake": ["0x..."] +} +``` + +The `providerId` must match your on chain registration and be unique across all submissions. The `providerSelfStake` field is an optional array of attester addresses for sequencers receiving direct provider funding. + +Good metadata helps delegators understand your offering and builds trust. + +## After Delegation: Configure Sequencer Coinbase + +Once a delegator stakes to your provider, the system creates a Split contract for that delegation and activates the corresponding sequencer. **Configure the sequencer to use the Split contract address as the coinbase.** + +### Why This Matters + +The coinbase address determines where your sequencer's block rewards are sent. Setting it to the Split contract address ensures rewards are distributed according to your agreed commission rate, which is critical for maintaining trust with your delegators. + +### How to Configure the Coinbase + +Update the `coinbase` field in your sequencer node's keystore configuration to the Split contract address created for this delegation. + +**Example keystore configuration:** + +```json +{ + "schemaVersion": 1, + "validators": [ + { + "attester": { + "eth": "0x...", // Your Ethereum sequencer private key + "bls": "0x..." // Your BLS sequencer private key + }, + "publisher": ["0x..."], // Address that submits blocks to L1 + "coinbase": "0x[SPLIT_CONTRACT_ADDRESS]", // Split contract for this delegation + "feeRecipient": "0x0000000000000000000000000000000000000000000000000000000000000000" // Not currently used, set to all zeros + } + ] +} +``` + +Replace `[SPLIT_CONTRACT_ADDRESS]` with the actual Split contract address created for this delegation. You can find this address in the staking dashboard (see "Finding Your Split Contract Address" below). + +For detailed information about keystore configuration, including different storage methods and advanced patterns, see the [Advanced Keystore Guide](../keystore/index.md). + +### Finding Your Split Contract Address + +**You have to manually monitor the delegations you receive and update the `coinbase` address to the correct Split contract!** + +You can retrieve the Split contract address for a specific delegation through the **Staking Dashboard**: + +1. Navigate to your provider dashboard on the staking dashboard +2. Look for the dropdown called **"Sequencer Registered (x)"** where x is the number of registered sequencers +3. Click on the dropdown to expand it +4. This shows the Sequencer address → Split contract relation +5. Set the Split contract as the `coinbase` for the respective Sequencer address on your node + +The dropdown will display a table showing which Split contract corresponds to each of your sequencer addresses, making it easy to configure the correct coinbase for each sequencer. + +**Manual monitoring approach:** + +Since coinbase configuration must be done manually, you should: +- Regularly check the staking dashboard for new delegations +- Set up alerts or scheduled checks (daily or more frequently during high activity) +- Update keystore configurations promptly when new delegations appear +- Maintain a record of which Split contracts map to which keystores + +### Important Notes + +- **Monitor delegations actively**: The system does not automatically notify you of new delegations +- Configure the coinbase immediately after each delegation to ensure rewards flow correctly from the start +- Each delegation creates a unique Split contract—configure each sequencer with its specific Split contract address +- Restart your sequencer node after updating the keystore for changes to take effect +- Keep a mapping of sequencer addresses to Split contracts for operational tracking + +## Monitoring Keystore Availability + +As a provider, you must maintain sufficient sequencer identities (keystores) in your queue to handle incoming delegations. When a delegator stakes to your provider and your queue is empty, they cannot activate a sequencer—this results in a poor delegator experience and lost opportunity. + +### Why Monitoring Matters + +Each time a delegator stakes to your provider: +1. One keystore is dequeued from your provider queue +2. A sequencer is activated using that keystore +3. Your available keystore count decreases by one + +If your queue runs empty, new delegations cannot activate sequencers until you add more keystores. This could cause delegators to choose other providers. + +### Checking Available Keystores + +Check your current keystore queue with this call: + +```bash +# Check provider queue length +cast call [STAKING_REGISTRY_ADDRESS] \ + "getProviderQueueLength(uint256) (uint256)" \ + [YOUR_PROVIDER_IDENTIFIER] \ + --rpc-url [RPC_URL] + +This returns your provider's queue length, which is the number of keystores currently available. + +### Setting Up Automated Monitoring + +Implement automated monitoring to alert you when your keystore queue runs low. + +#### Cron Job Example + +The following script monitors your keystore queue and alerts when it drops below a threshold. Replace the placeholder values and uncomment your preferred alert method (webhook or email): + +```bash +#!/bin/bash +# check-keystores.sh + +THRESHOLD=5 # Alert when fewer than 5 keystores remain +REGISTRY_ADDRESS="[STAKING_REGISTRY_ADDRESS]" +PROVIDER_ID="[YOUR_PROVIDER_IDENTIFIER]" +RPC_URL="[YOUR_RPC_URL]" +WEBHOOK_URL="[YOUR_WEBHOOK_URL]" # Optional: for Slack/Discord notifications + +# Gets current queue length +QUEUE_LENGTH=$(cast call "$REGISTRY_ADDRESS" \ + "getProviderQueueLength(uint256)" \ + "$PROVIDER_ID" \ + --rpc-url "$RPC_URL") + +echo "Queue length: $QUEUE_LENGTH" + +# Check if queue is running low +if [ "$QUEUE_LENGTH" -lt "$THRESHOLD" ]; then + echo "WARNING: Keystore queue running low! Only $QUEUE_LENGTH keystores remaining." + + # Send alert (uncomment and configure your preferred method) + # Slack/Discord webhook: + # curl -X POST "$WEBHOOK_URL" -H "Content-Type: application/json" \ + # -d "{\"text\":\"⚠️ Keystore queue low: $QUEUE_LENGTH remaining (threshold: $THRESHOLD)\"}" + + # Email via mail command: + # echo "Keystore queue has $QUEUE_LENGTH keys remaining" | mail -s "Low Keystore Alert" your-email@example.com +fi +``` + +Make the script executable and schedule it with cron: + +```bash +# Make the script executable +chmod +x /path/to/check-keystores.sh + +# Edit crontab +crontab -e + +# Add this line to check every 4 hours +0 */4 * * * /path/to/check-keystores.sh >> /var/log/keystore-monitor.log 2>&1 +``` + +### When to Add More Keystores + +Add keystores proactively before running out: +- Monitor your delegation growth rate +- Add in batches (max 100 per transaction) +- Stay ahead of demand during high-activity periods + +See [Step 2: Add Sequencer Identities](#step-2-add-sequencer-identities) for instructions. + +## Managing Your Provider + +Update your provider configuration using these functions. All must be called from your `providerAdmin` address. + +### Update Admin Address + +Transfer provider administration to a new address: + +```bash +cast send [STAKING_REGISTRY_ADDRESS] \ + "updateProviderAdmin(uint256,address)" \ + [YOUR_PROVIDER_IDENTIFIER] \ + [NEW_ADMIN_ADDRESS] \ + --rpc-url [RPC_URL] \ + --private-key [CURRENT_ADMIN_PRIVATE_KEY] +``` + +### Update Rewards Recipient + +Change the address receiving commission payments: + +```bash +cast send [STAKING_REGISTRY_ADDRESS] \ + "updateProviderRewardsRecipient(uint256,address)" \ + [YOUR_PROVIDER_IDENTIFIER] \ + [NEW_REWARDS_RECIPIENT_ADDRESS] \ + --rpc-url [RPC_URL] \ + --private-key [ADMIN_PRIVATE_KEY] +``` + +### Update Commission Rate + +Modify your commission rate (applies only to new delegations): + +```bash +cast send [STAKING_REGISTRY_ADDRESS] \ + "updateProviderTakeRate(uint256,uint16)" \ + [YOUR_PROVIDER_IDENTIFIER] \ + [NEW_RATE_BASIS_POINTS] \ + --rpc-url [RPC_URL] \ + --private-key [ADMIN_PRIVATE_KEY] +``` + +:::warning Commission Changes Only Apply to New Delegations +When you update your commission rate, only **new delegations** will use the updated rate. **Existing delegations cannot be changed**—they permanently retain the original commission rate that was agreed upon when the delegation was created. +::: + +## Verification + +Verify your setup is working correctly. + +### Check Provider Registration + +Query the StakingRegistry to confirm your provider details: + +```bash +cast call [STAKING_REGISTRY_ADDRESS] \ + "providerConfigurations(uint256) (address,uint16,address)" \ + [YOUR_PROVIDER_IDENTIFIER] \ + --rpc-url [RPC_URL] +``` + +This returns: +1. The provider's admin address +2. The provider's commission rate in bps +3. The provider's rewards recipient + +### Verify Queue Length + +Check your provider queue length: + +```bash +cast call [STAKING_REGISTRY_ADDRESS] \ + "getProviderQueueLength(uint256)" \ + [YOUR_PROVIDER_IDENTIFIER] \ + --rpc-url [RPC_URL] +``` + +### Monitor Delegations + +View these metrics on the staking dashboard: +- Total stake delegated to your provider +- Number of active sequencers +- Commission earned +- Provider performance metrics + +### Confirm Node Operation + +Ensure your sequencer nodes are running and synced. See [Useful Commands](../sequencer-management/useful-commands.md) for commands to check sequencer status. + +## Troubleshooting + +### Registration transaction fails + +**Issue**: The `registerProvider` transaction reverts or fails. + +**Solutions**: +- Ensure your wallet has sufficient ETH for gas fees +- Verify the StakingRegistry contract address is correct +- Check that the commission rate is within acceptable bounds (typically 0-10000 basis points) +- Review transaction logs for specific error messages using a block explorer + +### Cannot add sequencer identities + +**Issue**: The `addKeysToProvider` function fails. + +**Solutions**: +- Confirm you're calling from the `providerAdmin` address +- Verify your `providerIdentifier` is correct +- Ensure BLS signatures in `KeyStore` are properly formatted (use the keystore creation utility) +- Check that the sequencer addresses aren't already registered elsewhere +- Reduce batch size if hitting gas limits (max 100 keystores per transaction) + +### No delegators appearing + +**Issue**: No delegators are staking to your provider. + +**Solutions**: +- Verify your provider is visible on the staking dashboard +- Complete all metadata fields to build trust +- Ensure your commission rate is competitive with other providers +- Confirm your sequencer nodes are operational and performing well +- Engage with the community on Discord to build your reputation + +### Commission not being received + +**Issue**: Commission payments aren't arriving at the rewards recipient address. + +**Solutions**: +- Verify the `providerRewardsRecipient` address is correct +- Check that delegations are active and generating fees +- Confirm your sequencers are producing blocks and earning fees +- Allow time for reward distribution (may not be immediate) +- Check the contract for pending distributions that need to be claimed + +## Best Practices + +**Maintain Sufficient Keystores**: Set up automated monitoring to ensure your keystore queue never runs empty. See [Monitoring Keystore Availability](#monitoring-keystore-availability) for guidance on implementing alerts. + +**Communicate Changes**: Inform delegators about commission rate changes, planned maintenance, or infrastructure updates. Good communication builds trust. + +**Monitor Performance**: Track your sequencers' attestation rates, block proposals, and uptime. Poor performance may cause delegators to withdraw. + +**Secure Your Keys**: The `providerAdmin` key controls your provider configuration. Store it securely and consider using a hardware wallet or multisig. + +## Next Steps + +After completing this setup: + +1. Monitor your provider's performance through the staking dashboard +2. Maintain high uptime for your sequencer nodes +3. Keep open communication with delegators +4. Regularly add new keystores to your provider queue (see [Monitoring Keystore Availability](#monitoring-keystore-availability)) +5. Join the [Aztec Discord](https://discord.gg/aztec) for provider support and community discussions diff --git a/docs/lines/v6/docs-operate/operators/setup/syncing-best-practices.md b/docs/lines/v6/docs-operate/operators/setup/syncing-best-practices.md new file mode 100644 index 00000000000..1373ef0e52a --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/setup/syncing-best-practices.md @@ -0,0 +1,250 @@ +--- +id: syncing_best_practices +displayed_sidebar: operatorsSidebar +title: Using and uploading snapshots +description: Learn sync modes and snapshot strategies to efficiently sync your Aztec node with the network. +references: ["labs/yarn-project/node-lib/src/actions/snapshot-sync.ts", "labs/yarn-project/node-lib/src/actions/upload-snapshot.ts", "labs/yarn-project/node-lib/src/config/index.ts", "labs/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts"] +--- + +## Overview + +All nodes on the Aztec network must download and synchronize the blockchain state before they can operate. This guide covers different sync modes, including how to use snapshots for faster synchronization and how to create your own snapshots. + +:::tip Automatic Configuration +When using `--network [NETWORK_NAME]`, snapshot URLs are automatically configured for you. Most users don't need to manually set snapshot sources. +::: + +## Understanding sync modes + +Nodes can synchronize state in two ways: + +1. **L1 sync**: Queries the rollup and data availability layer for historical state directly from Layer 1 +2. **Snapshot sync**: Downloads pre-built state snapshots from a storage location for faster synchronization + +Since Aztec uses blobs, syncing from L1 requires an archive node that stores complete blob history from Aztec's deployment. Snapshot sync is significantly faster, doesn't require archive nodes, and reduces load on L1 infrastructure, making it the recommended approach for most deployments. + +## Prerequisites + +Before proceeding, you should: + +- Have the Aztec node software installed +- Understand basic node operation +- For uploading snapshots: Have access to cloud storage (Google Cloud Storage, Amazon S3, or Cloudflare R2) with appropriate permissions + +## Using snapshots to sync your node + +### Configuring sync mode + +Control how your node synchronizes using the `SYNC_MODE` environment variable in your `.env` file: + +```bash +aztec start --node --sync-mode [MODE] +SYNC_MODE=[MODE] +``` + +Available sync modes: + +- **`snapshot`**: Downloads and uses a snapshot only if no local data exists (default behavior) +- **`force-snapshot`**: Downloads and uses a snapshot even if local data exists, overwriting it +- **`l1`**: Syncs directly from Layer 1 without using snapshots + +### Setting the snapshot source + +By default, nodes use Aztec's official snapshot storage. To specify a custom snapshot location, add the `SNAPSHOTS_URL` environment variable to your `.env` file: + +```bash +SYNC_MODE=snapshot +SNAPSHOTS_URL=[BASE_URL] +``` + +The node searches for the snapshot index at: +``` +[BASE_URL]/aztec-[L1_CHAIN_ID]-[VERSION]-[ROLLUP_ADDRESS]/index.json +``` + +**Supported storage backends**: +- **Google Cloud Storage** - `gs://bucket-name/path/` +- **Amazon S3** - `s3://bucket-name/path/` +- **Cloudflare R2** - `s3://bucket-name/path/?endpoint=https://[ACCOUNT_ID].r2.cloudflarestorage.com` +- **HTTP/HTTPS** - `https://host/path` +- **Local filesystem** - `file:///absolute/path` + +**Default snapshot locations by network**: + +- **Mainnet**: `https://aztec-labs-snapshots.com/mainnet/` +- **Testnet**: `https://aztec-labs-snapshots.com/testnet/` +- **Staging networks**: Configured via network metadata + +### Using custom snapshot sources + +You can configure your node to use custom snapshot sources for various use cases. + +Add the following to your `.env` file: + +**Google Cloud Storage:** +```bash +SYNC_MODE=force-snapshot +SNAPSHOTS_URL=gs://my-snapshots/ +``` + +**Cloudflare R2:** +```bash +SYNC_MODE=snapshot +SNAPSHOTS_URL=s3://my-bucket/snapshots/?endpoint=https://[ACCOUNT_ID].r2.cloudflarestorage.com +``` +Replace `[ACCOUNT_ID]` with your Cloudflare account ID. + +**HTTP/HTTPS mirror:** +```bash +SYNC_MODE=snapshot +SNAPSHOTS_URL=https://my-mirror.example.com/snapshots/ +``` + +Then add the environment variables to your `docker-compose.yml`: + +```yaml +environment: + # ... other environment variables + SYNC_MODE: ${SYNC_MODE} + SNAPSHOTS_URL: ${SNAPSHOTS_URL} +``` + +## Creating and uploading snapshots + +You can create snapshots of your node's state for backup purposes or to share with other nodes. This is done by calling the `aztecAdmin_startSnapshotUpload` method on the node admin API. + +### How snapshot upload works + +When triggered, the upload process: + +1. Pauses node syncing temporarily +2. Creates a backup of the archiver and world-state databases +3. Uploads the backup to the specified storage location +4. Resumes normal operation + +### Uploading a snapshot + +Use the node admin API to trigger a snapshot upload. You can upload to Google Cloud Storage, Amazon S3, or Cloudflare R2 by specifying the appropriate storage URI. + +**Example command**: + +**Upload to Google Cloud Storage:** +```bash +docker exec -it aztec-node curl -XPOST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -d '{ + "method": "aztecAdmin_startSnapshotUpload", + "params": ["gs://your-bucket/snapshots/"], + "id": 1, + "jsonrpc": "2.0" + }' +``` + +**Upload to Amazon S3:** +```bash +docker exec -it aztec-node curl -XPOST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -d '{ + "method": "aztecAdmin_startSnapshotUpload", + "params": ["s3://your-bucket/snapshots/"], + "id": 1, + "jsonrpc": "2.0" + }' +``` + +**Upload to Cloudflare R2:** +```bash +docker exec -it aztec-node curl -XPOST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -d '{ + "method": "aztecAdmin_startSnapshotUpload", + "params": ["s3://your-bucket/snapshots/?endpoint=https://[ACCOUNT_ID].r2.cloudflarestorage.com"], + "id": 1, + "jsonrpc": "2.0" + }' +``` + +Replace `aztec-node` with your container name and `[ACCOUNT_ID]` with your Cloudflare account ID. + +**Note**: Ensure your storage credentials are configured before uploading: +- **Google Cloud Storage**: Set up [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) +- **Amazon S3 / Cloudflare R2**: Set environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` + +### Scheduling regular snapshots + +For continuous backup, schedule the upload command to run at regular intervals using cron or a similar scheduler. The frequency depends on how current you need your snapshots to be. + +Once uploaded, other nodes can download these snapshots by configuring their `--snapshots-url` to point to your storage location. + +## Verification + +To verify your sync configuration is working: + +### For snapshot downloads + +1. **Check startup logs**: Look for messages indicating snapshot download progress +2. **Monitor sync time**: Snapshot sync should be significantly faster than L1 sync +3. **Verify state completeness**: Confirm your node has the expected block height after sync +4. **Check data directories**: Ensure the archiver and world-state databases are populated + +### For snapshot uploads + +1. **Check API response**: The upload command should return a success response +2. **Monitor logs**: Watch for upload progress messages in the node logs +3. **Verify storage**: Check your storage bucket to confirm the snapshot files exist +4. **Validate index file**: Ensure the `index.json` file is created at the expected path +5. **Test download**: Try downloading the snapshot with another node to confirm it works + +## Troubleshooting + +### Snapshot download fails + +**Issue**: Node cannot download snapshot from the specified URL. + +**Solutions**: +- Verify the `--snapshots-url` is correct and accessible +- Check network connectivity to the storage location +- Confirm the snapshot index file exists at the expected path +- Review node logs for specific error messages +- Try using Aztec's default snapshot URL to isolate custom URL issues + +### Snapshot upload fails + +**Issue**: The `aztecAdmin_startSnapshotUpload` command returns an error. + +**Solutions**: +- Verify storage credentials are properly configured (Google Cloud, AWS, or Cloudflare R2) +- Check that the specified bucket exists and you have write permissions +- Confirm sufficient disk space is available for creating the backup +- Review node logs for detailed error messages +- For S3/R2: Ensure environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are set + +### Storage space issues + +**Issue**: Running out of disk space during sync or snapshot creation. + +**Solutions**: +- Ensure sufficient disk space (at least 2x the expected database size for snapshots) +- Clean up old snapshots or data if running recurring uploads +- Monitor disk usage and set up alerts +- Consider using a larger volume or adding storage + +## Best practices + +- **Use snapshot sync for production**: Snapshot sync is significantly faster and more efficient than L1 sync +- **Choose the right storage backend**: + - Google Cloud Storage for simplicity and GCP integration + - Amazon S3 for AWS infrastructure integration + - Cloudflare R2 for cost-effective public distribution (free egress) +- **Schedule regular snapshots**: Create snapshots at regular intervals if running critical infrastructure +- **Test snapshot restoration**: Periodically verify that your snapshots download and restore correctly +- **Monitor storage costs**: Implement retention policies to manage cloud storage costs +- **Keep snapshots current**: Older snapshots require more time to sync to the current state +- **Use `force-snapshot` sparingly**: Only use when you need to reset to a known state, as it overwrites local data + +## Next Steps + +- Learn about [running bootnodes](./bootnode-operation.md) for improved peer discovery +- Set up [monitoring](../monitoring/index.md) to track your node's sync progress +- Check the [CLI reference](../reference/cli-reference.md) for additional sync-related options +- Join the [Aztec Discord](https://discord.gg/aztec) for sync optimization tips diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/configure-environment.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/configure-environment.mdx new file mode 100644 index 00000000000..4d83a63321b --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/configure-environment.mdx @@ -0,0 +1,80 @@ +--- +title: "Configure environment" +description: "Create the project directory and a single .env file that holds every variable your Aztec sequencer needs." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/aztec/src/cli/aztec_start_options.ts", "labs/yarn-project/node-keystore/src/config.ts", "labs/yarn-project/p2p/src/config.ts", "labs/yarn-project/foundation/src/config/env_var.ts"] +--- + +# Configure environment + +**Time:** ~5 minutes. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="self" /> + +<OperatorConfig /> + +## 1. Create the directory + +```bash +mkdir -p aztec-sequencer/data && cd aztec-sequencer +``` + +## 2. Copy your keystore into the project + +Copy only the node keystore files (`key1.json`, `key2.json`, ...) into the directory the node loads. Do **not** copy the `*_staker_output.json` files. + +```bash +mkdir -p ./keys +cp ~/.aztec/keystore/key*.json ./keys +rm -f ./keys/*_staker_output.json +``` + +:::warning Keep staker output files out of `KEY_STORE_DIRECTORY` +The node loads every `.json` file in `KEY_STORE_DIRECTORY` and expects each one to be a node keystore. The `key*_staker_output.json` files generated by `aztec validator-keys new --staker-output` are a different format (used for registration, not for running the node), so leaving one in this directory makes the node fail to start. The `rm` above removes them after the copy. +::: + +## 3. Create the `.env` file + +<ConfigCode language="bash" title=".env">{`# ── Aztec Sequencer Configuration ── + +# Data & Keys +DATA_DIRECTORY=./data +KEY_STORE_DIRECTORY=./keys +LOG_LEVEL=info + +# Ethereum L1 endpoints +ETHEREUM_HOSTS={{ETH_RPC}} +L1_CONSENSUS_HOST_URLS={{CONSENSUS_RPC}} +ETHEREUM_DEBUG_HOSTS={{DEBUG_RPC}} + +# P2P networking +P2P_IP={{P2P_IP}} +P2P_PORT=40400 + +# Aztec ports +AZTEC_PORT=8080 +AZTEC_ADMIN_PORT=8880`}</ConfigCode> + +:::tip Don't know your external IP? +Run `curl ipv4.icanhazip.com`. +::: + +:::warning Never expose port 8880 +Port 8880 is the **admin port**: it reads and changes node configuration. It requires an API key by default, and it must still never be exposed. It is intentionally not mapped in the Docker Compose file in the next step. +::: + +<details open> +<summary>What does each variable do?</summary> + +- **`DATA_DIRECTORY`** is where the node stores blockchain data. The directory grows over time. +- **`KEY_STORE_DIRECTORY`** is the path to your validator keystore (the `key1.json` file). +- **`ETHEREUM_HOSTS`** is your L1 execution client. The node reads rollup contract state and submits proofs here. +- **`L1_CONSENSUS_HOST_URLS`** is your L1 consensus client (Beacon API). Needed for blob data access. +- **`ETHEREUM_DEBUG_HOSTS`** is a trace-capable endpoint (`debug_traceTransaction`). It is optional: if you leave it unset, the node uses `ETHEREUM_HOSTS` for trace calls, so set it separately only if your trace-capable endpoint differs from your main execution endpoint. Whichever endpoint the node traces with must serve trace methods: the node refuses to start otherwise, unless you set `ETHEREUM_ALLOW_NO_DEBUG_HOSTS=true`. +- **`P2P_IP`** is your server's public IP so other nodes can connect to you. +- **`P2P_PORT 40400`** is the port for peer-to-peer communication, both TCP and UDP. It must be forwarded through your firewall. + +</details> + diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/dappnode.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/dappnode.mdx new file mode 100644 index 00000000000..4d0afeba880 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/dappnode.mdx @@ -0,0 +1,50 @@ +--- +title: "DappNode setup" +description: "Run your solo sequencer on a DappNode box: the Aztec package replaces the configure, compose, and start steps of the manual guide." +displayed_sidebar: operatorsSidebar +--- + +# DappNode setup + +[DappNode](https://dappnode.com/) is a home-server platform for running blockchain nodes through a web UI instead of a terminal. The community-maintained [Aztec package](https://github.com/dappnode/DAppNodePackage-aztec-generic) runs the sequencer as a one-click install, replacing the [Configure environment](./configure-environment), [Docker Compose](./docker-compose), and [Start the node](./start-node) steps of this guide. Keystore generation, funding, and registration stay the same. + +A DappNode box that already runs your own Ethereum node is the standout combination: the sequencer points at the execution and consensus clients on the same machine, so there is no third-party RPC dependency, no rate limits, and no container-to-host networking to configure. + +<ForceStakeMode mode="self" /> + +## Before you start + +- Your box must meet the [sequencer hardware floor](./pre-flight#hardware) on top of whatever your Ethereum node already uses. +- Run through [Pre-flight](./pre-flight) for the network, reliability, and token requirements; they apply unchanged. + +## 1. Install the Aztec package + +Open the DAppStore on your DappNode and install the **Aztec** package. Pick the variant matching your network (mainnet or Sepolia testnet). + +## 2. Point it at L1 + +The setup wizard asks for two values: + +- **Ethereum RPC hosts**: your execution client. The package pre-fills the box's own execution client through DappNode's staker alias: `http://execution.mainnet.dncore.dappnode:8545` on mainnet, `http://execution.sepolia.dncore.dappnode:8545` on testnet. Any external RPC also works. +- **L1 consensus host URLs**: your beacon client. Pre-filled as `http://beaconchain.mainnet.dncore.dappnode:3500` on mainnet, `http://beaconchain.sepolia.dncore.dappnode:3500` on testnet. + +:::warning Blob-serving beacon required +Your beacon client must serve blob sidecars, or the sequencer fails with blob sidecar errors and cannot sync. Run it as a supernode or semi-supernode (for example Lighthouse v8.0.0 or later, or Prysm v7.10 or later, with `--super-node` or `--semi-supernode`). This applies on both mainnet and testnet. See [L1 RPC requirements](../concepts/l1-rpc#the-blob-serving-consensus-client) for details. +::: + +## 3. Generate and upload your keystore + +Generate the keystore on any machine, for example your laptop. Install the Aztec CLI there ([Install Aztec toolchain](./install-toolchain)), then generate the keystore exactly as in [Generate keystore](./generate-keystore#3-generate-the-validator-keystore). Then upload the private keystore file (`key1.json`) to the Aztec package through the DappNode **file manager**, into the package's `/keystore` directory. + +## 4. Fund, register, verify + +These steps are identical to the manual guide: + +- [Fund your accounts](./fund-accounts) for the publisher top-up and self-stake. +- [Register as sequencer](./register-sequencer) through the staking dashboard, using the public keystore file (`key1_staker_output.json`) from step 3. +- [Verify everything works](./verify): the explorer check and P2P check apply unchanged. For container health, use the package's logs and status in the DappNode UI. + +## Caveats + +- **Single box only.** The package does not support the [HA topology](/operate/operators/setup/high_availability_sequencers); for redundancy you need the manual setup. +- **Community-maintained.** The package is maintained by DappNode, not by Aztec. The [package repository](https://github.com/dappnode/DAppNodePackage-aztec-generic) is the place for package-specific issues. diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/day-2-essentials.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/day-2-essentials.mdx new file mode 100644 index 00000000000..e8d23c2c2c2 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/day-2-essentials.mdx @@ -0,0 +1,86 @@ +--- +title: "Updates, alerts, rewards" +description: "Set up version upgrades, monitoring alerts, and reward claiming for your live sequencer, plus the reference commands and links for daily operation." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/telemetry-client/src/*"] +--- + +# Updates, alerts, rewards + +**Time:** ~5 minutes. Your sequencer is running. Set up three things before you call it done: version upgrades, monitoring alerts, and reward claiming. The rest of this page is reference material to keep handy. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="self" /> + +<OperatorConfig /> + +## 1. Stay current on upgrades + +When a new version is released: + +```bash +# Pull new image and restart +docker compose pull && docker compose up -d + +# Verify version +docker compose logs aztec-sequencer | head -20 +``` + +:::warning Watch for upgrade announcements +Some upgrades require action within a specific timeframe; a node left on an old version eventually stops attesting. Watch the channels in [Where to stay informed](#where-to-stay-informed) below, especially Discord, where time-sensitive upgrade notices are posted first. +::: + +## 2. Set up monitoring and alerts + +Aztec exports metrics via **OpenTelemetry**. The full monitoring stack: + +- **OpenTelemetry Collector** receives metrics from the node via OTLP. +- **Prometheus** stores time-series metrics. +- **Grafana** renders dashboards and alerts. + +Key metrics to watch: + +- Attestation rate (target: 99%+) +- Block proposals +- Committee participation +- Peer count +- Disk usage + +:::tip Quick start +The [monitoring installer](/operate/operators/concepts/monitoring#set-up-monitoring-with-the-installer) sets up the whole stack (dashboard, metrics, and Telegram alerts) from one script. +::: + +See [Monitoring and metrics](/operate/operators/concepts/monitoring) for the installer walkthrough and what to alert on. + +## 3. Claim rewards + +Two paths: + +- **[Aztec staking dashboard](https://stake.aztec.network/my-position)** — aggregates pending rewards across every sequencer you operate, claims in one wallet flow. Easiest for most operators. +- **CLI claim** — see the [Claiming Rewards guide](/operate/operators/sequencer-management/claiming-rewards) for the full reference (pending-rewards check, `claimSequencerRewards` cast send, keystore-file and hardware-wallet variants, gas-limit troubleshooting). + + +## Slashing awareness + +Slashing is tiered. Inactivity (failing to attest or propose when assigned) is slashed 2,000 AZTEC, and so is submitting a proposal with invalid attestations. Duplicate proposals and duplicate attestations are slashed 5,000 AZTEC. There is a 190,000 AZTEC ejection threshold below which the sequencer is ejected. See [Slashing](../concepts/slashing) for the full offense set and thresholds. + +## Where to stay informed + +- [Discord](https://discord.com/invite/aztec) — upgrade announcements and operator discussion. This is where version updates and time-sensitive notices are posted first. +- [Forum](https://forum.aztec.network) — longer-form proposals, governance, and operator announcements. +- [GitHub](https://github.com/AztecProtocol/aztec-packages) — source, issues, and [releases](https://github.com/AztecProtocol/aztec-packages/releases). +- [Changelog](/operate/operators/reference/changelog) — per-version operator-facing changes and migration notes. + +## Key links + +- [Operator overview](/operate/operators) (the rest of this section) +- [Network info and contract addresses](https://docs.aztec.network/networks) +- [Dashtec — sequencer analytics](https://dashtec.xyz) +- [AztecScan — block explorer](https://aztecscan.xyz) +- [CLI reference (1000+ flags)](/operate/operators/reference/cli-reference) +- [Node API reference (JSON-RPC)](/operate/operators/reference/node_api_reference) + +## You finished the guide + +Your sequencer is live. Focus on keeping the publisher funded, the L1 endpoints reliable, and keeping your node on the latest recommended version. diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/docker-compose.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/docker-compose.mdx new file mode 100644 index 00000000000..bdb34bf8fe5 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/docker-compose.mdx @@ -0,0 +1,163 @@ +--- +title: "Docker Compose" +description: "Create the docker-compose.yml that runs the Aztec node and sequencer. HA mode runs the same attester across two nodes coordinating via shared PostgreSQL." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/aztec/src/cli/cmds/start_node.ts", "labs/yarn-project/aztec/src/cli/aztec_start_options.ts", "labs/yarn-project/aztec/src/cli/cmds/migrate_ha_db.ts", "labs/yarn-project/stdlib/src/ha-signing/*", "labs/yarn-project/validator-client/src/key_store/ha_key_store.ts"] +--- + +# Docker Compose + +**Time:** ~3 minutes. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="self" /> + +<IfChoice axis="topology" is="single"> + +<Heading as="h2" id="single-server-compose">Single-server compose</Heading> + +Create `docker-compose.yml` in your `aztec-sequencer/` directory: + +<ConfigCode language="yaml" title="docker-compose.yml">{`services: + aztec-sequencer: + image: "azteclabs/aztec:{{IMAGE_TAG}}" + container_name: "aztec-sequencer" + ports: + - "\${AZTEC_PORT}:\${AZTEC_PORT}" + - "\${P2P_PORT}:\${P2P_PORT}" + - "\${P2P_PORT}:\${P2P_PORT}/udp" + volumes: + - \${DATA_DIRECTORY}:/var/lib/data + - \${KEY_STORE_DIRECTORY}:/var/lib/keys:ro + environment: + DATA_DIRECTORY: /var/lib/data + KEY_STORE_DIRECTORY: /var/lib/keys + LOG_LEVEL: \${LOG_LEVEL} + ETHEREUM_HOSTS: \${ETHEREUM_HOSTS} + L1_CONSENSUS_HOST_URLS: \${L1_CONSENSUS_HOST_URLS} + ETHEREUM_DEBUG_HOSTS: \${ETHEREUM_DEBUG_HOSTS} + P2P_IP: \${P2P_IP} + P2P_PORT: \${P2P_PORT} + AZTEC_PORT: \${AZTEC_PORT} + AZTEC_ADMIN_PORT: \${AZTEC_ADMIN_PORT} + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --node + --sequencer + --network {{NETWORK_NAME}} + networks: + - aztec + restart: always + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:'+process.env.AZTEC_PORT,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method:'aztec_getChainTips',params:[],id:1})}).then(r=>r.json()).then(j=>{var b=j&&j.result&&j.result.proven&&j.result.proven.block;var n=b&&b.number;if(typeof n!=='number'||n<=0)process.exit(1);return fetch('http://localhost:'+process.env.AZTEC_PORT,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method:'aztec_getSyncedL1Timestamp',params:[],id:1})}).then(r=>r.json()).then(j2=>{var ts=Number(j2&&j2.result);if(!ts||(Math.floor(Date.now()/1000)-ts)>300){process.exit(1)}process.exit(0)})}).catch(()=>process.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10m + +networks: + aztec: + name: aztec`}</ConfigCode> + +</IfChoice> + +<IfChoice axis="topology" is="ha" label="HA topology"> + +<Heading as="h2" id="ha-compose-same-attester-two-nodes-shared-postgresql">HA compose: same attester, two nodes, shared PostgreSQL</Heading> + +In HA mode you run **two near-identical sequencer nodes**, both holding the same attester key. They coordinate via a shared PostgreSQL database that ensures only one node signs each duty (block proposal, attestation, governance vote). If one node dies, the other keeps attesting without missing a duty and without risking double-signing. + +You'll need: + +1. A PostgreSQL database reachable by both nodes. Use a managed multi-zone service with automatic failover; a database on a single machine takes both nodes down with it. See [the HA database setup](/operate/operators/setup/high_availability_sequencers#step-6-set-up-the-ha-database) for provider examples. +2. The HA database schema initialized once before starting either node: + +<ConfigCode language="bash">{`aztec migrate-ha-db up --database-url postgresql://user:password@host:5432/validator_ha`}</ConfigCode> + +3. The compose file below, deployed to **both servers** with **different `VALIDATOR_HA_NODE_ID` values** (and different `P2P_IP` values per server's public IP). Everything else stays identical. + +<ConfigCode language="yaml" title="docker-compose.yml (deploy on both servers)">{`services: + aztec-sequencer: + image: "azteclabs/aztec:{{IMAGE_TAG}}" + container_name: "aztec-sequencer" + ports: + - "\${AZTEC_PORT}:\${AZTEC_PORT}" + - "\${P2P_PORT}:\${P2P_PORT}" + - "\${P2P_PORT}:\${P2P_PORT}/udp" + volumes: + - \${DATA_DIRECTORY}:/var/lib/data + - \${KEY_STORE_DIRECTORY}:/var/lib/keys:ro + environment: + DATA_DIRECTORY: /var/lib/data + KEY_STORE_DIRECTORY: /var/lib/keys + LOG_LEVEL: \${LOG_LEVEL} + ETHEREUM_HOSTS: \${ETHEREUM_HOSTS} + L1_CONSENSUS_HOST_URLS: \${L1_CONSENSUS_HOST_URLS} + ETHEREUM_DEBUG_HOSTS: \${ETHEREUM_DEBUG_HOSTS} + P2P_IP: \${P2P_IP} + P2P_PORT: \${P2P_PORT} + AZTEC_PORT: \${AZTEC_PORT} + AZTEC_ADMIN_PORT: \${AZTEC_ADMIN_PORT} + # HA coordination + VALIDATOR_HA_SIGNING_ENABLED: "true" + VALIDATOR_HA_DATABASE_URL: \${VALIDATOR_HA_DATABASE_URL} + VALIDATOR_HA_NODE_ID: \${VALIDATOR_HA_NODE_ID} + entrypoint: >- + node + --no-warnings + /usr/src/yarn-project/aztec/dest/bin/index.js + start + --node + --sequencer + --network {{NETWORK_NAME}} + networks: + - aztec + restart: always + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:'+process.env.AZTEC_PORT,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method:'aztec_getChainTips',params:[],id:1})}).then(r=>r.json()).then(j=>{var b=j&&j.result&&j.result.proven&&j.result.proven.block;var n=b&&b.number;if(typeof n!=='number'||n<=0)process.exit(1);return fetch('http://localhost:'+process.env.AZTEC_PORT,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method:'aztec_getSyncedL1Timestamp',params:[],id:1})}).then(r=>r.json()).then(j2=>{var ts=Number(j2&&j2.result);if(!ts||(Math.floor(Date.now()/1000)-ts)>300){process.exit(1)}process.exit(0)})}).catch(()=>process.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10m + +networks: + aztec: + name: aztec`}</ConfigCode> + +In each server's `.env`, add: + +```bash +VALIDATOR_HA_DATABASE_URL=postgresql://user:password@host:5432/validator_ha +VALIDATOR_HA_NODE_ID=validator-node-1 # use validator-node-2 on the second server +``` + +:::warning Same keystore on both nodes +Both nodes hold the same attester key. The shared Postgres lock is what stops them from both signing the same duty. Do not run HA without the database, or you risk double-signing and slashing. +::: + +:::info What HA does and doesn't do +HA gives you **redundancy for one attester identity**. If you have N validator identities and want to spread them across multiple nodes for load (rather than redundancy), that's a different setup, not what this section covers. See [High availability sequencers](/operate/operators/setup/high_availability_sequencers) for the canonical reference, including remote-signer patterns and database tuning. +::: + +</IfChoice> + +:::info Health check +The `healthcheck` block probes the node's own HTTP API every 30 seconds. The container reports: + +- **`starting`** during initial sync. The node answers its API but does not yet have a proven block, so the check holds off. The `start_period` of 10 minutes covers the snapshot download and catch-up before any failure counts. If your node typically takes longer to reach a proven tip, raise `start_period` to match. +- **`healthy`** once the node has a proven tip greater than 0 and the chain time it has synced to is within 5 minutes of now, meaning it is keeping up with network state. +- **`unhealthy`** if the API stops answering, the node falls more than 5 minutes behind the chain, or the proven tip drops back to 0 for three checks in a row. + +The status shows in `docker compose ps` and can be scraped by monitoring. The check **reports** health; it does not restart the container on its own. For automatic recovery on `unhealthy`, pair it with a tool such as `autoheal`, or alert on the status from your monitoring stack. + +The check requires both a proven L2 tip greater than 0 and a recent synced chain time (within 5 minutes), so it catches a node that synced once and then stalled. Adjust the `300` (seconds) in the probe to change that staleness window. +::: + +:::warning Admin port stays internal +Port 8880 is the admin port and is intentionally not mapped to the host. Reach it only via `docker exec` on the box itself. +::: + diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/fund-accounts.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/fund-accounts.mdx new file mode 100644 index 00000000000..576b66b8822 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/fund-accounts.mdx @@ -0,0 +1,113 @@ +--- +title: "Fund accounts" +description: "Fund the publisher with ETH and confirm your AZTEC self-stake is available." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/ethereum/src/l1_tx_utils/config.ts", "labs/yarn-project/ethereum/src/publisher_manager.ts", "labs/yarn-project/sequencer-client/src/publisher/*"] +--- + +# Fund accounts + +**Time:** ~5 minutes. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="self" /> + +<OperatorConfig /> + +## Fund the publisher + +The publisher account pays a small amount of L1 gas every time your node publishes a checkpoint. At current mainnet gas levels a comfortable opening top-up is around **0.01 ETH per sequencer**. + +Paste your L1 RPC URL into the calculator below to scan the last 30 days of network activity and get a runway estimate. Without an RPC the calculator still shows the per-call cost at each scenario, but cannot compute runway, because the propose-rate depends on the live validator-set size. + +<PublisherFundingCalculator /> + +<IfChoice axis="topology" is="ha" label="HA topology"> + +In HA mode the publisher runs on a separate server, but the publisher *address* you funded is the same. The account just needs to be reachable by whichever node is currently the active publisher. + +</IfChoice> + +:::warning Monitor publisher balance +If the publisher balance runs dry, your sequencer cannot publish blocks, and an inactive sequencer is a slashable offense. Set a refill threshold and an alert using `aztec_l1_publisher_balance_eth` and `aztec_l1_publisher_sends_affordable` from your Prometheus stack (see [Metrics reference](../monitoring/metrics-reference#l1-publisher-eth-balance)), leaving enough headroom to refill before the publisher dries up at the worst-case gas price you expect, or set up [automatic top-ups](#optional-automatic-top-ups). +::: + + +## Optional: automatic top-ups + +Your node can refill its publishers from a separate funding account, so a publisher does not run dry between manual top-ups. Once a minute the node reads every publisher's balance, and sends `PUBLISHER_FUNDING_AMOUNT` to each publisher below `PUBLISHER_FUNDING_THRESHOLD`, in a single transaction from the funding account. + +To set it up: + +1. Add a funding account to your keystore. It goes in the top-level `fundingAccount` field, next to `validators`, and takes a private key, or an address held by a remote signer. You can set it with the CLI: + + ```bash + aztec validator-keys set-funding-account [KEYSTORE_PATH] [FUNDING_PRIVATE_KEY_OR_ADDRESS] + ``` + + Pass `--remote-signer [URL]` for an address held by a remote signer, or `--password [PASSWORD]` to store the key encrypted. + +2. Set both environment variables on the node. Both are in ETH (for example `0.05`), and top-ups stay off unless both are set. + + | Env var | What it does | + |---|---| + | `PUBLISHER_FUNDING_THRESHOLD` | A publisher whose balance falls below this amount gets topped up. | + | `PUBLISHER_FUNDING_AMOUNT` | ETH sent to the publisher in each top-up. | + +3. Fund the funding account, and restart the node. + +Keep in mind: + +- The funding account must not also be one of your publishers. If it is, the node logs an error and disables top-ups. +- Set the threshold above the low-balance warning level, so publishers are refilled before the node warns that they are running low. The warning level is 4 times the cost of a proposal at current L1 fees by default (`PUBLISHER_LOW_BALANCE_WARNING_MULTIPLIER`). The node warns if the threshold is below it. +- The node warns when the funding account holds less than 10 top-ups, and logs an error when it cannot pay for any. Watch `aztec_l1_publisher_funder_balance_eth` to refill it in time. + +## Optional: tuning publisher gas settings + +The publisher uses sensible defaults, so most operators do not need to touch these. The settings below let you control what the publisher is willing to pay for L1 inclusion. All are environment variables set on the node. + +:::warning A gas-price cap does not stop the broadcast +`L1_GAS_PRICE_MAX` (gwei) caps the L1 gas price your publisher will pay, to avoid runaway costs during fee spikes. The trap: **the cap does not stop the transaction from being broadcast.** When L1 gas is above your cap, the publisher still sends the proposal at your capped max fee. It loses the inclusion race against higher-paying transactions and does not get mined, but the broadcast still consumes fees, so a cap set too low costs money without producing a block. It also silently misses proposals during every fee spike even on a healthy node (one operator capped near 1 gwei missed roughly 20% of proposals while attesting at 100%). Missed proposals are forgone block rewards now, and skip-block slashing is slated to return in a later upgrade. Size the cap against real L1 conditions using the gas prices the [funding calculator](#fund-the-publisher) reports, not a low round number. +::: + +| Setting | Env var | What it does | +|---|---|---| +| Max gas price | `L1_GAS_PRICE_MAX` | Ceiling on the L1 gas price (gwei) the publisher will pay. See the warning above. | +| Max blob fee | `L1_BLOB_FEE_PER_GAS_MAX` | Ceiling on the blob fee per gas (gwei). The same broadcast caveat applies. | +| Priority fee bump | `L1_PRIORITY_FEE_BUMP_PERCENTAGE` | How much to raise the priority fee on each send, to win inclusion. | +| Priority fee retry bump | `L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE` | Extra priority-fee increase on each retry of a stuck transaction. | +| Minimum priority fee | `L1_MINIMUM_PRIORITY_FEE_PER_GAS_GWEI` | Floor on the computed priority fee (gwei). | +| Gas-limit buffer | `L1_GAS_LIMIT_BUFFER_PERCENTAGE` | Headroom added to the estimated gas limit, to avoid underestimates. | + +If your proposals are frequently not getting included, the usual cause is a `L1_GAS_PRICE_MAX` set too low or priority fees too low for current L1 competition. Raise the cap and the priority-fee settings rather than lowering them. + +## Verify the balance + +<ConfigCode language="bash">{`cast balance {{PUBLISHER_ADDR}} --rpc-url {{ETH_RPC}} --ether`}</ConfigCode> + +## Stake / delegated stake + +You need **<AztecTotal /> AZTEC tokens** (200,000 × <Count /> sequencer<IfCount gt={1}>s</IfCount>) available in the wallet you'll connect to the staking dashboard when you [register as sequencer](./register-sequencer). Self-stake comes from this wallet directly. + +| Item | Cost (for <Count /> sequencer<IfCount gt={1}>s</IfCount>) | +|---|---| +| AZTEC self-stake | **<AztecTotal /> AZTEC** | +| Opening publisher top-up | **<EthTotal /> ETH** (see the calculator above for runway) | +| Registration gas | ~0.005 ETH per sequencer (rough) | + +## Set your reward and withdrawer addresses + +The **coinbase address** is the L1 (Ethereum) address that accumulates your sequencer rewards in the Rollup contract; you claim from it via `claimSequencerRewards(coinbase)`. The previous step's `aztec validator-keys new` command read this value from the **Coinbase Address** field in the configuration panel and baked it into `key1.json`. Confirm the panel still shows the wallet you intend to receive rewards. If the field is empty or wrong, fix it and rerun the keystore command before continuing. Coinbase can also be changed later by editing the `coinbase` field in `key1.json` and reloading the keystore (admin API call or node restart); doing it correctly at setup just avoids the cleanup. + +The **withdrawer address** is a separate role: it controls the stake. It can later call `initiateWithdraw` to exit, names the recipient of the stake for every exit (including exits started by the attester), signs governance delegations, and receives residual stake on slash-driven ejection. See [Exiting a sequencer](/operate/operators/sequencer-management/exiting-a-sequencer). Use a cold wallet or multisig you control. Paste it into the **Withdrawer Address** field in the configuration panel. + +For the full role map (caller / attester / publisher / withdrawer / coinbase / fee recipient), see the [keys-and-addresses deep-dive](./generate-keystore#the-keys-the-addresses-and-who-calls-what). + +<details open> +<summary>What just happened?</summary> + +You confirmed the publisher account has enough ETH for at least a few weeks of attestation gas, and you confirmed your AZTEC self-stake is available. Self-staking locks your 200K AZTEC per sequencer in the Rollup contract on registration. It's economic security: if your sequencer misbehaves (double-signing, persistent liveness failure), a portion gets slashed. + +</details> + diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/generate-keystore.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/generate-keystore.mdx new file mode 100644 index 00000000000..691e0357315 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/generate-keystore.mdx @@ -0,0 +1,193 @@ +--- +title: "Generate keystore" +description: "Create the publisher mnemonic and validator keystore. Action steps first, theory at the bottom." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/cli/src/cmds/validator_keys/*", "labs/yarn-project/node-keystore/src/schemas.ts"] +--- + +# Generate keystore + +**Time:** ~5 minutes. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="self" /> + +## 1. Generate a publisher mnemonic (24 words) + +```bash +cast wallet new-mnemonic --words 24 +``` + +:::danger Back up the mnemonic NOW, before anything else +**Write it down on paper, or store it on a hardware device. Do this before you continue.** + +This 24-word phrase is the **only** way to regenerate your publisher private key if you lose the server. **There is no recovery if you skip this step.** Lose the mnemonic, lose the funds in the publisher account, lose the ability to publish, get slashed for inactivity, lose your stake. + +Do **not**: +- Store it on the server running the node. +- Email it to yourself. +- Save it in a password manager that syncs to the cloud without a hardware-key second factor. +- Take a screenshot. +::: + +## 2. Derive the publisher private key + +```bash +cast wallet derive-key "your 24 word mnemonic phrase here" +``` + +Copy the returned private key into the **Publisher Private Key** field in the configuration panel. + +<OperatorConfig /> + +## 3. Generate the validator keystore + +The configuration panel says you'll register **<Count />** sequencer identit<IfCount eq={1}>y</IfCount><IfCount gt={1}>ies</IfCount>. Adjust the **Sequencer count** field above if that's wrong before running the command. + +<ConfigCode language="bash">{`aztec validator-keys new \\ + --fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \\ + --coinbase {{COINBASE_ADDR}} \\ + --staker-output \\ + --gse-address {{GSE_ADDR}} \\ + --l1-rpc-urls {{ETH_RPC}} \\ + --l1-chain-id {{L1_CHAIN_ID}} \\ + --publishers {{PUBLISHER_KEY}} \\ + --count {{COUNT}}`}</ConfigCode> + +The `--coinbase` flag is included by default. **Use a wallet you control that is not the attester key.** If you omit `--coinbase`, the CLI writes the attester ETH address into the keystore's `coinbase` field and rewards accumulate against the signing key. Coinbase can be changed later by editing the `coinbase` field in `key1.json` and reloading the keystore (admin API call or node restart), but doing it right at setup avoids the cleanup. + +<IfCount gt={1}> + +All <Count /> identities are derived from the same mnemonic using sequential derivation paths and end up in the same two files (`key1.json` and `key1_staker_output.json`). The public file is a JSON array with <Count /> objects, one per validator. + +</IfCount> + +This writes two files in `~/.aztec/keystore/`: + +- **`key1.json`** is the private keystore. Never share it, never commit it to git. +- **`key1_staker_output.json`** is the public keystore. Safe to share. You'll upload it to the staking dashboard when you [register as sequencer](./register-sequencer). + +:::tip Securing the keystore in production +This command stores private keys inline in `key1.json`. That's the simplest setup, but means anyone with read access to the file gets your keys. At a minimum, restrict file permissions (`chmod 600 ~/.aztec/keystore/key1.json`), use full-disk encryption, and limit who can SSH into the server. For more advanced storage (encrypted JSON V3 keystores, remote signers like Web3Signer), see [Key storage methods](/operate/operators/keystore/storage-methods). +::: + +:::danger Make sure you have the mnemonic saved offline +The mnemonic from step 1 is your **single backup**. Both the publisher ECDSA key and the attester BLS key are derived deterministically from it, so re-running the same `aztec validator-keys new` command with the same mnemonic reproduces `key1.json` byte-for-byte. If the server's disk fails and you have the mnemonic, you can rebuild the node from scratch. + +Without the mnemonic and without a copy of `key1.json`, a disk loss means a forced exit and re-registration, including the withdrawal delay of about 10 days. + +Optional, for faster recovery: keep an encrypted copy of `key1.json` and `key1_staker_output.json` somewhere off this server. Restoring from a copy is faster than re-deriving from the mnemonic. The command below uses GPG symmetric encryption (most Linux distros ship `gpg`; macOS users can `brew install gnupg`). It prompts for a passphrase, asks you to confirm it, and writes the encrypted archive: + +```bash +tar czf - ~/.aztec/keystore/key1.json ~/.aztec/keystore/key1_staker_output.json \ + | gpg --symmetric --cipher-algo AES256 --output aztec-keystore-backup.tar.gz.gpg +``` + +Move `aztec-keystore-backup.tar.gz.gpg` off this server (hardware drive, encrypted cloud bucket, air-gapped machine). To restore later, on a fresh server, run: + +```bash +gpg --decrypt aztec-keystore-backup.tar.gz.gpg | tar xzf - -C / +``` + +GPG prompts for the same passphrase. The keystore lands back in `~/.aztec/keystore/key1.json`. +::: + +## 4. Extract your addresses + +<IfCount eq={1}> + +```bash +# Attester address (your sequencer's identifier) +jq -r '.[0].attester' ~/.aztec/keystore/key1_staker_output.json + +# Publisher address (the account that pays L1 gas) +jq -r '.validators[0].publisher[0]' ~/.aztec/keystore/key1.json | xargs cast wallet address --private-key +``` + +Paste both into the configuration panel above. + +</IfCount> + +<IfCount gt={1}> + +You generated <Count /> validators. List all of them at once: + +```bash +# All attester addresses (one per line) +jq -r '.[].attester' ~/.aztec/keystore/key1_staker_output.json + +# All publisher addresses (one per line) +jq -r '.validators[].publisher[0]' ~/.aztec/keystore/key1.json | \ + xargs -n1 cast wallet address --private-key +``` + +Paste **the first attester** and **the first publisher address** into the configuration panel. The configuration panel holds one address each because the Playbook uses the first validator as the canonical example; downstream multi-validator commands extract all <Count /> from the keystore directly. + +</IfCount> + +## 5. Verify the keystore loaded correctly + +```bash +cat ~/.aztec/keystore/key1.json | jq . +``` + +Expected structure: + +```json +{ + "schemaVersion": 1, + "validators": [{ + "attester": { "eth": "0x...", "bls": "0x..." }, + "publisher": ["0x..."], + "feeRecipient": "0x000...000", + "coinbase": "0x..." + }] +} +``` + +## Before you continue + +- **Mnemonic written down on paper or stored on a hardware device.** Not on this server. Not in a synced password manager. Not in a screenshot. +- `key1.json` and `key1_staker_output.json` exist in `~/.aztec/keystore/`. +- Attester address and Publisher address filled in the configuration panel. + +If you cannot honestly check off the first item, **stop now and do it**. The next steps fund accounts with real ETH and AZTEC; losing the mnemonic after that point costs money. + +--- + +<details id="the-keys-the-addresses-and-who-calls-what"> +<summary>The keys, the addresses, and who calls what</summary> + +A sequencer involves a small cast of distinct identities. They can be the same wallet for a casual setup, or fully separated for a production setup. + +**Cryptographic keys (live in `key1.json`):** + +- **Attester ETH key (ECDSA)** identifies your sequencer on the network. The address derived from this key is what gets registered in the Rollup contract and what other operators see. It can also start an exit of its position (an [attester-initiated exit](/operate/operators/sequencer-management/exiting-a-sequencer)). The stake still goes to the withdrawer, but anyone holding this key can take the sequencer out of the active set, so protect it like the node itself. +- **Attester BLS key (BLS12-381)** signs block attestations. BLS signatures aggregate, so when a 32-member committee attests to a block, all 32 signatures combine into one short signature on L1. That saves gas. +- **Publisher key (ECDSA)** is a separate Ethereum account that pays L1 gas when your node submits proofs and attestations. Keeping it separate means you can rotate the funded account without rotating your sequencer identity. + +**Operational addresses (passed in at registration time, stored onchain):** + +- **Caller** is whoever calls `Rollup.deposit(...)` to register the sequencer. This is the address whose 200K AZTEC gets pulled in as stake (verified against `StakingLib.deposit`: `safeTransferFrom(msg.sender, ...)`). Typically a treasury or operator multisig that holds the AZTEC. +- **Withdrawer** is the address that controls the stake: it can start an exit with `initiateWithdraw`, names the recipient for every exit (including attester-initiated ones), signs governance delegations ([governance docs](/operate/operators/sequencer-management/creating_and_voting_on_proposals)), and receives residual stake on slash-driven ejection ([slashing docs](/operate/operators/sequencer-management/slashing_and_offenses)). Use a cold wallet or multisig you control. It does **not** need to be the same as the caller. +- **Coinbase** is an L1 Ethereum address that accumulates sequencer rewards on the Rollup contract. You claim them via `claimSequencerRewards(coinbase)`. +- **Fee recipient** is an L2 Aztec address that would collect L2 transaction fees. L2 fees are not enabled today, so this is a placeholder set to `0x000...000`. + +**Casual setup** uses one operator wallet for caller + withdrawer + coinbase, plus a hot publisher key on the box, plus the in-keystore attester. **Production setup** keeps caller and withdrawer on a multisig, attester hot on the node, publisher funded separately, and coinbase on a treasury wallet. Four (or five) distinct identities, each rotatable on its own. + +`--gse-address` in the keystore-generation command points at the **Governance Staking Escrow** contract that manages validator registration and stake. It is a different address on testnet vs mainnet; the configuration panel handles that for you. + +For the deeper schema, advanced patterns (multi-validator, hardware-wallet publisher), and recovery procedures, see the [Keystore Management section](/operate/operators/keystore). + +</details> + +<details open> +<summary>Something wrong?</summary> + +- **"Error: could not connect to RPC"** — Your L1 RPC URL may be wrong or rate-limited. Test with `cast block-number --rpc-url YOUR_URL`. +- **`jq` shows an empty validators array** — Re-run the command. Ensure `--publishers` has a valid private key with the `0x` prefix. + +</details> + +**Stuck?** Ask in [Discord](https://discord.com/invite/aztec). diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/index.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/index.mdx new file mode 100644 index 00000000000..b8b1ca8f70f --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/index.mdx @@ -0,0 +1,50 @@ +--- +title: "Solo sequencer: zero to attesting" +description: "An opinionated, end-to-end walkthrough that takes a new operator from a blank server to a registered, self-staked, attesting Aztec sequencer in roughly 45 minutes." +displayed_sidebar: operatorsSidebar +--- + +# Solo sequencer: zero to attesting + +The fastest path from a blank server to an attesting Aztec sequencer staked with your own AZTEC. Pick your network and topology, fill the configuration panel once, copy commands as you go. + +:::info One of two ways to learn the operator stack +This **guided setup** is one entry point into the operator docs. It's the right starting place if you've never run an Aztec sequencer before. For lookup material once your node is live, the topic-organized reference pages stay authoritative: [Setup](/operate/operators/setup/sequencer_management) (deeper details on each subsystem), [Keystore management](/operate/operators/keystore), [Monitoring](/operate/operators/monitoring), [Sequencer management](/operate/operators/sequencer-management), and the [CLI / API references](/operate/operators/reference/cli-reference). When this guide hands you off to a deeper topic, it links to the reference page for that topic. +::: + +## Pick your network and topology + +<TrackPicker hideStake /> + +<ForceStakeMode mode="self" /> + +The guide adapts to your choices: +- **Network** swaps RPC URL hints, contract addresses, and Docker image tag in every code block on every page. +- **Topology** changes the server requirements and the Docker Compose file (HA mode runs the same attester across two coordinated nodes). + +Your choices persist across pages. Flip any toggle at any time. Running on delegated stake instead of your own? Switch to the [Staking provider guide](/operate/operators/provider/). + +:::tip Running a DappNode box? +The community-maintained Aztec package replaces the configure, compose, and start steps of this guide with a one-click install. See [DappNode setup](/operate/operators/solo-sequencer/dappnode); you'll rejoin this guide for keystore, funding, and registration. +::: + +## What you'll do + +| # | Step | Time | +|---|------|------| +| 1 | [Pre-flight](/operate/operators/solo-sequencer/pre-flight) | ~2 min | +| 2 | [Install Aztec toolchain](/operate/operators/solo-sequencer/install-toolchain) | ~2 min | +| 3 | [Generate keystore](/operate/operators/solo-sequencer/generate-keystore) | ~5 min | +| 4 | [Fund your accounts](/operate/operators/solo-sequencer/fund-accounts) | ~5 min | +| 5 | [Configure environment](/operate/operators/solo-sequencer/configure-environment) | ~5 min | +| 6 | [Create Docker Compose](/operate/operators/solo-sequencer/docker-compose) | ~3 min | +| 7 | [Start the node](/operate/operators/solo-sequencer/start-node) | 5 to 15 min | +| 8 | [Register as sequencer](/operate/operators/solo-sequencer/register-sequencer) | ~10 min | +| 9 | [Verify everything works](/operate/operators/solo-sequencer/verify) | ~5 min | +| 10 | [Updates, alerts, rewards](/operate/operators/solo-sequencer/day-2-essentials) | ~5 min | + +## How to read it + +- **Code blocks personalize themselves.** Starting in [generate keystore](/operate/operators/solo-sequencer/generate-keystore), a configuration panel appears at the top of pages that need it. Paste your RPC URLs, keys, and addresses once. Every code block on every page after that auto-fills with your values. Commands fill in with your real values and copy ready to run; keys stay masked in the panel inputs and live in session-only storage. +- **"What just happened?"** sections at the end of each step explain the why. Skip them on the first pass; come back when something breaks. +- **Sections in dashed boxes with a coloured tag** (such as "HA") only show because of your topology choice. Flip the toggles up top to see other paths. diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/install-toolchain.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/install-toolchain.mdx new file mode 100644 index 00000000000..b462427c36b --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/install-toolchain.mdx @@ -0,0 +1,79 @@ +--- +title: "Install the Aztec toolchain" +description: "Install the aztec CLI and Foundry, the two tools you need to generate keys and interact with Aztec L1 contracts." +displayed_sidebar: operatorsSidebar +references: ["labs/aztec-up/bin/*"] +--- + +# Install the Aztec toolchain + +**Time:** ~2 minutes. The Aztec CLI is needed to generate validator keys and interact with L1 contracts. Foundry's `cast` is used throughout the Playbook to query contracts. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="self" /> + +## 1. Install Aztec + +```bash +bash -i <(curl -sL https://install.aztec.network) +``` + +The installer pulls the `latest` tag by default. The current Aztec-recommended release is <RecommendedVersion />. To pin to it, set `VERSION` before running the installer: + +<ConfigCode language="bash">{`VERSION={{RECOMMENDED_VERSION}} bash -i <(curl -sL https://install.aztec.network)`}</ConfigCode> + +## 2. Install Foundry + +You'll use `cast` later to query contracts and claim rewards. + +```bash +curl -L https://foundry.paradigm.xyz | bash && foundryup +``` + +## 3. Make the installation persistent + +Both installers append `PATH` lines to a shell rc file, but new shell sessions only pick them up if that rc file is loaded automatically. The fastest fix that survives reboots and new SSH sessions: + +```bash +# Source the rc file once in this session +source ~/.bashrc # or ~/.zshrc if you use zsh + +# Make sure login shells pick it up too (covers non-interactive SSH) +grep -q 'source ~/.bashrc' ~/.bash_profile 2>/dev/null || \ + echo '[ -f ~/.bashrc ] && source ~/.bashrc' >> ~/.bash_profile +``` + +If you run the node under `systemd` or as a different user, that process won't read your rc files. Set `PATH` explicitly in the unit file or use absolute paths (`/home/<user>/.aztec/bin/aztec`, `/home/<user>/.foundry/bin/cast`). + +## 4. Verify the installation + +Open a new shell or run `source ~/.bashrc`, then: + +```bash +aztec --version +cast --version +``` + +Expected output: `aztec` followed by the version you installed (the recommended pin is <RecommendedVersion />, or whatever `latest` resolves to today), plus `cast Version: ...`. + +<details open> +<summary>What just happened?</summary> + +You installed two tools and made them available in every new shell. + +- **`aztec` CLI** generates BLS and ECDSA keypairs for your sequencer and registers your validator on L1. +- **Foundry (`cast`)** is a CLI for interacting with Ethereum smart contracts. You'll use `cast call` to check your sequencer status and `cast send` to claim rewards. +- **The `~/.bash_profile` line** ensures non-interactive shells (the kind `ssh user@host "aztec --version"` uses) load `~/.bashrc` and find the new tools on `PATH`. + +</details> + +<details open> +<summary>Something wrong?</summary> + +- **`command not found: aztec` after reopening shell** — Your shell may not be `bash`. Run `echo $SHELL`. If it's `zsh`, source `~/.zshrc` instead and add the equivalent line to `~/.zprofile`. +- **Tools work in interactive shell but not via `ssh user@host "aztec --version"`** — The `~/.bash_profile` step above fixes this. Make sure that file exists and contains the source line. +- **`curl` fails** — Check your firewall allows HTTPS. Try `curl -I https://install.aztec.network`. + +</details> + diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/pre-flight.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/pre-flight.mdx new file mode 100644 index 00000000000..4a75e5c3551 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/pre-flight.mdx @@ -0,0 +1,147 @@ +--- +title: "Pre-flight" +description: "Hardware, software, network access, and tokens you need before starting. Adapts to your topology choice." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/p2p/src/config.ts", "labs/yarn-project/aztec/src/cli/aztec_start_options.ts"] +--- + +import NodeHardware from "../_node_hardware.mdx"; + +# Pre-flight + +**Time:** ~2 minutes. Confirm you have everything before starting. Every item is required. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="self" /> + +:::warning Reliability is a hard requirement +An Aztec sequencer must stay online 24/7 with a stable internet connection. The protocol slashes 2,000 AZTEC per offense when you miss attestation duties. + +Before starting, confirm your setup can sustain: + +- **Power:** backed by UPS or hosted on infrastructure with N+1 power. +- **Network:** not behind a NAT or firewall you don't control; not a cellular hotspot; not a residential connection prone to ISP outages. +- **Compute:** machine that doesn't sleep, doesn't reboot for OS updates without your control, and doesn't share resources with workloads that could cause downtime. +- **Operator availability:** someone responds to alerts within a few hours (you, an on-call rotation, or a monitoring service). + +If any of these is uncertain, consider delegating instead. +::: + +## Hardware + +Each server needs: + +<NodeHardware /> + +The CPU must be Skylake or newer: on x86 the node's cryptography library (barretenberg) is compiled with `-march=skylake`, so it uses instructions that older chips lack and fails on them (some 2015 CPUs are pre-Skylake). Co-hosting an L1 node needs more storage; see [Hardware spec](/operate/operators/concepts/hardware#co-hosting-an-l1-node). + +<IfChoice axis="topology" is="single"> + +You need **one server** with a static public IP. + +A DappNode box that meets these specs can run the sequencer through the community-maintained Aztec package instead of the manual Docker setup; see [DappNode setup](./dappnode). + +<IfCount gt={1}> + +You configured **<Count />** attesters. Running more attester identities on the same node does not multiply the spec: the node validates each block proposal once and then signs cheaply per attester, so one node with 1 attester and one node with many attesters need roughly the same CPU and RAM. What multiplies the spec is the number of **nodes** you run, not the attester count. See [Hardware spec](/operate/operators/concepts/hardware) for details. + +</IfCount> + +</IfChoice> + +<IfChoice axis="topology" is="ha" label="HA topology"> + +In HA mode the **same attester identity** runs across multiple nodes for redundancy. The nodes coordinate via a shared PostgreSQL database that prevents double-signing. This is not the same as splitting N attesters across N nodes. + +You need **two servers** running the same attester identity, each meeting the spec above, with: + +- **Network:** static public IPs on both +- **Shared PostgreSQL** reachable by both nodes for HA coordination +- **Private link:** the nodes need a low-latency path to the shared database. Same VPC or a Wireguard mesh works. + +If one node fails, the other continues attesting without missing duties. See [High availability sequencers](/operate/operators/setup/high_availability_sequencers) for the full setup. + +</IfChoice> + +:::info Where you host matters +If a significant share of the network is running on the same few cloud providers (AWS, GCP, Azure, Hetzner, etc.), it creates a centralization risk: a single provider outage, policy change, or region failure can affect many sequencers at once. When choosing where to run your node, consider options beyond the major clouds. +::: + +## Software + +You need this on **every** server in your topology. + +- **OS:** Ubuntu 22.04+, Debian 12+, or macOS. Windows WSL2 also works but is not recommended for production. +- **Docker + Docker Compose:** installed and running. Verify with: + +```bash +docker --version && docker compose version +``` + +- **jq:** JSON processor used in verification commands. `sudo apt install -y jq` on Ubuntu/Debian or `brew install jq` on macOS. Verify with `jq --version`. + +<details open> +<summary>Need to install Docker?</summary> + +**Ubuntu / Debian:** + +```bash +sudo apt-get update +sudo apt-get install -y ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ + -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +echo "deb [arch=$(dpkg --print-architecture) \ + signed-by=/etc/apt/keyrings/docker.asc] \ + https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +sudo apt-get update +sudo apt-get install -y docker-ce docker-ce-cli \ + containerd.io docker-compose-plugin + +sudo usermod -aG docker $USER +newgrp docker +``` + +**macOS:** install [Docker Desktop](https://docs.docker.com/desktop/install/mac-install/). + +**Verify:** `docker run hello-world`. + +</details> + +## Network access + +- **L1 Ethereum endpoints:** an execution RPC, a blob-serving consensus (beacon) RPC, and a debug/trace RPC. Self-hosted (for example Geth plus Lighthouse or Prysm in supernode mode) or a hosted provider that supports all three. The beacon client must serve blob sidecars, which has specific version and flag requirements. See [L1 RPC requirements](/operate/operators/concepts/l1-rpc) before you choose a provider; it is the most common setup blocker. +- **Port 40400** open for both TCP and UDP (P2P). +- **Port 8080** available locally (Aztec HTTP API). Do not expose it publicly. + +<IfChoice axis="topology" is="ha" label="HA topology"> + +Additional ports for the publisher node: + +- **Port 8880** (admin API) reachable only over the private link from the sequencer node. **Never** expose this to the internet. + +</IfChoice> + +## Tokens + +- **AZTEC tokens:** 200,000 AZTEC minimum stake **per sequencer**. You'll self-stake these when you [register as sequencer](./register-sequencer). +- **ETH for gas:** the publisher consumes a small amount of L1 gas each time it submits a checkpoint. Around **0.01 ETH per sequencer** is a comfortable opening top-up at current L1 prices. Size it against real network activity with the [funding calculator](./fund-accounts#fund-the-publisher), which scans the last 30 days using your own L1 RPC. + +<details open> +<summary>What just happened?</summary> + +You confirmed your servers meet Aztec's minimum requirements. The node runs as a Docker container that connects to Ethereum L1 (for anchoring proofs and reading rollup state) and the Aztec P2P network (for receiving transactions and participating in consensus). + +The 200K AZTEC self-stake is required by the Rollup contract to prevent Sybil attacks. + +</details> + +--- + +**Stuck?** Ask in [Discord](https://discord.com/invite/aztec). diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/register-sequencer.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/register-sequencer.mdx new file mode 100644 index 00000000000..a7e99a8c260 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/register-sequencer.mdx @@ -0,0 +1,126 @@ +--- +title: "Register as sequencer" +description: "Self-stake your AZTEC through the staking dashboard and enter the validator queue." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/cli/src/cmds/validator_keys/*"] +--- + +# Register as sequencer + +**Time:** ~10 minutes. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="self" /> + +<OperatorConfig /> + +## Register through the staking dashboard + +You'll register through the [Aztec Staking Dashboard](https://stake.aztec.network) using your **public keystore** (`~/.aztec/keystore/key1_staker_output.json`). This file contains only public information (attester address, BLS public keys, proof of possession). Safe to share. + +### 1. Open the dashboard and connect your wallet + +Navigate to [https://stake.aztec.network](https://stake.aztec.network) and connect the wallet that holds your AZTEC tokens. + +### 2. Click **Stake** + +![Staking dashboard home](/img/staking_dashboard_1.png) + +### 3. Select **Run your own Sequencer** + +![Select sequencer option](/img/staking_dashboard_2.png) + +### 4. Click through **Start Registration** after reviewing the requirements + +### 5. Select the AZTEC tokens you want to stake + +### 6. Upload your keystore JSON file + +<IfCount eq={1}> + +Use `~/.aztec/keystore/key1_staker_output.json`. + +</IfCount> + +<IfCount gt={1}> + +Because you used <code>--count <Count /></code> in step 3 of [generate keystore](./generate-keystore), all <Count /> validators are already in a single file: + +```bash +~/.aztec/keystore/key1_staker_output.json +``` + +The dashboard expects a JSON array where each object is one sequencer; that's exactly the shape your file already has. **Just upload that one file.** + +If you generated validators across multiple `aztec validator-keys new` invocations (rare), combine the individual files into one array with `jq`: + +```bash +jq -s 'add' \ + ~/.aztec/keystore/key1_staker_output.json \ + ~/.aztec/keystore/key2_staker_output.json \ + > ~/.aztec/keystore/combined_staker_output.json +``` + +Upload the combined file. + +</IfCount> + +![Upload keystore file](/img/staking_dashboard_3.png) + +### 7. Confirm your attester / sequencer addresses + +![Confirm addresses](/img/staking_dashboard_4.png) + +### 8. Approve token spend in your wallet + +![Approve tokens](/img/staking_dashboard_5.png) + +### 9. Add staking for all sequencers to the queue + +![Add to queue](/img/staking_dashboard_6.png) + +### 10. Execute transactions in the dashboard + +![Execute transactions](/img/staking_dashboard_7.png) + +### 11. Confirm each transaction in your wallet, then click **Complete** + +Your sequencer is now in the queue. Verify position at [dashtec.xyz/queue](https://dashtec.xyz/queue), then continue to the next step, [Verify everything works](./verify). + +## Adding another sequencer later + +To register an additional self-staked sequencer after your first, generate another attester **with its own staker output**, then upload that staker output to the dashboard: + +<ConfigCode language="bash">{`aztec validator-keys new --staker-output \\ + --mnemonic "<your mnemonic>" \\ + --gse-address {{GSE_ADDR}} \\ + --l1-rpc-urls {{ETH_RPC}} \\ + --l1-chain-id {{L1_CHAIN_ID}}`}</ConfigCode> + +`aztec validator-keys new` does not overwrite your existing keystore: if `key1.json` exists it writes `key2.json` and `key2_staker_output.json`, leaving your first attester untouched. The node loads every keystore file in `KEY_STORE_DIRECTORY`, so once `key2.json` is in that directory it runs alongside the first. Copy only the `keyN.json` files into that directory (not the `*_staker_output.json` files, which make the node fail to start), then repeat the dashboard steps above, uploading `key2_staker_output.json`. + +:::note `add` does not produce a staker output +`aztec validator-keys add <keystore>` appends an attester to an existing keystore file, but it does **not** generate a staker output, so the dashboard has nothing to upload for the new attester. For dashboard registration, use `new --staker-output` as shown above. +::: + +:::tip One mnemonic or a fresh one +You can derive every attester from a single mnemonic, or use a separate mnemonic per batch. With one mnemonic, pass `--address-index <N>` each time, where `N` is the number of attesters already derived from it, so the new keys do not duplicate the existing ones (`new` derives from index 0 by default). One mnemonic means a single backup to secure; a fresh mnemonic per machine keeps each machine's keys independent if one backup is ever exposed. +::: + +## L1 contract addresses (<NetworkName />) + +<ConfigCode language="text">{`Registry {{REGISTRY_ADDR}} +Rollup {{ROLLUP_ADDR}} +GSE {{GSE_ADDR}} +Slasher {{SLASHER_ADDR}}`}</ConfigCode> + +Full list: [docs.aztec.network/networks](https://docs.aztec.network/networks). + +<details open> +<summary>What just happened?</summary> + +You registered your attester address and BLS public key in the L1 Rollup contract. The contract locked 200K AZTEC tokens as your stake. Once processed (1 to 2 epochs), your sequencer starts being selected for committees. When selected, it attests to proposed blocks and occasionally proposes a block itself. Sequencer rewards (issuance + attestation) accumulate against your coinbase address on the Rollup contract; you withdraw them with `claimSequencerRewards(coinbase)`. L2 transaction fees are not enabled yet, so the keystore's `feeRecipient` field stays zero. + +</details> + diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/start-node.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/start-node.mdx new file mode 100644 index 00000000000..3b20d527154 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/start-node.mdx @@ -0,0 +1,122 @@ +--- +title: "Start the node" +description: "Launch the Aztec sequencer container, watch it sync to the proven tip, and confirm the HTTP API is responding." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/aztec/src/cli/cmds/start_node.ts", "labs/yarn-project/stdlib/src/interfaces/aztec-node.ts", "labs/yarn-project/node-lib/src/config/index.ts"] +--- + +# Start the node + +**Time:** 5 to 15 minutes. Launch the container and wait for it to sync with the network. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="self" /> + +## 1. Start the node + +<IfChoice axis="topology" is="single"> + +```bash +docker compose up -d +``` + +</IfChoice> + +<IfChoice axis="topology" is="ha" label="HA topology"> + +Make sure the HA Postgres database is reachable from both nodes and the schema migration ran (see [Docker Compose](./docker-compose) for the `aztec migrate-ha-db up` command). Then start each node, in any order: + +```bash +ssh node-1 'cd ~/aztec-sequencer && docker compose up -d' +ssh node-2 'cd ~/aztec-sequencer && docker compose up -d' +``` + +Both nodes hold the same attester key. The shared database guarantees only one of them signs each duty, so order doesn't matter. + +</IfChoice> + +## 2. Watch the logs + +```bash +docker compose logs -f aztec-sequencer +``` + +Press `Ctrl+C` to exit the log view. The node continues running in the background. + +:::info Initial sync +On first start the node automatically downloads a state snapshot from a group of snapshot providers, then catches up the remaining blocks from L1. This is the default sync mode (`SYNC_MODE=snapshot`); snapshot sources are configured for you when you pass `--network`. Initial sync takes 5 to 30 minutes depending on your L1 endpoint speed and current chain height. You'll see blocks being processed in the logs. Wait until the node catches up to the current proven tip. See [Using and uploading snapshots](../setup/syncing_best_practices) for sync modes and custom snapshot sources. +::: + +## 3. Check sync progress + +In a new terminal, ask the node which L2 block tips it has seen: + +<ConfigCode language="bash">{`curl -s -X POST -H 'Content-Type: application/json' \\ + -d '{"jsonrpc":"2.0","method":"aztec_getChainTips","params":[],"id":1}' \\ + http://localhost:{{AZTEC_PORT}} | jq '.result | {proposed: .proposed.number, proven: .proven.block.number, finalized: .finalized.block.number}'`}</ConfigCode> + +Expected output looks like: + +```json +{ + "proposed": 75432, + "proven": 75408, + "finalized": 75392 +} +``` + +- **`proposed`** is the latest block the network is producing. As soon as your node is alive and on the right network, this number should increase every few seconds. +- **`proven`** is the latest block with a verified ZK proof submitted to L1. Lags behind `proposed` by an epoch or so. This is the "fully synced" signal. +- **`finalized`** is the latest block whose L1 finalization is also confirmed. Lags behind `proven` by an Ethereum epoch. + +`proven` and `finalized` may show `null` for the first few minutes after startup, before the node has seen a proven epoch. That's normal. Re-run the command after a minute or two; the numbers should appear and start climbing. + +## 4. Check node readiness and identity + +`aztec_isReady` returns `true` once the node is ready to accept transactions: + +<ConfigCode language="bash">{`curl -s -X POST -H 'Content-Type: application/json' \\ + -d '{"jsonrpc":"2.0","method":"aztec_isReady","params":[],"id":1}' \\ + http://localhost:{{AZTEC_PORT}} | jq -r '.result'`}</ConfigCode> + +`aztec_getNodeInfo` confirms which network and version you're on. Use it to catch "I started the node against the wrong `--network`" mistakes early: + +<ConfigCode language="bash">{`curl -s -X POST -H 'Content-Type: application/json' \\ + -d '{"jsonrpc":"2.0","method":"aztec_getNodeInfo","params":[],"id":1}' \\ + http://localhost:{{AZTEC_PORT}} | jq '.result | {nodeVersion, l1ChainId, rollupVersion, rollupAddress: .l1ContractAddresses.rollupAddress}'`}</ConfigCode> + +Expected output looks like: + +```json +{ + "nodeVersion": "6.0.0", + "l1ChainId": 1, + "rollupVersion": "...", + "rollupAddress": "0xE37A2663Cc9c119BA41572C40fAb0bbb23a5402f" +} +``` + +`l1ChainId: 1` is Ethereum mainnet, `11155111` is Sepolia. The `rollupAddress` should match the address in the [L1 contract addresses](./register-sequencer) table. + +## Before you continue + +- `proposed` is increasing every few seconds (node is alive). +- `proven` is non-null and converging on `proposed` (node is fully synced). +- `aztec_isReady` returns `true`. +- `aztec_getNodeInfo` shows the rollup address and L1 chain id you expect. +- Logs show no persistent errors. + +<details open> +<summary>Something wrong?</summary> + +- **"Error connecting to Ethereum"** — Check `ETHEREUM_HOSTS` in your `.env`. Test with `cast block-number --rpc-url YOUR_URL`. +- **Node exits immediately** — Run `docker compose logs aztec-sequencer` (without `-f`) to see the exit reason. Common causes: invalid keystore path, port conflict, or Docker out of disk space. +- **Sync stuck** — Your L1 endpoints may be rate-limited. Consider self-hosting Geth + Prysm or using a higher-tier provider plan. +- **P2P connection issues** — Verify port forwarding with `nc -zv YOUR_IP 40400`. Check both TCP and UDP are forwarded. + +</details> + +--- + +**Stuck?** Ask in [Discord](https://discord.com/invite/aztec). diff --git a/docs/lines/v6/docs-operate/operators/solo-sequencer/verify.mdx b/docs/lines/v6/docs-operate/operators/solo-sequencer/verify.mdx new file mode 100644 index 00000000000..ffb728c0f79 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/solo-sequencer/verify.mdx @@ -0,0 +1,128 @@ +--- +title: "Verify everything works" +description: "Confirm your sequencer is healthy and visible on the network: one container check, one explorer check, one port check." +displayed_sidebar: operatorsSidebar +references: ["labs/yarn-project/stdlib/src/interfaces/aztec-node.ts", "docs/static/scripts/check-attester.sh"] +--- + +# Verify everything works + +**Time:** ~2 minutes. Three checks confirm your sequencer is synced, registered, and reachable. + +<TrackPicker compact hideStake /> + +<ForceStakeMode mode="self" /> + +<OperatorConfig /> + +## 1. Container healthy + +```bash +docker compose ps +``` + +Expected: + +``` +NAME IMAGE STATUS +aztec-sequencer azteclabs/aztec:... Up 12 minutes (healthy) +``` + +The `(healthy)` flag comes from the health check in your compose file. It probes the node's own API every 30 seconds and requires a proven L2 tip, so this one line covers sync progress, API readiness, and container stability. + +- `(health: starting)`: the node is still syncing. Initial sync takes 5 to 30 minutes. +- `(unhealthy)`: the API stopped answering or the node lost its proven tip. Check `docker compose logs -f aztec-sequencer`. +- `Up` with no health suffix: your compose file predates the health check. Add it from the [Docker Compose](./docker-compose) step. + +## 2. Find your sequencer on an explorer + +Paste your attester address into one of the community explorers: + +- **[Dashtec](https://dashtec.xyz/)**: validator status, queue position, attestation activity. +- **[Aztec Vision](https://aztec.vision/)**: sequencer explorer with per-attester activity. + +What you should see: + +- **Just registered**: your attester sits in the entry queue. Status stays `NONE` for the first 1 to 2 epochs until the queue is flushed. This is normal, not a failed deposit. +- **Active**: status `VALIDATING`, with attestations appearing whenever your sequencer is selected for a committee. + +<IfCount gt={1}> + +Check all <Count /> attesters, not just the first. One stuck in the queue is easy to miss. + +</IfCount> + +Once attestations show up, your node has proven itself end to end: it is synced, signing, and publishing over P2P. + +:::note Community-maintained +These explorers are community projects and can lag the chain. If your network or attester is not listed, or you want an answer straight from the contracts, use the CLI check at the bottom of this page. More explorers are listed in [awesome-aztec](https://github.com/AztecProtocol/awesome-aztec#explorers---node-focussed). +::: + +## 3. P2P port reachable + +Until your first attestations appear on an explorer, a closed P2P port is invisible. Confirm it once, from a machine outside your network: + +<ConfigCode language="bash">{`nc -zv {{P2P_IP}} 40400`}</ConfigCode> + +Expected output: + +``` +Connection to <P2P_IP> 40400 port [tcp/*] succeeded! +``` + +After your sequencer starts attesting, this check is redundant: attestation activity already proves connectivity. + +<details> +<summary id="advanced-check-contract-state-directly">Advanced: check contract state directly (CLI)</summary> + +Explorers read the same contracts through an indexer. These checks query the contracts directly. + +**Quick status check.** The expected status is `1` (VALIDATING). Codes: `0` NONE, `1` VALIDATING, `2` ZOMBIE, `3` EXITING. A fresh deposit sits in the entry queue until an epoch boundary passes and someone calls `flushEntryQueue(uint256)` on the Rollup (anyone can, including you), so `0` during the first 1 to 2 epochs does not mean the deposit failed. + +<IfCount eq={1}> + +<ConfigCode language="bash">{`cast call {{ROLLUP_ADDR}} \\ + "getStatus(address)(uint8)" {{ATTESTER_ADDR}} \\ + --rpc-url {{ETH_RPC}}`}</ConfigCode> + +</IfCount> + +<IfCount gt={1}> + +Loop the check across every attester in your keystore: + +<ConfigCode language="bash">{`KEYSTORE=~/.aztec/keystore/key1_staker_output.json + +for i in $(seq 0 {{COUNT_MINUS_1}}); do + ATTESTER=$(jq -r ".[$i].attester" "$KEYSTORE") + STATUS=$(cast call {{ROLLUP_ADDR}} \\ + "getStatus(address)(uint8)" "$ATTESTER" \\ + --rpc-url {{ETH_RPC}}) + echo "Validator $((i+1)) of {{COUNT}} ($ATTESTER): status=$STATUS" +done`}</ConfigCode> + +All <Count /> should report `status=1`. Anything stuck on `0` (NONE) after a couple of epochs means the deposit didn't land or the entry queue hasn't been flushed. + +</IfCount> + +**Full attester report.** When a status is anything other than `1` and you need to see why, download [`check-attester.sh`](pathname:///scripts/check-attester.sh). It checks one or more attesters (status, effective balance, exit state, withdrawer) and prints a labeled, color-coded report with a health verdict per attester: + +<ConfigCode language="bash">{`curl -o check-attester.sh https://docs.aztec.network/scripts/check-attester.sh +chmod +x check-attester.sh + +# Check several attesters at once: comma-separated list +ATTESTER={{ATTESTER_ADDR}} \\ +ROLLUP={{ROLLUP_ADDR}} \\ +GSE={{GSE_ADDR}} \\ +ETH_RPC={{ETH_RPC}} \\ + ./check-attester.sh`}</ConfigCode> + +</details> + +## You're live + +Your sequencer is registered and attesting. The node<IfCount gt={1}>s</IfCount> will be randomly selected for committees starting next epoch and earn AZTEC rewards for attesting to and proposing blocks. + +One step remains: go through [Updates, alerts, rewards](./day-2-essentials) to learn how to keep your node on the current version, set up monitoring alerts, and claim your rewards. + +If your node is running but proposals or attestations are not landing, see [Sequencer troubleshooting](../concepts/sequencer-troubleshooting). diff --git a/docs/lines/v6/docs-operate/operators/tooling/index.md b/docs/lines/v6/docs-operate/operators/tooling/index.md new file mode 100644 index 00000000000..95a5ebf9786 --- /dev/null +++ b/docs/lines/v6/docs-operate/operators/tooling/index.md @@ -0,0 +1,88 @@ +--- +id: index +slug: /operators/tooling +title: Operator tooling +description: A curated index of Foundation and community-built tools that operators rely on for monitoring, key management, slashing protection, and reward tracking. +displayed_sidebar: operatorsSidebar +references: ["docs/static/scripts/aztec-monitoring.sh"] +--- + +Operating an Aztec node usually involves more than the node software itself. Most operators rely on a combination of Foundation-built tools (staking dashboard, fee model) and community-built tools (dashboards, scripts, indexers) that fill specific gaps. This page is a curated index. + +:::info Community tools are third-party +Foundation tools are built and maintained by the Aztec Foundation. Community tools are independent projects by individual operators or teams. Use them at your own discretion: read the source where it's available, and don't assume Foundation endorsement of operational correctness. The list below reflects what operators in the network's community actively use today. +::: + +## Foundation tools + +### Staking dashboard + +[stake.aztec.network](https://stake.aztec.network) + +The canonical interface for staking, delegation, registering sequencers, claiming rewards, and signaling governance. Supports both delegator and provider flows, including multi-rollup reward claiming and a provider commission-claim tab. + +The backend API that serves this dashboard is not a supported public surface: its endpoints, response shapes, and rate limits can change without notice. If you need stable programmatic access to staking data, run your own indexer instead of calling the dashboard backend. The dashboard's indexer, [`atp-indexer`](https://github.com/AztecProtocol/staking-dashboard/tree/main/atp-indexer), is open source (a Ponder indexer in the staking-dashboard repository) and serves as the reference implementation you can self-host. + +### Monitoring installer + +[aztec-monitoring.sh](/scripts/aztec-monitoring.sh) + +A single self-contained script that sets up a full self-hosted monitoring stack for your sequencers: an OpenTelemetry collector next to each node, plus Grafana, Prometheus, and Alertmanager with Telegram alerts on a monitoring machine. The Grafana dashboard, alert rules, and Alertmanager template are embedded in the script. Targets `v5.0.0` and works with nodes run via Docker Compose. This is the recommended starting point for node monitoring; see [Monitoring and metrics](/operate/operators/concepts/monitoring#set-up-monitoring-with-the-installer) for the walkthrough. It is optional and replaceable with your own monitoring. + +### Fee model + +[github.com/AztecProtocol/aztec-fee-model](https://github.com/AztecProtocol/aztec-fee-model) + +A self-hostable spreadsheet model for sequencer profitability under different gas, AZTEC price, and commission inputs. Useful for providers calibrating their commission rate. + +### Staking payout script + +[github.com/AztecProtocol/aztec-staking-payout](https://github.com/AztecProtocol/aztec-staking-payout) + +Opt-in payout script for providers who set their coinbase to an address they control instead of the per-delegation Split contracts. Calculates each delegator's rewards at the provider's published commission and produces a Multicall3 transfer batch plus auditable JSON artifacts. See the [forum announcement](https://forum.aztec.network/t/operator-commission-adjustments/8588). + +## Community tools + +### Monitoring and dashboards + +**[dashtec.xyz](https://dashtec.xyz)** +The de-facto operator dashboard for the network. Per-epoch performance, provider pages, the activation queue, watchlists. + +**[aztec.vision](https://aztec.vision)** +Indexer-style provider information. Surfaces misconfigured coinbase addresses (a frequent operator footgun), per-provider stats, and history. + +**[haveibeenslashed.xyz](https://haveibeenslashed.xyz)** +A focused single-purpose tool for checking whether a given attester address has been hit by slashing or jailing events. + +**[aztec-providers.01.ro](https://aztec-providers.01.ro)** +A provider-side indexer, useful as a cross-check for provider-side reward routing. + +**[citizenweb3/chain-data-indexer (aztec branch)](https://github.com/citizenweb3/chain-data-indexer/tree/aztec)** +A self-hostable Aztec L2 indexer (Chicmoz-based, MIT): a listener that ingests proposed and proven blocks, transactions, and chain info, plus an explorer REST API. Useful if you want your own chain/explorer data feed rather than relying on a hosted explorer. For staking-specific data, the dashboard's [`atp-indexer`](https://github.com/AztecProtocol/staking-dashboard/tree/main/atp-indexer) (above) is the closer fit. + +### Scripts and installers + +**[pittpv/aztec-monitoring-script](https://github.com/pittpv/aztec-monitoring-script)** +A community bash-menu installer and monitoring script. One-stop install, upgrade, downgrade, status check, and Telegram alerting. An alternative to the Foundation [monitoring installer](#monitoring-installer) above. + +**[pittpv/aztec-rpc-script](https://github.com/pittpv/aztec-rpc-script)** +Companion script for setting up your own Ethereum execution + consensus client (with the supernode flags Aztec requires). Avoids depending on a hosted RPC provider. + +### Provider and key management + +**[aztec-scan/aztec-butler](https://github.com/aztec-scan/aztec-butler)** +A CLI for operators running multiple nodes, from the team behind the AztecScan explorer. Generates and deploys validator keys, registers them, tracks each attester through its lifecycle states, sets coinbase addresses, and exports telemetry. Runs on Node.js as a systemd daemon; supports native and Olla registries. A newer, more advanced tool aimed at provider-scale operations rather than single-node setups. + +### Coordination + +**[aztecgov.nethermind.io](https://aztecgov.nethermind.io)** +Nethermind's governance signaling dashboard. Shows the current AZIP payload, who has signaled, and what's pending. Operators use this to confirm a vote made it onchain. + +**[slashveto.me](https://slashveto.me)** +The community veto council's coordination surface. Monitors slashing payloads and signals which ones the council intends to veto. Operators consult this during incidents where their setup might trip a slashing condition. + +## Suggest a tool + +If you maintain or use an operator tool that should be listed here, open an issue or PR on the docs repo. + +Foundation does not formally endorse community tools; this page reflects what's in active use. diff --git a/docs/lines/v6/docs-operate/reference/changelog/v4.md b/docs/lines/v6/docs-operate/reference/changelog/v4.md new file mode 100644 index 00000000000..a7f3b5a9179 --- /dev/null +++ b/docs/lines/v6/docs-operate/reference/changelog/v4.md @@ -0,0 +1,333 @@ +--- +title: v4.x (Upgrade from Ignition) +description: Breaking changes and migration guide for upgrading from Ignition (v2.x) to Alpha (v4.x). +--- + +## Overview + +**Migration difficulty**: High + +## Breaking changes + +### Node.js upgraded to v24 + +Node.js minimum version changed from v22 to v24.12.0. + +### Bot fee padding configuration renamed + +The bot configuration for fee padding has been renamed from "base fee" to "min fee". + +**v3.x:** + +```bash +--bot.baseFeePadding <value> ($BOT_BASE_FEE_PADDING) +``` + +**v4.0.0:** + +```bash +--bot.minFeePadding <value> ($BOT_MIN_FEE_PADDING) +``` + +**Migration**: Update your configuration to use the new flag name and environment variable. + +### L2Tips API restructured with checkpoint information + +The `getL2Tips()` RPC endpoint now returns a restructured response with additional checkpoint tracking. + +**v3.x response:** + +```json +{ + "latest": { "number": 100, "hash": "0x..." }, + "proven": { "number": 98, "hash": "0x..." }, + "finalized": { "number": 95, "hash": "0x..." } +} +``` + +**v4.0.0 response:** + +```json +{ + "proposed": { "number": 100, "hash": "0x..." }, + "checkpointed": { + "block": { "number": 99, "hash": "0x..." }, + "checkpoint": { "number": 10, "hash": "0x..." } + }, + "proven": { + "block": { "number": 98, "hash": "0x..." }, + "checkpoint": { "number": 9, "hash": "0x..." } + }, + "finalized": { + "block": { "number": 95, "hash": "0x..." }, + "checkpoint": { "number": 8, "hash": "0x..." } + } +} +``` + +**Migration**: + +- Replace `tips.latest` with `tips.proposed` +- For `checkpointed`, `proven`, and `finalized` tips, access block info via `.block` (e.g., `tips.proven.block.number`) + +### Block gas limits reworked + +The byte-based block size limit has been removed and replaced with field-based blob limits and automatic gas budget computation from L1 rollup limits. + +**Removed:** + +```bash +--maxBlockSizeInBytes <value> ($SEQ_MAX_BLOCK_SIZE_IN_BYTES) +``` + +**Changed to optional (now auto-computed from L1 if not set):** + +```bash +--maxL2BlockGas <value> ($SEQ_MAX_L2_BLOCK_GAS) +--maxDABlockGas <value> ($SEQ_MAX_DA_BLOCK_GAS) +``` + +**New (proposer):** + +```bash +--perBlockAllocationMultiplier <value> ($SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER) +--maxTxsPerCheckpoint <value> ($SEQ_MAX_TX_PER_CHECKPOINT) +``` + +**New (validator):** + +```bash +--validateMaxL2BlockGas <value> ($VALIDATOR_MAX_L2_BLOCK_GAS) +--validateMaxDABlockGas <value> ($VALIDATOR_MAX_DA_BLOCK_GAS) +--validateMaxTxsPerBlock <value> ($VALIDATOR_MAX_TX_PER_BLOCK) +--validateMaxTxsPerCheckpoint <value> ($VALIDATOR_MAX_TX_PER_CHECKPOINT) +``` + +**Migration**: Remove `SEQ_MAX_BLOCK_SIZE_IN_BYTES` from your configuration. Per-block L2 and DA gas budgets are now derived automatically as `(checkpointLimit / maxBlocks) * multiplier`, where the multiplier defaults to 2. You can still override `SEQ_MAX_L2_BLOCK_GAS` and `SEQ_MAX_DA_BLOCK_GAS` explicitly, but they will be capped at the checkpoint-level limits. Validators can now set independent per-block and per-checkpoint limits via the `VALIDATOR_` env vars; when not set, only checkpoint-level protocol limits are enforced. + +### Setup phase allow list requires function selectors + +The transaction setup phase allow list now enforces function selectors, restricting which specific functions can run during setup on whitelisted contracts. Previously, any public function on a whitelisted contract or class was permitted. + +The semantics of the environment variable `TX_PUBLIC_SETUP_ALLOWLIST` have changed: + +**v3.x:** + +```bash +--txPublicSetupAllowList <value> ($TX_PUBLIC_SETUP_ALLOWLIST) +``` + +The variable fully **replaced** the hardcoded defaults. Format allowed entries without selectors: `I:address`, `C:classId`. + +**v4.0.0:** + +```bash +--txPublicSetupAllowListExtend <value> ($TX_PUBLIC_SETUP_ALLOWLIST) +``` + +The variable now **extends** the hardcoded defaults (which are always present). Selectors are now mandatory. An optional flags segment can be appended for additional validation: + +``` +I:address:selector[:flags] +C:classId:selector[:flags] +``` + +Where `flags` is a `+`-separated list of: +- `os` — `onlySelf`: only allow calls where msg_sender == contract address +- `rn` — `rejectNullMsgSender`: reject calls with a null msg_sender +- `cl=N` — `calldataLength`: enforce exact calldata length of N fields + +Example: `C:0xabc:0x1234:os+cl=4` + +**Migration**: If you were using `TX_PUBLIC_SETUP_ALLOWLIST`, ensure all entries include function selectors. Note the variable now adds to defaults rather than replacing them. If you were not setting this variable, no action is needed — the hardcoded defaults now include the correct selectors automatically. + +### Token removed from default setup allowlist + +Token class-based entries (`_increase_public_balance` and `transfer_in_public`) have been removed from the default public setup allowlist. FPC-based fee payments using custom tokens no longer work out of the box. + +This change was made because Token class IDs change with aztec-nr releases, making the allowlist impossible to keep up to date with new library releases. In addition, `transfer_in_public` requires complex additional logic to be built into the node to prevent mass transaction invalidation attacks. **FPC-based fee payment with custom tokens won't work on mainnet alpha**. + +**Migration**: Node operators who need FPC support must manually add Token entries via `TX_PUBLIC_SETUP_ALLOWLIST`. Example: + +```bash +TX_PUBLIC_SETUP_ALLOWLIST="C:<tokenClassId>:<increaseBalanceSelector>:os+cl=3,C:<tokenClassId>:<transferInPublicSelector>:cl=5" +``` + +Replace `<tokenClassId>` with the deployed Token contract class ID and `<increaseBalanceSelector>`/`<transferInPublicSelector>` with the respective function selectors. Keep in mind that this will only work on local network setups, since even if you as an operator add these entries, other nodes will not have them and will not pick up these transactions. + +### Sequencer environment variable renames + +Several sequencer environment variables have been renamed: + +| Old variable | New variable | +|---|---| +| `SEQ_TX_POLLING_INTERVAL_MS` | `SEQ_POLLING_INTERVAL_MS` | +| `SEQ_MAX_L1_TX_INCLUSION_TIME_INTO_SLOT` | `SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT` | +| `SEQ_MAX_TX_PER_BLOCK` | `SEQ_MAX_TX_PER_CHECKPOINT` | +| `SEQ_MAX_BLOCK_SIZE_IN_BYTES` | Removed (see [Block gas limits reworked](#block-gas-limits-reworked)) | + +**Migration**: Search your configuration for the old variable names and replace them. The node will not recognize the old names. + +### Double signing slashing + +New slashable offenses have been introduced for duplicate proposals and duplicate attestations. Penalty amounts are currently set to 0, but the detection infrastructure is active. + +If you run redundant sequencer nodes, you **must** enable high-availability signing with PostgreSQL to prevent accidental double signing: + +```bash +VALIDATOR_HA_SIGNING_ENABLED=true +VALIDATOR_HA_DATABASE_URL=postgresql://<user>:<password>@<host>:<port>/<database> +VALIDATOR_HA_NODE_ID=<unique-id-per-node> +``` + +Run the database migration before starting your nodes: + +```bash +aztec migrate-ha-db up --database-url <url> +``` + +**Migration**: If you run a single node, no action is required. If you run redundant nodes for high availability, configure HA signing immediately. See the [High Availability Sequencers](../../operators/setup/high-availability.md) guide for details. + +### Blob-only data publication + +Transaction data is now published entirely via EIP-4844 blobs. The calldata fallback has been removed. + +Your consensus client (e.g., Lighthouse, Prysm) must run as a **supernode** or **semi-supernode** to make blobs available for retrieval. Standard pruning configurations will not retain blobs long enough. + +You should also configure blob file stores for redundancy: + +```bash +BLOB_FILE_STORE_URLS=<comma-separated-read-urls> +BLOB_FILE_STORE_UPLOAD_URL=<upload-url> +BLOB_ARCHIVE_API_URL=<archive-api-url> +``` + +**Migration**: Ensure your consensus client is configured as a supernode. If you previously relied on calldata for data availability, switch to blob-based retrieval. See the [Blob Storage](../../operators/setup/blob_storage) guide for configuration details. + +### Withdrawal delay increase + +The governance execution delay has increased from 7 days to 30 days. This extends the time required for staker withdrawals from approximately 15 days to approximately 38 days. + +**Migration**: No configuration changes needed. Be aware that withdrawal processing will take longer after the upgrade. + +### Prover architecture change + +The prover now runs as a node subsystem rather than a separate standalone process. Start it alongside your node using the `--prover-node` flag: + +```bash +aztec start --node --prover-node +``` + +**Migration**: If you were running the prover as a separate process, update your deployment to run it as part of the node with `--prover-node`. + +## Removed features + +## New features + +### Initial ETH per fee asset configuration + +A new environment variable `AZTEC_INITIAL_ETH_PER_FEE_ASSET` has been added to configure the initial exchange rate between ETH and the fee asset (AZTEC) at contract deployment. This value uses 1e12 precision. + +**Default**: `10000000` (0.00001 ETH per AZTEC) + +**Configuration:** + +```bash +--initialEthPerFeeAsset <value> ($AZTEC_INITIAL_ETH_PER_FEE_ASSET) +``` + +This replaces the previous hardcoded default and allows network operators to set the starting price point for the fee asset. + +### `reloadKeystore` admin RPC endpoint + +Node operators can now update validator attester keys, coinbase, and fee recipient without restarting the node by calling the new `reloadKeystore` admin RPC endpoint. + +What is updated on reload: + +- Validator attester keys (add, remove, or replace) +- Coinbase and fee recipient per validator +- Publisher-to-validator mapping + +What is NOT updated (requires restart): + +- L1 publisher signers +- Prover keys +- HA signer connections + +New validators must use a publisher key already initialized at startup. Reload is rejected with a clear error if validation fails. + +### Admin API key authentication + +The admin JSON-RPC endpoint now supports auto-generated API key authentication. + +**Behavior:** + +- A cryptographically secure API key is auto-generated at first startup and displayed once via stdout +- Only the SHA-256 hash is persisted to `<dataDirectory>/admin/api_key_hash` +- The key is reused across restarts when `--data-directory` is set +- Supports both `x-api-key` and `Authorization: Bearer <key>` headers +- Health check endpoint (`GET /status`) is excluded from auth (for k8s probes) + +**Configuration:** + +```bash +--admin-api-key-hash <hex> ($AZTEC_ADMIN_API_KEY_HASH) # Use a pre-generated SHA-256 key hash +--disable-admin-api-key ($AZTEC_DISABLE_ADMIN_API_KEY) # Disable auth entirely +--reset-admin-api-key ($AZTEC_RESET_ADMIN_API_KEY) # Force key regeneration +``` + +**Helm charts**: Admin API key auth is disabled by default (`disableAdminApiKey: true`). Set to `false` in production values to enable. + +**Migration**: No action required — auth is opt-out. To enable, ensure `--disable-admin-api-key` is not set and note the key printed at startup. + +### Transaction pool error codes for RPC callers + +Transaction submission via RPC now returns structured rejection codes when a transaction is rejected by the mempool: + +- `LOW_PRIORITY_FEE` — tx priority fee is too low +- `INSUFFICIENT_FEE_PAYER_BALANCE` — fee payer doesn't have enough balance +- `NULLIFIER_CONFLICT` — conflicting nullifier already in pool + +**Impact**: Improved developer experience — callers can now programmatically handle specific rejection reasons. + +### RPC transaction replacement price bump + +Transactions submitted via RPC that clash on nullifiers with existing pool transactions must now pay at least X% more in priority fee to replace them. The same bump applies when the pool is full and the incoming tx needs to evict the lowest-priority tx. P2P gossip behavior is unchanged. + +**Configuration:** + +```bash +P2P_RPC_PRICE_BUMP_PERCENTAGE=10 # default: 10 (percent) +``` + +Set to `0` to disable the percentage-based bump (still requires strictly higher fee). + +### Validator-specific block limits + +Validators can now enforce per-block and per-checkpoint limits independently from the sequencer (proposer) limits. This allows operators to accept proposals that exceed their own proposer settings, or to reject proposals that are too large even if the proposer's limits allow them. + +**Configuration:** + +```bash +VALIDATOR_MAX_L2_BLOCK_GAS=<value> # Max L2 gas per block for validation +VALIDATOR_MAX_DA_BLOCK_GAS=<value> # Max DA gas per block for validation +VALIDATOR_MAX_TX_PER_BLOCK=<value> # Max txs per block for validation +VALIDATOR_MAX_TX_PER_CHECKPOINT=<value> # Max txs per checkpoint for validation +``` + +When not set, no per-block limit is enforced for that dimension — only checkpoint-level protocol limits apply. These do not fall back to the `SEQ_` values. + +### Setup allow list extendable via network config + +The setup phase allow list can now be extended via the network configuration JSON (`txPublicSetupAllowListExtend` field). This allows network operators to distribute additional allowed setup functions to all nodes without requiring code changes. The local environment variable takes precedence over the network-json value. + +## Changed defaults + +## Troubleshooting + +## Next steps + +- [How to Run a Sequencer Node](../../operators/setup/sequencer-setup.md) - Updated setup instructions +- [Advanced Keystore Usage](../../operators/keystore/creating-keystores.md) - Keystore configuration +- [Ethereum RPC Calls Reference](../../operators/reference/ethereum-rpc-reference.md) - Infrastructure requirements +- [Aztec Discord](https://discord.gg/aztec) - Upgrade support diff --git a/docs/lines/v6/examples/circuits/Nargo.toml b/docs/lines/v6/examples/circuits/Nargo.toml new file mode 100644 index 00000000000..c747d8f214e --- /dev/null +++ b/docs/lines/v6/examples/circuits/Nargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "hello_circuit" +] diff --git a/docs/lines/v6/examples/circuits/hello_circuit/Nargo.toml b/docs/lines/v6/examples/circuits/hello_circuit/Nargo.toml new file mode 100644 index 00000000000..2717a768a74 --- /dev/null +++ b/docs/lines/v6/examples/circuits/hello_circuit/Nargo.toml @@ -0,0 +1,8 @@ +# docs:start:circuit_nargo_toml +[package] +name = "hello_circuit" +type = "bin" +authors = [""] + +[dependencies] +# docs:end:circuit_nargo_toml diff --git a/docs/lines/v6/examples/circuits/hello_circuit/src/main.nr b/docs/lines/v6/examples/circuits/hello_circuit/src/main.nr new file mode 100644 index 00000000000..569c173e57a --- /dev/null +++ b/docs/lines/v6/examples/circuits/hello_circuit/src/main.nr @@ -0,0 +1,10 @@ +// docs:start:circuit +fn main(x: u64, y: pub u64) { + assert(x != y); +} + +#[test] +fn test_main() { + main(1, 2); +} +// docs:end:circuit diff --git a/docs/lines/v6/examples/contracts/aave_bridge/Nargo.toml b/docs/lines/v6/examples/contracts/aave_bridge/Nargo.toml new file mode 100644 index 00000000000..f396474a66f --- /dev/null +++ b/docs/lines/v6/examples/contracts/aave_bridge/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "aave_bridge" +type = "contract" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } +token_portal_content_hash_lib = { path = "../../../.aztec-node/tree/noir-projects/noir-contracts/contracts/libs/token_portal_content_hash_lib" } +token = { path = "../../../.aztec-node/tree/noir-projects/noir-contracts/contracts/app/token_contract" } diff --git a/docs/lines/v6/examples/contracts/aave_bridge/src/config.nr b/docs/lines/v6/examples/contracts/aave_bridge/src/config.nr new file mode 100644 index 00000000000..7ea7e5724d5 --- /dev/null +++ b/docs/lines/v6/examples/contracts/aave_bridge/src/config.nr @@ -0,0 +1,13 @@ +// docs:start:config +use aztec::protocol::{ + address::{AztecAddress, EthAddress}, + traits::{Deserialize, Packable, Serialize}, +}; +use std::meta::derive; + +#[derive(Deserialize, Eq, Packable, Serialize)] +pub struct Config { + pub token: AztecAddress, + pub portal: EthAddress, +} +// docs:end:config diff --git a/docs/lines/v6/examples/contracts/aave_bridge/src/main.nr b/docs/lines/v6/examples/contracts/aave_bridge/src/main.nr new file mode 100644 index 00000000000..44ac497b207 --- /dev/null +++ b/docs/lines/v6/examples/contracts/aave_bridge/src/main.nr @@ -0,0 +1,134 @@ +// docs:start:bridge_setup +mod config; + +// A bridge contract that allows users to deposit tokens into Aave on L1 from Aztec L2, +// and claim yield-bearing tokens back on L2. The bridge mirrors TokenBridge's pattern: +// all Aave-specific logic lives on L1, while L2 simply burns/mints tokens and passes messages. + +use aztec::macros::aztec; +#[aztec] +pub contract AaveBridge { + use crate::config::Config; + + use aztec::{protocol::address::{AztecAddress, EthAddress}, state_vars::PublicImmutable}; + + use token_portal_content_hash_lib::{ + get_mint_to_private_content_hash, get_mint_to_public_content_hash, + get_withdraw_content_hash, + }; + + use token::Token; + + use aztec::macros::{functions::{external, initializer, view}, storage::storage}; + + #[storage] + struct Storage<Context> { + config: PublicImmutable<Config, Context>, + } + + #[external("public")] + #[initializer] + fn constructor(token: AztecAddress, portal: EthAddress) { + self.storage.config.initialize(Config { token, portal }); + } + + #[external("private")] + #[view] + fn get_config() -> Config { + self.storage.config.read() + } + // docs:end:bridge_setup + + // docs:start:claim_public + /// Consume an L1->L2 message and mint tokens publicly. + /// Called after the L1 AavePortal withdraws from Aave and sends a message. + #[external("public")] + fn claim_public(to: AztecAddress, amount: u128, secret: Field, message_leaf_index: Field) { + let content_hash = get_mint_to_public_content_hash(to, amount); + let config = self.storage.config.read(); + + // Consume message and emit nullifier + self.context.consume_l1_to_l2_message( + content_hash, + [secret], + config.portal, + message_leaf_index, + ); + + // Mint tokens (including any yield from Aave) + self.call(Token::at(config.token).mint_to_public(to, amount)); + } + // docs:end:claim_public + + // docs:start:claim_private + /// Consume an L1->L2 message and mint tokens privately. + /// The recipient's address is not revealed, but the amount is. + #[external("private")] + fn claim_private( + recipient: AztecAddress, + amount: u128, + secret_for_L1_to_L2_message_consumption: Field, + message_leaf_index: Field, + ) { + let config = self.storage.config.read(); + + // Consume L1 to L2 message and emit nullifier + let content_hash = get_mint_to_private_content_hash(amount); + self.context.consume_l1_to_l2_message( + content_hash, + [secret_for_L1_to_L2_message_consumption], + config.portal, + message_leaf_index, + ); + + // Mint tokens privately + self.call(Token::at(config.token).mint_to_private(recipient, amount)); + } + // docs:end:claim_private + + // docs:start:exit_to_l1_public + /// Burn tokens publicly and create an L2->L1 message. + /// The L1 AavePortal will consume this message and deposit into Aave. + #[external("public")] + fn exit_to_l1_public( + recipient: EthAddress, + amount: u128, + caller_on_l1: EthAddress, + authwit_nonce: Field, + ) { + let config = self.storage.config.read(); + + // Send an L2 to L1 message + let content = get_withdraw_content_hash(recipient, amount, caller_on_l1); + self.context.message_portal(config.portal, content); + + // Burn tokens + self.call(Token::at(config.token).burn_public(self.msg_sender(), amount, authwit_nonce)); + } + // docs:end:exit_to_l1_public + + // docs:start:exit_to_l1_private + /// Burn tokens privately and create an L2->L1 message. + /// The L1 AavePortal will consume this message and deposit into Aave. + #[external("private")] + fn exit_to_l1_private( + token: AztecAddress, + recipient: EthAddress, + amount: u128, + caller_on_l1: EthAddress, + authwit_nonce: Field, + ) { + let config = self.storage.config.read(); + + // Assert that user provided token address is same as seen in storage + assert_eq(config.token, token, "Token address is not the same as seen in storage"); + + // Send an L2 to L1 message + let content = get_withdraw_content_hash(recipient, amount, caller_on_l1); + self.context.message_portal(config.portal, content); + + // Burn tokens privately + self.call(Token::at(token).burn_private(self.msg_sender(), amount, authwit_nonce)); + } + // docs:end:exit_to_l1_private +} diff --git a/docs/lines/v6/examples/contracts/bob_token_contract/Nargo.toml b/docs/lines/v6/examples/contracts/bob_token_contract/Nargo.toml new file mode 100644 index 00000000000..5c721ddbca7 --- /dev/null +++ b/docs/lines/v6/examples/contracts/bob_token_contract/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "bob_token_contract" +type = "contract" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } +balance_set = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/balance-set" } diff --git a/docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr b/docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr new file mode 100644 index 00000000000..f1d56bed4bc --- /dev/null +++ b/docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr @@ -0,0 +1,171 @@ +// docs:start:start +use aztec::macros::aztec; + +// docs:start:imports +#[aztec] +pub contract BobToken { + // docs:end:start + use aztec::{ + macros::{functions::{external, initializer, only_self}, storage::storage}, + messages::delivery::MessageDelivery, + protocol::address::AztecAddress, + state_vars::{Map, Owned, PublicMutable}, + }; + // docs:end:imports + + use balance_set::BalanceSet; + + // docs:start:public_storage + // docs:start:storage + #[storage] + struct Storage<Context> { + // Giggle's admin address + owner: PublicMutable<AztecAddress, Context>, + // Public balances - visible for transparency + public_balances: Map<AztecAddress, PublicMutable<u64, Context>, Context>, + // docs:end:public_storage + // Private balances - only the owner can see these + private_balances: Owned<BalanceSet<Context>, Context>, + } + // docs:end:storage + + // docs:start:setup + #[initializer] + #[external("public")] + fn setup() { + // Giggle becomes the owner who can mint mental health tokens + self.storage.owner.write(self.msg_sender()); + } + // docs:end:setup + + // docs:start:mint_public + #[external("public")] + fn mint_public(employee: AztecAddress, amount: u64) { + // Only Giggle can mint tokens + assert_eq(self.msg_sender(), self.storage.owner.read(), "Only Giggle can mint BOB tokens"); + + // Add tokens to employee's public balance + let current_balance = self.storage.public_balances.at(employee).read(); + self.storage.public_balances.at(employee).write(current_balance + amount); + } + // docs:end:mint_public + + // docs:start:transfer_public + #[external("public")] + fn transfer_public(to: AztecAddress, amount: u64) { + let sender = self.msg_sender(); + let sender_balance = self.storage.public_balances.at(sender).read(); + assert(sender_balance >= amount, "Insufficient BOB tokens"); + + // Deduct from sender + self.storage.public_balances.at(sender).write(sender_balance - amount); + + // Add to recipient + let recipient_balance = self.storage.public_balances.at(to).read(); + self.storage.public_balances.at(to).write(recipient_balance + amount); + } + // docs:end:transfer_public + + // docs:start:transfer_ownership + #[external("public")] + fn transfer_ownership(new_owner: AztecAddress) { + assert_eq( + self.msg_sender(), + self.storage.owner.read(), + "Only current admin can transfer ownership", + ); + self.storage.owner.write(new_owner); + } + // docs:end:transfer_ownership + + // docs:start:public_to_private + #[external("private")] + fn public_to_private(amount: u64) { + let sender = self.msg_sender(); + // This will enqueue a public function to deduct from public balance + self.enqueue_self._deduct_public_balance(sender, amount); + // Add to private balance + self.storage.private_balances.at(sender).add(amount as u128).deliver( + MessageDelivery::onchain_constrained(), + ); + } + // docs:end:public_to_private + + // docs:start:_deduct_public_balance + #[external("public")] + #[only_self] + fn _deduct_public_balance(owner: AztecAddress, amount: u64) { + let balance = self.storage.public_balances.at(owner).read(); + assert(balance >= amount, "Insufficient public BOB tokens"); + self.storage.public_balances.at(owner).write(balance - amount); + } + // docs:end:_deduct_public_balance + + // docs:start:transfer_private + #[external("private")] + fn transfer_private(to: AztecAddress, amount: u64) { + let sender = self.msg_sender(); + // Spend sender's notes (consumes existing notes) + self.storage.private_balances.at(sender).sub(amount as u128).deliver( + MessageDelivery::onchain_constrained(), + ); + // Create new notes for recipient + self.storage.private_balances.at(to).add(amount as u128).deliver( + MessageDelivery::onchain_constrained(), + ); + } + // docs:end:transfer_private + + // docs:start:check_balances + #[external("utility")] + unconstrained fn private_balance_of(owner: AztecAddress) -> pub u128 { + self.storage.private_balances.at(owner).balance_of() + } + + #[external("utility")] + unconstrained fn public_balance_of(owner: AztecAddress) -> pub u64 { + self.storage.public_balances.at(owner).read() + } + // docs:end:check_balances + + // docs:start:_assert_is_owner + #[external("public")] + #[only_self] + fn _assert_is_owner(address: AztecAddress) { + assert_eq(address, self.storage.owner.read(), "Only Giggle can mint BOB tokens"); + } + // docs:end:_assert_is_owner + + // docs:start:mint_private + #[external("private")] + fn mint_private(employee: AztecAddress, amount: u64) { + // Enqueue ownership check (will revert if not Giggle) + self.enqueue_self._assert_is_owner(self.msg_sender()); + + // If check passes, mint tokens privately + self.storage.private_balances.at(employee).add(amount as u128).deliver( + MessageDelivery::onchain_constrained(), + ); + } + // docs:end:mint_private + + // docs:start:private_to_public + #[external("private")] + fn private_to_public(amount: u64) { + let sender = self.msg_sender(); + // Remove from private balance + self.storage.private_balances.at(sender).sub(amount as u128).deliver( + MessageDelivery::onchain_constrained(), + ); + // Enqueue public credit + self.enqueue_self._credit_public_balance(sender, amount); + } + + #[external("public")] + #[only_self] + fn _credit_public_balance(owner: AztecAddress, amount: u64) { + let balance = self.storage.public_balances.at(owner).read(); + self.storage.public_balances.at(owner).write(balance + amount); + } + // docs:end:private_to_public +} diff --git a/docs/lines/v6/examples/contracts/counter_contract/Nargo.toml b/docs/lines/v6/examples/contracts/counter_contract/Nargo.toml new file mode 100644 index 00000000000..e589e573ac5 --- /dev/null +++ b/docs/lines/v6/examples/contracts/counter_contract/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "counter_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } +balance_set = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/balance-set" } diff --git a/docs/lines/v6/examples/contracts/counter_contract/src/main.nr b/docs/lines/v6/examples/contracts/counter_contract/src/main.nr new file mode 100644 index 00000000000..0eb435c7e9c --- /dev/null +++ b/docs/lines/v6/examples/contracts/counter_contract/src/main.nr @@ -0,0 +1,50 @@ +// docs:start:setup +use aztec::macros::aztec; + +#[aztec] +pub contract Counter { + // docs:end:setup + // docs:start:imports + use aztec::{ + macros::{functions::{external, initializer}, storage::storage}, + messages::delivery::MessageDelivery, + oracle::logging::debug_log_format, + protocol::{address::AztecAddress, traits::ToField}, + state_vars::Owned, + }; + use balance_set::BalanceSet; + // docs:end:imports + + // docs:start:storage_struct + #[storage] + struct Storage<Context> { + counters: Owned<BalanceSet<Context>, Context>, + } + // docs:end:storage_struct + + // docs:start:constructor + #[initializer] + #[external("private")] + // We can name our initializer anything we want as long as it's marked as #[initializer] + fn constructor(initial_value: u128, owner: AztecAddress) { + self.storage.counters.at(owner).add(initial_value).deliver( + MessageDelivery::onchain_constrained(), + ); + } + // docs:end:constructor + + // docs:start:increment + #[external("private")] + fn increment(owner: AztecAddress) { + debug_log_format("Incrementing counter for owner {0}", [owner.to_field()]); + self.storage.counters.at(owner).add(1).deliver(MessageDelivery::onchain_constrained()); + } + // docs:end:increment + + // docs:start:get_counter + #[external("utility")] + unconstrained fn get_counter(owner: AztecAddress) -> pub u128 { + self.storage.counters.at(owner).balance_of() + } + // docs:end:get_counter +} diff --git a/docs/lines/v6/examples/contracts/counter_contract_test/Nargo.toml b/docs/lines/v6/examples/contracts/counter_contract_test/Nargo.toml new file mode 100644 index 00000000000..da6679ab0d4 --- /dev/null +++ b/docs/lines/v6/examples/contracts/counter_contract_test/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "counter_contract_test" +authors = [""] +compiler_version = ">=0.25.0" +type = "lib" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } +counter_contract = { path = "../counter_contract" } diff --git a/docs/lines/v6/examples/contracts/counter_contract_test/src/lib.nr b/docs/lines/v6/examples/contracts/counter_contract_test/src/lib.nr new file mode 100644 index 00000000000..c9b1c24424d --- /dev/null +++ b/docs/lines/v6/examples/contracts/counter_contract_test/src/lib.nr @@ -0,0 +1,33 @@ +use counter_contract::Counter; +use aztec::{protocol::address::AztecAddress, test::helpers::test_environment::TestEnvironment}; + +pub unconstrained fn setup(initial_value: u128) -> (TestEnvironment, AztecAddress, AztecAddress) { + let mut env = TestEnvironment::new(); + let owner = env.create_light_account(); + + let initializer = Counter::interface().constructor(initial_value, owner); + let contract_address = + env.deploy("@counter_contract/Counter").with_private_initializer(owner, initializer); + (env, contract_address, owner) +} + +#[test] +unconstrained fn test_constructor_sets_initial_value() { + let initial_value = 5; + let (env, contract_address, owner) = setup(initial_value); + + let value = env.execute_utility(Counter::at(contract_address).get_counter(owner)); + assert(value == initial_value, f"Expected {initial_value} but got {value}"); +} + +#[test] +unconstrained fn test_increment() { + let initial_value = 5; + let (env, contract_address, owner) = setup(initial_value); + + env.call_private(owner, Counter::at(contract_address).increment(owner)); + + let expected = initial_value + 1; + let value = env.execute_utility(Counter::at(contract_address).get_counter(owner)); + assert(value == expected, f"Expected {expected} but got {value}"); +} diff --git a/docs/lines/v6/examples/contracts/example_uniswap/Nargo.toml b/docs/lines/v6/examples/contracts/example_uniswap/Nargo.toml new file mode 100644 index 00000000000..a7e172018e9 --- /dev/null +++ b/docs/lines/v6/examples/contracts/example_uniswap/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "example_uniswap" +type = "contract" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } +token = { path = "../../../.aztec-node/tree/noir-projects/noir-contracts/contracts/app/token_contract" } +token_bridge = { path = "../../../.aztec-node/tree/noir-projects/noir-contracts/contracts/app/token_bridge_contract" } +keccak256 = { tag = "v0.1.3", git = "https://github.com/noir-lang/keccak256" } diff --git a/docs/lines/v6/examples/contracts/example_uniswap/src/main.nr b/docs/lines/v6/examples/contracts/example_uniswap/src/main.nr new file mode 100644 index 00000000000..90f1115c8ec --- /dev/null +++ b/docs/lines/v6/examples/contracts/example_uniswap/src/main.nr @@ -0,0 +1,221 @@ +// docs:start:example_uniswap_setup +mod util; + +// Example Uniswap L2 contract for tutorial purposes. +// Demonstrates how to use portal contracts to swap on L1 with funds on L2. +// Has two separate flows for public and private swaps. +use aztec::macros::aztec; + +#[aztec] +pub contract ExampleUniswap { + use aztec::{ + authwit::auth::{ + assert_current_call_valid_authwit_public, compute_authwit_message_hash_from_call, + set_authorized, + }, + macros::{functions::{external, initializer, only_self}, storage::storage}, + protocol::{ + abis::function_selector::FunctionSelector, + address::{AztecAddress, EthAddress}, + traits::ToField, + }, + state_vars::PublicImmutable, + }; + + use crate::util::{compute_swap_private_content_hash, compute_swap_public_content_hash}; + use token::Token; + use token_bridge::TokenBridge; + + #[storage] + struct Storage<Context> { + portal_address: PublicImmutable<EthAddress, Context>, + } + + #[external("public")] + #[initializer] + fn constructor(portal_address: EthAddress) { + self.storage.portal_address.initialize(portal_address); + } + // docs:end:example_uniswap_setup + + // docs:start:swap_public + #[external("public")] + fn swap_public( + sender: AztecAddress, + input_asset_bridge: AztecAddress, + input_amount: u128, + output_asset_bridge: AztecAddress, + // params for the swap + uniswap_fee_tier: Field, + minimum_output_amount: u128, + // params for depositing output_asset back to Aztec + recipient: AztecAddress, + secret_hash_for_L1_to_l2_message: Field, + ) { + // If caller is not the sender, check they have approval + if (!sender.eq(self.msg_sender())) { + assert_current_call_valid_authwit_public(self.context, sender); + } + + let input_asset_bridge_config = + self.view(TokenBridge::at(input_asset_bridge).get_config_public()); + + let input_asset = input_asset_bridge_config.token; + let input_asset_bridge_portal_address = input_asset_bridge_config.portal; + + // Transfer funds from sender to this contract + // We use a fixed nonce since we authorize and spend in the same public call + let nonce_for_transfer = 0xdeadbeef; + let transfer_selector = + FunctionSelector::from_signature("transfer_in_public((Field),(Field),u128,Field)"); + let transfer_msg_hash = compute_authwit_message_hash_from_call( + self.address, + input_asset, + self.context.chain_id(), + self.context.version(), + transfer_selector, + [sender.to_field(), self.address.to_field(), input_amount as Field, nonce_for_transfer], + ); + set_authorized(self.context, transfer_msg_hash, true); + + self.call(Token::at(input_asset).transfer_in_public( + sender, + self.address, + input_amount, + nonce_for_transfer, + )); + + // Approve bridge to burn this contract's funds and exit to L1 Uniswap Portal. + // `let _ =` explicitly discards the return value (Noir requires handling all return values). + let _ = self.call_self._approve_bridge_and_exit_input_asset_to_L1( + input_asset, + input_asset_bridge, + input_amount, + ); + + // Create swap message and send to Outbox for Uniswap Portal + let output_asset_bridge_portal_address = + self.view(TokenBridge::at(output_asset_bridge).get_config_public()).portal; + + // Ensure portals exist - else funds might be lost + assert( + !input_asset_bridge_portal_address.is_zero(), + "L1 portal address of input_asset's bridge is 0", + ); + assert( + !output_asset_bridge_portal_address.is_zero(), + "L1 portal address of output_asset's bridge is 0", + ); + + let content_hash = compute_swap_public_content_hash( + input_asset_bridge_portal_address, + input_amount, + uniswap_fee_tier, + output_asset_bridge_portal_address, + minimum_output_amount, + recipient, + secret_hash_for_L1_to_l2_message, + ); + self.context.message_portal(self.storage.portal_address.read(), content_hash); + } + // docs:end:swap_public + + // docs:start:swap_private + #[external("private")] + fn swap_private( + input_asset: AztecAddress, + input_asset_bridge: AztecAddress, + input_amount: u128, + output_asset_bridge: AztecAddress, + // params for the swap + uniswap_fee_tier: Field, + minimum_output_amount: u128, + // params for depositing output_asset back to Aztec + secret_hash_for_L1_to_l2_message: Field, + ) { + let input_asset_bridge_config = + self.view(TokenBridge::at(input_asset_bridge).get_config()); + let output_asset_bridge_config = + self.view(TokenBridge::at(output_asset_bridge).get_config()); + + // Verify the token address matches the bridge's config + assert( + input_asset.eq(input_asset_bridge_config.token), + "input_asset address is not the same as seen in the bridge contract", + ); + + // Transfer funds from sender to this contract (private -> public) + // We use a fixed nonce since we authorize and spend in the same tx + let nonce_for_transfer = 0xdeadbeef; + self.call(Token::at(input_asset).transfer_to_public( + self.msg_sender(), + self.address, + input_amount, + nonce_for_transfer, + )); + + // Approve bridge to burn this contract's funds and exit to L1 Uniswap Portal + self.enqueue_self._approve_bridge_and_exit_input_asset_to_L1( + input_asset, + input_asset_bridge, + input_amount, + ); + + // Ensure portals exist + assert( + !input_asset_bridge_config.portal.is_zero(), + "L1 portal address of input_asset's bridge is 0", + ); + assert( + !output_asset_bridge_config.portal.is_zero(), + "L1 portal address of output_asset's bridge is 0", + ); + + let content_hash = compute_swap_private_content_hash( + input_asset_bridge_config.portal, + input_amount, + uniswap_fee_tier, + output_asset_bridge_config.portal, + minimum_output_amount, + secret_hash_for_L1_to_l2_message, + ); + self.context.message_portal(self.storage.portal_address.read(), content_hash); + } + // docs:end:swap_private + + // docs:start:approve_bridge_and_exit + // Internal helper: approves the bridge to burn this contract's funds and exits to L1. + // Used by both public and private swap flows. + #[external("public")] + #[only_self] + fn _approve_bridge_and_exit_input_asset_to_L1( + token: AztecAddress, + token_bridge: AztecAddress, + amount: u128, + ) { + // Since we authorize and instantly spend in the same public call, reuse a fixed nonce. + let authwit_nonce = 0xdeadbeef; + + let selector = FunctionSelector::from_signature("burn_public((Field),u128,Field)"); + let message_hash = compute_authwit_message_hash_from_call( + token_bridge, + token, + self.context.chain_id(), + self.context.version(), + selector, + [self.address.to_field(), amount as Field, authwit_nonce], + ); + + set_authorized(self.context, message_hash, true); + + let this_portal_address = self.storage.portal_address.read(); + // Exit to L1 Uniswap Portal + self.call(TokenBridge::at(token_bridge).exit_to_l1_public( + this_portal_address, + amount, + this_portal_address, + authwit_nonce, + )); + } + // docs:end:approve_bridge_and_exit +} diff --git a/docs/lines/v6/examples/contracts/example_uniswap/src/util.nr b/docs/lines/v6/examples/contracts/example_uniswap/src/util.nr new file mode 100644 index 00000000000..f651d3eea84 --- /dev/null +++ b/docs/lines/v6/examples/contracts/example_uniswap/src/util.nr @@ -0,0 +1,110 @@ +// docs:start:swap_public_content_hash +use aztec::protocol::{ + address::{AztecAddress, EthAddress}, + hash::sha256_to_field, + traits::ToField, +}; + +// Hash byte sizes: number of fields x 32 bytes + 4 byte selector +// Public: 7 fields (input portal, amount, fee, output portal, min output, recipient, secret hash) +global PUBLIC_SWAP_HASH_SIZE: u32 = 228; +// Private: 6 fields (no recipient - implicit from sender for privacy) +global PRIVATE_SWAP_HASH_SIZE: u32 = 196; + +// Computes the L2 to L1 message content hash for the public swap flow. +// Must match the hash computed in ExampleUniswapPortal.sol on L1. +pub fn compute_swap_public_content_hash( + input_asset_bridge_portal_address: EthAddress, + input_amount: u128, + uniswap_fee_tier: Field, + output_asset_bridge_portal_address: EthAddress, + minimum_output_amount: u128, + aztec_recipient: AztecAddress, + secret_hash_for_L1_to_l2_message: Field, +) -> Field { + let mut hash_bytes = [0; PUBLIC_SWAP_HASH_SIZE]; + let input_token_portal_bytes: [u8; 32] = + input_asset_bridge_portal_address.to_field().to_be_bytes(); + let in_amount_bytes: [u8; 32] = input_amount.to_field().to_be_bytes(); + let uniswap_fee_tier_bytes: [u8; 32] = uniswap_fee_tier.to_be_bytes(); + let output_token_portal_bytes: [u8; 32] = + output_asset_bridge_portal_address.to_field().to_be_bytes(); + let amount_out_min_bytes: [u8; 32] = minimum_output_amount.to_field().to_be_bytes(); + let aztec_recipient_bytes: [u8; 32] = aztec_recipient.to_field().to_be_bytes(); + let secret_hash_for_L1_to_l2_message_bytes: [u8; 32] = + secret_hash_for_L1_to_l2_message.to_be_bytes(); + + // The function selector makes the message unique to this specific call type. + let selector = comptime { + keccak256::keccak256( + "swap_public(address,uint256,uint24,address,uint256,bytes32,bytes32)".as_bytes(), + 67, + ) + }; + + hash_bytes[0] = selector[0]; + hash_bytes[1] = selector[1]; + hash_bytes[2] = selector[2]; + hash_bytes[3] = selector[3]; + + for i in 0..32 { + hash_bytes[i + 4] = input_token_portal_bytes[i]; + hash_bytes[i + 36] = in_amount_bytes[i]; + hash_bytes[i + 68] = uniswap_fee_tier_bytes[i]; + hash_bytes[i + 100] = output_token_portal_bytes[i]; + hash_bytes[i + 132] = amount_out_min_bytes[i]; + hash_bytes[i + 164] = aztec_recipient_bytes[i]; + hash_bytes[i + 196] = secret_hash_for_L1_to_l2_message_bytes[i]; + } + + sha256_to_field(hash_bytes) +} +// docs:end:swap_public_content_hash + +// docs:start:swap_private_content_hash +// Computes the L2 to L1 message content hash for the private swap flow. +// Must match the hash computed in ExampleUniswapPortal.sol on L1. +pub fn compute_swap_private_content_hash( + input_asset_bridge_portal_address: EthAddress, + input_amount: u128, + uniswap_fee_tier: Field, + output_asset_bridge_portal_address: EthAddress, + minimum_output_amount: u128, + secret_hash_for_L1_to_l2_message: Field, +) -> Field { + let mut hash_bytes = [0; PRIVATE_SWAP_HASH_SIZE]; + let input_token_portal_bytes: [u8; 32] = + input_asset_bridge_portal_address.to_field().to_be_bytes(); + let in_amount_bytes: [u8; 32] = input_amount.to_field().to_be_bytes(); + let uniswap_fee_tier_bytes: [u8; 32] = uniswap_fee_tier.to_be_bytes(); + let output_token_portal_bytes: [u8; 32] = + output_asset_bridge_portal_address.to_field().to_be_bytes(); + let amount_out_min_bytes: [u8; 32] = minimum_output_amount.to_field().to_be_bytes(); + let secret_hash_for_L1_to_l2_message_bytes: [u8; 32] = + secret_hash_for_L1_to_l2_message.to_be_bytes(); + + // The function selector makes the message unique to this specific call type. + let selector = comptime { + keccak256::keccak256( + "swap_private(address,uint256,uint24,address,uint256,bytes32)".as_bytes(), + 60, + ) + }; + + hash_bytes[0] = selector[0]; + hash_bytes[1] = selector[1]; + hash_bytes[2] = selector[2]; + hash_bytes[3] = selector[3]; + + for i in 0..32 { + hash_bytes[i + 4] = input_token_portal_bytes[i]; + hash_bytes[i + 36] = in_amount_bytes[i]; + hash_bytes[i + 68] = uniswap_fee_tier_bytes[i]; + hash_bytes[i + 100] = output_token_portal_bytes[i]; + hash_bytes[i + 132] = amount_out_min_bytes[i]; + hash_bytes[i + 164] = secret_hash_for_L1_to_l2_message_bytes[i]; + } + + sha256_to_field(hash_bytes) +} +// docs:end:swap_private_content_hash diff --git a/docs/lines/v6/examples/contracts/logging_example/Nargo.toml b/docs/lines/v6/examples/contracts/logging_example/Nargo.toml new file mode 100644 index 00000000000..d99b36561e3 --- /dev/null +++ b/docs/lines/v6/examples/contracts/logging_example/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "logging_example" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } diff --git a/docs/lines/v6/examples/contracts/logging_example/src/main.nr b/docs/lines/v6/examples/contracts/logging_example/src/main.nr new file mode 100644 index 00000000000..52f71c1be93 --- /dev/null +++ b/docs/lines/v6/examples/contracts/logging_example/src/main.nr @@ -0,0 +1,87 @@ +use aztec::macros::aztec; + +#[aztec] +pub contract LoggingExample { + // docs:start:logging_imports + use aztec::oracle::logging::{ + debug_log, debug_log_format, error_log, error_log_format, fatal_log, fatal_log_format, + info_log, info_log_format, trace_log, trace_log_format, verbose_log, verbose_log_format, + warn_log, warn_log_format, + }; + // docs:end:logging_imports + use aztec::{ + macros::functions::{external, initializer}, + protocol::{address::AztecAddress, traits::ToField}, + }; + + #[initializer] + #[external("public")] + fn constructor() {} + + #[external("private")] + fn log_private(value: Field) { + // docs:start:log_simple + // Simple messages (no arguments) + info_log("Private function called"); + debug_log("Checkpoint reached in private function"); + // docs:end:log_simple + + // Messages with format arguments + debug_log_format("Processing value: {0}", [value]); + } + + // docs:start:log_address + #[external("private")] + fn log_with_address(sender: AztecAddress) { + info_log_format("Sender: {0}", [sender.to_field()]); + } + // docs:end:log_address + + // docs:start:log_public + #[external("public")] + fn log_public(value: Field) { + info_log("Public function called"); + debug_log_format("Public value: {0}", [value]); + } + // docs:end:log_public + + // docs:start:log_all_levels + #[external("private")] + fn log_all_levels(value: Field) { + fatal_log("fatal level message"); + fatal_log_format("fatal: {0}", [value]); + error_log("error level message"); + error_log_format("error: {0}", [value]); + warn_log("warn level message"); + warn_log_format("warn: {0}", [value]); + info_log("info level message"); + info_log_format("info: {0}", [value]); + verbose_log("verbose level message"); + verbose_log_format("verbose: {0}", [value]); + debug_log("debug level message"); + debug_log_format("debug: {0}", [value]); + trace_log("trace level message"); + trace_log_format("trace: {0}", [value]); + } + // docs:end:log_all_levels + + // docs:start:log_format_patterns + #[external("private")] + fn log_format_patterns(a: Field, b: Field, c: Field) { + // Single indexed argument + debug_log_format("First value: {0}", [a]); + + // Multiple indexed arguments + info_log_format("Values: {0}, {1}, {2}", [a, b, c]); + + // Whole array dump + debug_log_format("All values: {}", [a, b, c]); + } + // docs:end:log_format_patterns + + #[external("utility")] + unconstrained fn log_utility(value: Field) { + debug_log("Utility function called"); + debug_log_format("Utility value: {0}", [value]); + } +} diff --git a/docs/lines/v6/examples/contracts/logging_example_test/Nargo.toml b/docs/lines/v6/examples/contracts/logging_example_test/Nargo.toml new file mode 100644 index 00000000000..bb5295fee9a --- /dev/null +++ b/docs/lines/v6/examples/contracts/logging_example_test/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "logging_example_test" +authors = [""] +compiler_version = ">=0.25.0" +type = "lib" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } +logging_example = { path = "../logging_example" } diff --git a/docs/lines/v6/examples/contracts/logging_example_test/src/lib.nr b/docs/lines/v6/examples/contracts/logging_example_test/src/lib.nr new file mode 100644 index 00000000000..844acbc5e1d --- /dev/null +++ b/docs/lines/v6/examples/contracts/logging_example_test/src/lib.nr @@ -0,0 +1,52 @@ +use logging_example::LoggingExample; +use aztec::{protocol::address::AztecAddress, test::helpers::test_environment::TestEnvironment}; + +pub unconstrained fn setup() -> (TestEnvironment, AztecAddress, AztecAddress) { + let mut env = TestEnvironment::new(); + let owner = env.create_light_account(); + let contract_address = env.deploy("@logging_example/LoggingExample") + .with_public_initializer(owner, LoggingExample::interface().constructor()); + (env, contract_address, owner) +} + +#[test] +unconstrained fn test_private_logging() { + let (env, contract_address, owner) = setup(); + env.call_private(owner, LoggingExample::at(contract_address).log_private(42)); +} + +#[test] +unconstrained fn test_public_logging() { + let (env, contract_address, owner) = setup(); + env.call_public(owner, LoggingExample::at(contract_address).log_public(99)); +} + +#[test] +unconstrained fn test_all_log_levels() { + let (env, contract_address, owner) = setup(); + env.call_private(owner, LoggingExample::at(contract_address).log_all_levels(42)); +} + +#[test] +unconstrained fn test_format_patterns() { + let (env, contract_address, owner) = setup(); + env.call_private( + owner, + LoggingExample::at(contract_address).log_format_patterns(10, 20, 30), + ); +} + +#[test] +unconstrained fn test_log_with_address() { + let (env, contract_address, owner) = setup(); + env.call_private( + owner, + LoggingExample::at(contract_address).log_with_address(owner), + ); +} + +#[test] +unconstrained fn test_utility_logging() { + let (env, contract_address, _owner) = setup(); + let _ = env.execute_utility(LoggingExample::at(contract_address).log_utility(77)); +} diff --git a/docs/lines/v6/examples/contracts/nft/Nargo.toml b/docs/lines/v6/examples/contracts/nft/Nargo.toml new file mode 100644 index 00000000000..6c56240996b --- /dev/null +++ b/docs/lines/v6/examples/contracts/nft/Nargo.toml @@ -0,0 +1,6 @@ +[package] +name = "nft" +type = "contract" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } diff --git a/docs/lines/v6/examples/contracts/nft/src/main.nr b/docs/lines/v6/examples/contracts/nft/src/main.nr new file mode 100644 index 00000000000..38efbf96d89 --- /dev/null +++ b/docs/lines/v6/examples/contracts/nft/src/main.nr @@ -0,0 +1,95 @@ +// docs:start:contract_setup +use aztec::macros::aztec; +pub mod nft; + +#[aztec] +pub contract NFTPunk { + use crate::nft::NFTNote; + use aztec::{ + macros::{functions::{external, initializer, only_self}, storage::storage}, + protocol::address::AztecAddress, + state_vars::{DelayedPublicMutable, Map, Owned, PrivateSet, PublicImmutable}, + }; + use aztec::messages::delivery::MessageDelivery; + use aztec::note::{ + note_getter_options::NoteGetterOptions, note_interface::NoteProperties, + note_viewer_options::NoteViewerOptions, + }; + use aztec::utils::comparison::Comparator; + + global NFT_EXISTS_DELAY: u64 = 60 * 60; // 1 hour + + #[storage] + struct Storage<Context> { + admin: PublicImmutable<AztecAddress, Context>, + minter: PublicImmutable<AztecAddress, Context>, + nfts: Map<Field, DelayedPublicMutable<bool, NFT_EXISTS_DELAY, Context>, Context>, + owners: Owned<PrivateSet<NFTNote, Context>, Context>, + } + #[external("public")] + #[initializer] + fn constructor(admin: AztecAddress) { + self.storage.admin.initialize(admin); + } + // docs:end:contract_setup + + // docs:start:set_minter + #[external("public")] + fn set_minter(minter: AztecAddress) { + assert(self.storage.admin.read().eq(self.msg_sender()), "caller is not admin"); + self.storage.minter.initialize(minter); + } + // docs:end:set_minter + + // docs:start:mark_nft_exists + #[external("public")] + #[only_self] + fn _mark_nft_exists(token_id: Field, exists: bool) { + self.storage.nfts.at(token_id).schedule_value_change(exists); + } + // docs:end:mark_nft_exists + + // docs:start:mint + #[external("private")] + fn mint(to: AztecAddress, token_id: Field) { + assert( + self.storage.minter.read().eq(self.msg_sender()), + "caller is not the authorized minter", + ); + + // we create an NFT note and insert it to the PrivateSet - a collection of notes meant to be read in private + let new_nft = NFTNote { token_id }; + self.storage.owners.at(to).insert(new_nft).deliver(MessageDelivery::onchain_constrained()); + + // calling the internal public function above to indicate that the NFT is taken + self.enqueue_self._mark_nft_exists(token_id, true); + } + // docs:end:mint + + // docs:start:notes_of + #[external("utility")] + unconstrained fn notes_of(from: AztecAddress) -> Field { + let notes = self.storage.owners.at(from).view_notes(NoteViewerOptions::new()); + notes.len() as Field + } + // docs:end:notes_of + + // docs:start:burn + #[external("private")] + fn burn(from: AztecAddress, token_id: Field) { + assert( + self.storage.minter.read().eq(self.msg_sender()), + "caller is not the authorized minter", + ); + + // from the NFTNote properties, selects token_id and compares it against the token_id to be burned + let options = NoteGetterOptions::new() + .select(NFTNote::properties().token_id, Comparator.EQ, token_id) + .set_limit(1); + let notes = self.storage.owners.at(from).pop_notes(options); + assert(notes.len() == 1, "NFT not found"); + + self.enqueue_self._mark_nft_exists(token_id, false); + } + // docs:end:burn +} diff --git a/docs/lines/v6/examples/contracts/nft/src/nft.nr b/docs/lines/v6/examples/contracts/nft/src/nft.nr new file mode 100644 index 00000000000..c4410942e48 --- /dev/null +++ b/docs/lines/v6/examples/contracts/nft/src/nft.nr @@ -0,0 +1,9 @@ +// docs:start:nft_note_struct +use aztec::{macros::notes::note, protocol::traits::Packable}; + +#[derive(Eq, Packable)] +#[note] +pub struct NFTNote { + pub token_id: Field, +} +// docs:end:nft_note_struct diff --git a/docs/lines/v6/examples/contracts/nft_bridge/Nargo.toml b/docs/lines/v6/examples/contracts/nft_bridge/Nargo.toml new file mode 100644 index 00000000000..0b48afdd88b --- /dev/null +++ b/docs/lines/v6/examples/contracts/nft_bridge/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "nft_bridge" +type = "contract" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } +NFTPunk = { path = "../nft" } diff --git a/docs/lines/v6/examples/contracts/nft_bridge/src/main.nr b/docs/lines/v6/examples/contracts/nft_bridge/src/main.nr new file mode 100644 index 00000000000..b2ae53de4ca --- /dev/null +++ b/docs/lines/v6/examples/contracts/nft_bridge/src/main.nr @@ -0,0 +1,66 @@ +// docs:start:bridge_setup +use aztec::macros::aztec; + +#[aztec] +pub contract NFTBridge { + use aztec::{ + macros::{functions::{external, initializer}, storage::storage}, + protocol::{address::{AztecAddress, EthAddress}, hash::sha256_to_field}, + state_vars::PublicImmutable, + }; + use NFTPunk::NFTPunk; + + #[storage] + struct Storage<Context> { + nft: PublicImmutable<AztecAddress, Context>, + portal: PublicImmutable<EthAddress, Context>, + } + + #[external("public")] + #[initializer] + fn constructor(nft: AztecAddress) { + self.storage.nft.initialize(nft); + } + + #[external("public")] + fn set_portal(portal: EthAddress) { + self.storage.portal.initialize(portal); + } + // docs:end:bridge_setup + + // docs:start:claim + #[external("private")] + fn claim(to: AztecAddress, token_id: Field, secret: Field, message_leaf_index: Field) { + // Compute the message hash that was sent from L1 + let token_id_bytes: [u8; 32] = (token_id as Field).to_be_bytes(); + let content_hash = sha256_to_field(token_id_bytes); + + // Consume the L1 -> L2 message + self.context.consume_l1_to_l2_message( + content_hash, + [secret], + self.storage.portal.read(), + message_leaf_index, + ); + + // Mint the NFT on L2 + let nft: AztecAddress = self.storage.nft.read(); + self.call(NFTPunk::at(nft).mint(to, token_id)); + } + // docs:end:claim + + // docs:start:exit + #[external("private")] + fn exit(token_id: Field, recipient: EthAddress) { + // Create L2->L1 message to unlock NFT on L1 + let token_id_bytes: [u8; 32] = token_id.to_be_bytes(); + let recipient_bytes: [u8; 20] = recipient.to_be_bytes(); + let content = sha256_to_field(token_id_bytes.concat(recipient_bytes)); + self.context.message_portal(self.storage.portal.read(), content); + + // Burn the NFT on L2 + let nft: AztecAddress = self.storage.nft.read(); + self.call(NFTPunk::at(nft).burn(self.msg_sender(), token_id)); + } + // docs:end:exit +} diff --git a/docs/lines/v6/examples/contracts/packing_example/Nargo.toml b/docs/lines/v6/examples/contracts/packing_example/Nargo.toml new file mode 100644 index 00000000000..eaf6484d4de --- /dev/null +++ b/docs/lines/v6/examples/contracts/packing_example/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "packing_example" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } diff --git a/docs/lines/v6/examples/contracts/packing_example/src/main.nr b/docs/lines/v6/examples/contracts/packing_example/src/main.nr new file mode 100644 index 00000000000..037878aff0b --- /dev/null +++ b/docs/lines/v6/examples/contracts/packing_example/src/main.nr @@ -0,0 +1,39 @@ +mod types; + +use aztec::macros::aztec; + +#[aztec] +pub contract PackingExample { + use aztec::{ + macros::{functions::{external, initializer}, storage::storage}, + protocol::address::AztecAddress, + state_vars::PublicMutable, + }; + + use crate::types::{Card, GameConfig}; + + #[storage] + struct Storage<Context> { + card: PublicMutable<Card, Context>, + config: PublicMutable<GameConfig, Context>, + } + + #[initializer] + #[external("public")] + fn constructor() {} + + #[external("public")] + fn set_card(strength: u32, points: u32) { + self.storage.card.write(Card { strength, points }); + } + + #[external("public")] + fn get_card() -> Card { + self.storage.card.read() + } + + #[external("public")] + fn set_config(interest_accumulator: u128, last_updated_ts: u64, admin: AztecAddress) { + self.storage.config.write(GameConfig { interest_accumulator, last_updated_ts, admin }); + } +} diff --git a/docs/lines/v6/examples/contracts/packing_example/src/types.nr b/docs/lines/v6/examples/contracts/packing_example/src/types.nr new file mode 100644 index 00000000000..b76f00bba43 --- /dev/null +++ b/docs/lines/v6/examples/contracts/packing_example/src/types.nr @@ -0,0 +1,225 @@ +use aztec::macros::notes::note; +use aztec::protocol::{ + address::AztecAddress, + traits::{Deserialize, FromField, Packable, Serialize, ToField}, +}; +use std::meta::derive; + +// docs:start:derived_packable +// When all members are Field-sized, derive(Packable) is sufficient. +// Each member gets its own Field -- no packing benefit, but no manual work needed. +#[derive(Eq, Packable, Serialize)] +pub struct ServerConfig { + pub admin: AztecAddress, + pub token: AztecAddress, + pub max_supply: Field, +} +// N = 3 (one Field per member) +// docs:end:derived_packable + +// docs:start:field_like_note +// A note whose members are already Field-sized. +// #[note] requires Packable; derive is sufficient here -- N = 1. +#[derive(Deserialize, Eq, Packable, Serialize)] +#[note] +pub struct OwnerNote { + pub owner: AztecAddress, +} +// docs:end:field_like_note + +// docs:start:card_note +// A note with two u32 members. Derived Packable would give N = 2; +// a custom impl halves that to N = 1, reducing note-hash inputs. +#[derive(Eq)] +#[note] +pub struct CardNote { + pub strength: u32, + pub points: u32, +} + +impl Packable for CardNote { + let N: u32 = 1; + + fn pack(self) -> [Field; Self::N] { + [(self.strength as Field) * 2.pow_32(32) + (self.points as Field)] + } + + fn unpack(packed: [Field; Self::N]) -> Self { + let points = packed[0] as u32; + let strength = ((packed[0] - points as Field) / 2.pow_32(32)) as u32; + Self { strength, points } + } +} +// docs:end:card_note + +// docs:start:card_custom_packable +// Two u32 values packed into a single Field. +// Derived Packable would give N = 2; manual packing gives N = 1. +#[derive(Deserialize, Eq, Serialize)] +pub struct Card { + pub strength: u32, + pub points: u32, +} + +impl Packable for Card { + let N: u32 = 1; + + fn pack(self) -> [Field; Self::N] { + [(self.strength as Field) * 2.pow_32(32) + (self.points as Field)] + } + + fn unpack(packed: [Field; Self::N]) -> Self { + let points = packed[0] as u32; + let strength = ((packed[0] - points as Field) / 2.pow_32(32)) as u32; + Self { strength, points } + } +} +// docs:end:card_custom_packable + +// docs:start:mixed_width_packable +// Mixed-width integers: a u128 and a u64 packed into one Field, +// plus an AztecAddress that takes a full Field on its own. +// Derived Packable would give N = 3; manual packing gives N = 2. +#[derive(Eq, Serialize)] +pub struct GameConfig { + pub interest_accumulator: u128, + pub last_updated_ts: u64, + pub admin: AztecAddress, +} + +impl Packable for GameConfig { + let N: u32 = 2; + + fn pack(self) -> [Field; Self::N] { + [ + // u128 (128 bits) + u64 (64 bits) = 192 bits, fits in one Field + (self.interest_accumulator as Field) * 2.pow_32(64) + + (self.last_updated_ts as Field), + self.admin.to_field(), + ] + } + + fn unpack(packed: [Field; Self::N]) -> Self { + let last_updated_ts = packed[0] as u64; + let interest_accumulator = + ((packed[0] - last_updated_ts as Field) / 2.pow_32(64)) as u128; + let admin = AztecAddress::from_field(packed[1]); + Self { interest_accumulator, last_updated_ts, admin } + } +} +// docs:end:mixed_width_packable + +// docs:start:game_state_manual_packable +// Mixed-width with a bool: started (1 bit) + round (32 bits) + score (32 bits). +// Derived Packable would give N = 3; manual packing gives N = 1. +#[derive(Eq, Serialize)] +pub struct GameState { + pub started: bool, + pub round: u32, + pub score: u32, +} + +impl Packable for GameState { + let N: u32 = 1; + + // docs:start:game_state_pack + fn pack(self) -> [Field; Self::N] { + // Layout within a single Field: + // [ started (1 bit) | round (32 bits) | score (32 bits) ] + // bit 64 bits 32..63 bits 0..31 + [ + (self.started as Field) * 2.pow_32(64) // shift left by 64 bits + + (self.round as Field) * 2.pow_32(32) // shift left by 32 bits + + (self.score as Field), // lowest bits + ] + } + // docs:end:game_state_pack + + // docs:start:game_state_unpack + fn unpack(packed: [Field; Self::N]) -> Self { + // 1. Extract lowest value via truncating cast + let score = packed[0] as u32; + + // 2. Subtract and shift right to get next value + let round = ((packed[0] - score as Field) / 2.pow_32(32)) as u32; + + // 3. Subtract and shift right to get highest value. + // Bools are extracted by comparing the resulting Field to 0. + let started = ((packed[0] - score as Field - (round as Field) * 2.pow_32(32)) + / 2.pow_32(64)) + != 0; + + Self { started, round, score } + } + // docs:end:game_state_unpack +} +// docs:end:game_state_manual_packable + +// docs:start:pack_unpack_tests +mod test { + use super::{AztecAddress, Card, FromField, GameConfig, GameState, Packable}; + + #[test] + fn test_card_pack_unpack() { + let card = Card { strength: 42, points: 100 }; + let unpacked = Card::unpack(card.pack()); + assert_eq(unpacked.strength, card.strength); + assert_eq(unpacked.points, card.points); + } + + #[test] + fn test_card_pack_unpack_max() { + let card = Card { strength: 0xffffffff, points: 0xffffffff }; + let unpacked = Card::unpack(card.pack()); + assert_eq(unpacked.strength, card.strength); + assert_eq(unpacked.points, card.points); + } + + #[test] + fn test_config_pack_unpack() { + let config = GameConfig { + interest_accumulator: 1000000, + last_updated_ts: 1700000000, + admin: AztecAddress::from_field(0xabcdef), + }; + let unpacked = GameConfig::unpack(config.pack()); + assert_eq(unpacked.interest_accumulator, config.interest_accumulator); + assert_eq(unpacked.last_updated_ts, config.last_updated_ts); + assert(unpacked.admin.eq(config.admin)); + } + + #[test] + fn test_config_pack_unpack_max() { + let config = GameConfig { + interest_accumulator: 0xffffffffffffffffffffffffffffffff, + last_updated_ts: 0xffffffffffffffff, + admin: AztecAddress::from_field(0xabcdef), + }; + let unpacked = GameConfig::unpack(config.pack()); + assert_eq(unpacked.interest_accumulator, config.interest_accumulator); + assert_eq(unpacked.last_updated_ts, config.last_updated_ts); + assert(unpacked.admin.eq(config.admin)); + } + + // docs:start:game_state_tests + #[test] + fn test_game_state_pack_unpack() { + let state = GameState { started: true, round: 42, score: 1000 }; + let unpacked = GameState::unpack(state.pack()); + assert_eq(unpacked.started, state.started); + assert_eq(unpacked.round, state.round); + assert_eq(unpacked.score, state.score); + } + + #[test] + fn test_game_state_pack_unpack_max() { + let state = + GameState { started: true, round: 0xffffffff, score: 0xffffffff }; + let unpacked = GameState::unpack(state.pack()); + assert_eq(unpacked.started, state.started); + assert_eq(unpacked.round, state.round); + assert_eq(unpacked.score, state.score); + } + // docs:end:game_state_tests +} +// docs:end:pack_unpack_tests diff --git a/docs/lines/v6/examples/contracts/recursive_verification_contract/Nargo.toml b/docs/lines/v6/examples/contracts/recursive_verification_contract/Nargo.toml new file mode 100644 index 00000000000..dbcefe87a09 --- /dev/null +++ b/docs/lines/v6/examples/contracts/recursive_verification_contract/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "recursive_verification_contract" +type = "contract" +authors = [""] +compiler_version = ">=0.25.0" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } +bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages", tag = "v6.0.0-rc.7", directory = "barretenberg/noir/bb_proof_verification" } diff --git a/docs/lines/v6/examples/contracts/recursive_verification_contract/src/main.nr b/docs/lines/v6/examples/contracts/recursive_verification_contract/src/main.nr new file mode 100644 index 00000000000..ea47f2fccbd --- /dev/null +++ b/docs/lines/v6/examples/contracts/recursive_verification_contract/src/main.nr @@ -0,0 +1,64 @@ +// docs:start:full_contract +use aztec::macros::aztec; + +#[aztec] +pub contract ValueNotEqual { + use aztec::{ + macros::{functions::{external, initializer, only_self, view}, storage::storage}, + oracle::logging::debug_log_format, + protocol::{address::AztecAddress, traits::ToField}, + state_vars::{Map, PublicImmutable, PublicMutable}, + }; + use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; + + #[storage] + struct Storage<Context> { + counters: Map<AztecAddress, PublicMutable<Field, Context>, Context>, + vk_hash: PublicImmutable<Field, Context>, + } + + #[initializer] + #[external("public")] + fn constructor(headstart: Field, owner: AztecAddress, vk_hash: Field) { + self.storage.counters.at(owner).write(headstart); + self.storage.vk_hash.initialize(vk_hash); + } + + #[external("private")] + fn increment( + owner: AztecAddress, + verification_key: UltraHonkVerificationKey, + proof: UltraHonkZKProof, + public_inputs: [Field; 1], + ) { + debug_log_format("Incrementing counter for owner {0}", [owner.to_field()]); + + // Read the stored VK hash - this is readable from private context + // because PublicImmutable values are committed at deployment + let vk_hash = self.storage.vk_hash.read(); + + // Verify the proof - this is the core operation + // The function checks: + // 1. The VK hashes to the stored vk_hash + // 2. The proof is valid for the given VK and public inputs + verify_honk_proof(verification_key, proof, public_inputs, vk_hash); + + // If we reach here, the proof is valid + // Enqueue a public function call to update state + self.enqueue_self._increment_public(owner); + } + + #[only_self] + #[external("public")] + fn _increment_public(owner: AztecAddress) { + let current = self.storage.counters.at(owner).read(); + self.storage.counters.at(owner).write(current + 1); + } + + #[view] + #[external("public")] + fn get_counter(owner: AztecAddress) -> Field { + self.storage.counters.at(owner).read() + } +} +// docs:end:full_contract diff --git a/docs/lines/v6/examples/solidity/aave_bridge/AavePortal.sol b/docs/lines/v6/examples/solidity/aave_bridge/AavePortal.sol new file mode 100644 index 00000000000..4ac043cf087 --- /dev/null +++ b/docs/lines/v6/examples/solidity/aave_bridge/AavePortal.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; + +import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; +import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; +import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; +import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; +import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; +import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; +import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; + +// docs:start:portal_setup +interface IAavePool { + function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; + function withdraw(address asset, uint256 amount, address to) external returns (uint256); +} + +contract AavePortal { + using SafeERC20 for IERC20; + + IRegistry public registry; + IERC20 public underlying; + IERC20 public aToken; + IAavePool public aavePool; + bytes32 public l2Bridge; + + IRollup public rollup; + IOutbox public outbox; + IInbox public inbox; + uint256 public rollupVersion; + + bool private _initialized; + + function initialize(address _registry, address _underlying, address _aToken, address _aavePool, bytes32 _l2Bridge) + external + { + require(!_initialized, "Already initialized"); + _initialized = true; + + registry = IRegistry(_registry); + underlying = IERC20(_underlying); + aToken = IERC20(_aToken); + aavePool = IAavePool(_aavePool); + l2Bridge = _l2Bridge; + + rollup = IRollup(address(registry.getCanonicalRollup())); + outbox = rollup.getOutbox(); + inbox = rollup.getInbox(); + rollupVersion = rollup.getVersion(); + } + + // docs:end:portal_setup + + // docs:start:portal_deposit_to_aave + /// @notice Consume an L2->L1 withdraw message and deposit the underlying tokens into Aave + /// @dev The content hash must match what the L2 bridge emits via get_withdraw_content_hash + function depositToAave( + address _recipient, + uint256 _amount, + bool _withCaller, + Epoch _epoch, + uint256 _numCheckpointsInEpoch, + uint256 _leafIndex, + bytes32[] calldata _path + ) external { + // Reconstruct the L2->L1 message (must match the L2 bridge's exit_to_l1_public/private) + DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2Bridge, rollupVersion), + recipient: DataStructures.L1Actor(address(this), block.chainid), + content: Hash.sha256ToField( + abi.encodeWithSignature( + "withdraw(address,uint256,address)", _recipient, _amount, _withCaller ? msg.sender : address(0) + ) + ) + }); + + // Consume the message from the outbox (verifies merkle proof) + outbox.consume(message, _epoch, _numCheckpointsInEpoch, _leafIndex, _path); + + // Deposit into Aave instead of sending tokens to the recipient. + // The portal must already hold the underlying tokens (pre-funded or bridged separately). + underlying.forceApprove(address(aavePool), _amount); + aavePool.supply(address(underlying), _amount, address(this), 0); + } + + // docs:end:portal_deposit_to_aave + + // docs:start:portal_claim_public + /// @notice Withdraw from Aave and send an L1->L2 message to mint tokens publicly on L2 + function claimFromAavePublic(uint256 _aTokenAmount, bytes32 _to, bytes32 _secretHash) + external + returns (bytes32, uint256) + { + // Withdraw from Aave (returns underlying + yield) + aToken.forceApprove(address(aavePool), _aTokenAmount); + uint256 withdrawn = aavePool.withdraw(address(underlying), _aTokenAmount, address(this)); + + // Send L1->L2 message with the total withdrawn amount (including yield) + DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion); + bytes32 contentHash = + Hash.sha256ToField(abi.encodeWithSignature("mint_to_public(bytes32,uint256)", _to, withdrawn)); + + (bytes32 key, uint256 index) = inbox.sendL2Message(actor, contentHash, _secretHash); + return (key, index); + } + + // docs:end:portal_claim_public + + // docs:start:portal_claim_private + /// @notice Withdraw from Aave and send an L1->L2 message to mint tokens privately on L2 + function claimFromAavePrivate(uint256 _aTokenAmount, bytes32 _secretHash) external returns (bytes32, uint256) { + // Withdraw from Aave (returns underlying + yield) + aToken.forceApprove(address(aavePool), _aTokenAmount); + uint256 withdrawn = aavePool.withdraw(address(underlying), _aTokenAmount, address(this)); + + // Send L1->L2 message for private minting + DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion); + bytes32 contentHash = Hash.sha256ToField(abi.encodeWithSignature("mint_to_private(uint256)", withdrawn)); + + (bytes32 key, uint256 index) = inbox.sendL2Message(actor, contentHash, _secretHash); + return (key, index); + } + // docs:end:portal_claim_private +} diff --git a/docs/lines/v6/examples/solidity/aave_bridge/MockAToken.sol b/docs/lines/v6/examples/solidity/aave_bridge/MockAToken.sol new file mode 100644 index 00000000000..9de706f82a3 --- /dev/null +++ b/docs/lines/v6/examples/solidity/aave_bridge/MockAToken.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {ERC20} from "@oz/token/ERC20/ERC20.sol"; + +// docs:start:mock_atoken +contract MockAToken is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external { + _burn(from, amount); + } +} +// docs:end:mock_atoken diff --git a/docs/lines/v6/examples/solidity/aave_bridge/MockAavePool.sol b/docs/lines/v6/examples/solidity/aave_bridge/MockAavePool.sol new file mode 100644 index 00000000000..6ceead7b64b --- /dev/null +++ b/docs/lines/v6/examples/solidity/aave_bridge/MockAavePool.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {MockERC20} from "./MockERC20.sol"; +import {MockAToken} from "./MockAToken.sol"; + +// docs:start:mock_aave_pool +/// @notice A simplified mock of Aave V3's lending pool for tutorial purposes. +/// Supports supply and withdraw with a configurable yield in basis points. +contract MockAavePool { + using SafeERC20 for IERC20; + + MockERC20 public underlyingToken; + MockAToken public aToken; + uint256 public yieldBps; // e.g. 1000 = 10% + + constructor(address _underlyingToken, address _aToken, uint256 _yieldBps) { + underlyingToken = MockERC20(_underlyingToken); + aToken = MockAToken(_aToken); + yieldBps = _yieldBps; + } + + /// @notice Deposit underlying tokens and receive aTokens (mimics Aave V3 IPool.supply) + function supply( + address asset, + uint256 amount, + address onBehalfOf, + uint16 /* referralCode */ + ) external { + require(asset == address(underlyingToken), "Wrong asset"); + IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); + aToken.mint(onBehalfOf, amount); + } + + /// @notice Withdraw underlying tokens by burning aTokens (mimics Aave V3 IPool.withdraw) + /// Returns the original amount plus simulated yield + function withdraw(address asset, uint256 amount, address to) external returns (uint256) { + require(asset == address(underlyingToken), "Wrong asset"); + + // Burn caller's aTokens + aToken.burn(msg.sender, amount); + + // Simulate yield: return amount + yield + uint256 yieldAmount = (amount * yieldBps) / 10000; + uint256 totalReturn = amount + yieldAmount; + + // Mint extra underlying to cover yield (mock-only behavior) + underlyingToken.mint(address(this), yieldAmount); + + // Transfer underlying + yield to recipient + IERC20(address(underlyingToken)).safeTransfer(to, totalReturn); + return totalReturn; + } +} +// docs:end:mock_aave_pool diff --git a/docs/lines/v6/examples/solidity/aave_bridge/MockERC20.sol b/docs/lines/v6/examples/solidity/aave_bridge/MockERC20.sol new file mode 100644 index 00000000000..062b0c5905c --- /dev/null +++ b/docs/lines/v6/examples/solidity/aave_bridge/MockERC20.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {ERC20} from "@oz/token/ERC20/ERC20.sol"; + +// docs:start:mock_erc20 +contract MockERC20 is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} +// docs:end:mock_erc20 diff --git a/docs/lines/v6/examples/solidity/example_swap/ExampleERC20.sol b/docs/lines/v6/examples/solidity/example_swap/ExampleERC20.sol new file mode 100644 index 00000000000..6cbaa356f51 --- /dev/null +++ b/docs/lines/v6/examples/solidity/example_swap/ExampleERC20.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {ERC20} from "@oz/token/ERC20/ERC20.sol"; + +/// @title ExampleERC20 +/// @notice Minimal ERC20 with public mint for testing L1<>L2 swap flows. +contract ExampleERC20 is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + + /// @notice Anyone can mint tokens (test only!) + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/docs/lines/v6/examples/solidity/example_swap/ExampleTokenPortal.sol b/docs/lines/v6/examples/solidity/example_swap/ExampleTokenPortal.sol new file mode 100644 index 00000000000..c6729f81e18 --- /dev/null +++ b/docs/lines/v6/examples/solidity/example_swap/ExampleTokenPortal.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +// docs:start:example_token_portal +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; +import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; +import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; +import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; +import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; +import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; +import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; + +/// @title ExampleTokenPortal +/// @notice Example token portal for tutorial. +contract ExampleTokenPortal { + using SafeERC20 for IERC20; + + IRegistry public registry; + IERC20 public underlying; + bytes32 public l2Bridge; + + IRollup public rollup; + IOutbox public outbox; + IInbox public inbox; + uint256 public rollupVersion; + + /// @dev No access control for simplicity. A production contract should restrict this to the deployer/owner. + function initialize(address _registry, address _underlying, bytes32 _l2Bridge) external { + registry = IRegistry(_registry); + underlying = IERC20(_underlying); + l2Bridge = _l2Bridge; + + rollup = IRollup(address(registry.getCanonicalRollup())); + outbox = rollup.getOutbox(); + inbox = rollup.getInbox(); + rollupVersion = rollup.getVersion(); + } + + // docs:end:example_token_portal + + // docs:start:deposit_to_aztec_public + /// @notice Deposit tokens and send L1->L2 message for public minting on Aztec + function depositToAztecPublic(bytes32 _to, uint256 _amount, bytes32 _secretHash) + external + returns (bytes32, uint256) + { + DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion); + + bytes32 contentHash = + Hash.sha256ToField(abi.encodeWithSignature("mint_to_public(bytes32,uint256)", _to, _amount)); + + underlying.safeTransferFrom(msg.sender, address(this), _amount); + + return inbox.sendL2Message(actor, contentHash, _secretHash); + } + + // docs:end:deposit_to_aztec_public + + /// @notice Deposit tokens and send L1->L2 message for private minting on Aztec + function depositToAztecPrivate(uint256 _amount, bytes32 _secretHash) external returns (bytes32, uint256) { + DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion); + + bytes32 contentHash = Hash.sha256ToField(abi.encodeWithSignature("mint_to_private(uint256)", _amount)); + + underlying.safeTransferFrom(msg.sender, address(this), _amount); + + return inbox.sendL2Message(actor, contentHash, _secretHash); + } + + // docs:start:withdraw + /// @notice Withdraw tokens after consuming an L2->L1 message. + /// @param _numCheckpointsInEpoch The partial-proof depth (1-indexed) the witness was built against. + function withdraw( + address _recipient, + uint256 _amount, + Epoch _epoch, + uint256 _numCheckpointsInEpoch, + uint256 _leafIndex, + bytes32[] calldata _path + ) external { + DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2Bridge, rollupVersion), + recipient: DataStructures.L1Actor(address(this), block.chainid), + content: Hash.sha256ToField( + abi.encodeWithSignature("withdraw(address,uint256,address)", _recipient, _amount, msg.sender) + ) + }); + + outbox.consume(message, _epoch, _numCheckpointsInEpoch, _leafIndex, _path); + + underlying.safeTransfer(_recipient, _amount); + } + // docs:end:withdraw +} diff --git a/docs/lines/v6/examples/solidity/example_swap/ExampleUniswapPortal.sol b/docs/lines/v6/examples/solidity/example_swap/ExampleUniswapPortal.sol new file mode 100644 index 00000000000..b9135ce366a --- /dev/null +++ b/docs/lines/v6/examples/solidity/example_swap/ExampleUniswapPortal.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +// docs:start:example_uniswap_portal +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; +import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; +import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; +import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; +import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; +import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; +import {ExampleTokenPortal} from "./ExampleTokenPortal.sol"; + +/// @title ExampleUniswapPortal +/// @notice Example swap portal for tutorial. Instead of using a real Uniswap V3 router, +/// performs a mock 1:1 swap by transferring pre-funded output tokens. +/// Still demonstrates the core pattern: consuming 2 L2->L1 messages per swap. +contract ExampleUniswapPortal { + using SafeERC20 for IERC20; + + IRegistry public registry; + bytes32 public l2UniswapAddress; + IRollup public rollup; + IOutbox public outbox; + uint256 public rollupVersion; + + function initialize(address _registry, bytes32 _l2UniswapAddress) external { + registry = IRegistry(_registry); + l2UniswapAddress = _l2UniswapAddress; + + rollup = IRollup(address(registry.getCanonicalRollup())); + outbox = rollup.getOutbox(); + rollupVersion = rollup.getVersion(); + } + + // docs:end:example_uniswap_portal + + // docs:start:swap_public + /// @notice Execute a public swap: consume 2 L2->L1 messages, mock-swap, deposit output to L2 + /// @dev Message 1: TokenBridge exit (withdraw input tokens to this contract) + /// Message 2: Uniswap swap intent (proves the user authorized this exact swap) + function swapPublic( + address _inputTokenPortal, + uint256 _inAmount, + uint24 _uniswapFeeTier, + address _outputTokenPortal, + uint256 _amountOutMinimum, + bytes32 _aztecRecipient, + bytes32 _secretHashForL1ToL2Message, + // Outbox message metadata for the two L2->L1 messages + Epoch[2] calldata _epochs, + uint256[2] calldata _numCheckpointsInEpochs, + uint256[2] calldata _leafIndices, + bytes32[][2] calldata _paths + ) external returns (bytes32, uint256) { + IERC20 outputAsset = ExampleTokenPortal(_outputTokenPortal).underlying(); + + // Message 1: Consume the token bridge exit message (withdraw input tokens) + ExampleTokenPortal(_inputTokenPortal) + .withdraw(address(this), _inAmount, _epochs[0], _numCheckpointsInEpochs[0], _leafIndices[0], _paths[0]); + + // Message 2: Consume the uniswap swap intent message + bytes32 contentHash = Hash.sha256ToField( + abi.encodeWithSignature( + "swap_public(address,uint256,uint24,address,uint256,bytes32,bytes32)", + _inputTokenPortal, + _inAmount, + _uniswapFeeTier, + _outputTokenPortal, + _amountOutMinimum, + _aztecRecipient, + _secretHashForL1ToL2Message + ) + ); + + outbox.consume( + DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2UniswapAddress, rollupVersion), + recipient: DataStructures.L1Actor(address(this), block.chainid), + content: contentHash + }), + _epochs[1], + _numCheckpointsInEpochs[1], + _leafIndices[1], + _paths[1] + ); + + // Mock swap: 1:1 transfer (this contract must be pre-funded with output tokens) + uint256 amountOut = _inAmount; + require(amountOut >= _amountOutMinimum, "Insufficient output amount"); + + // Approve output token portal and deposit back to Aztec + outputAsset.forceApprove(_outputTokenPortal, amountOut); + return ExampleTokenPortal(_outputTokenPortal) + .depositToAztecPublic(_aztecRecipient, amountOut, _secretHashForL1ToL2Message); + } + + // docs:end:swap_public + + // docs:start:swap_private + /// @notice Execute a private swap: same pattern but deposits output privately + function swapPrivate( + address _inputTokenPortal, + uint256 _inAmount, + uint24 _uniswapFeeTier, + address _outputTokenPortal, + uint256 _amountOutMinimum, + bytes32 _secretHashForL1ToL2Message, + // Outbox message metadata for the two L2->L1 messages + Epoch[2] calldata _epochs, + uint256[2] calldata _numCheckpointsInEpochs, + uint256[2] calldata _leafIndices, + bytes32[][2] calldata _paths + ) external returns (bytes32, uint256) { + IERC20 outputAsset = ExampleTokenPortal(_outputTokenPortal).underlying(); + + // Message 1: Consume the token bridge exit message (withdraw input tokens) + ExampleTokenPortal(_inputTokenPortal) + .withdraw(address(this), _inAmount, _epochs[0], _numCheckpointsInEpochs[0], _leafIndices[0], _paths[0]); + + // Message 2: Consume the uniswap swap intent message + bytes32 contentHash = Hash.sha256ToField( + abi.encodeWithSignature( + "swap_private(address,uint256,uint24,address,uint256,bytes32)", + _inputTokenPortal, + _inAmount, + _uniswapFeeTier, + _outputTokenPortal, + _amountOutMinimum, + _secretHashForL1ToL2Message + ) + ); + + outbox.consume( + DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2UniswapAddress, rollupVersion), + recipient: DataStructures.L1Actor(address(this), block.chainid), + content: contentHash + }), + _epochs[1], + _numCheckpointsInEpochs[1], + _leafIndices[1], + _paths[1] + ); + + // Mock swap: 1:1 transfer + uint256 amountOut = _inAmount; + require(amountOut >= _amountOutMinimum, "Insufficient output amount"); + + // Approve output token portal and deposit back to Aztec privately + outputAsset.forceApprove(_outputTokenPortal, amountOut); + return ExampleTokenPortal(_outputTokenPortal).depositToAztecPrivate(amountOut, _secretHashForL1ToL2Message); + } + // docs:end:swap_private +} diff --git a/docs/lines/v6/examples/solidity/foundry.toml b/docs/lines/v6/examples/solidity/foundry.toml new file mode 100644 index 00000000000..4fc8f09b070 --- /dev/null +++ b/docs/lines/v6/examples/solidity/foundry.toml @@ -0,0 +1,16 @@ +[profile.default] +src = '.' +out = 'out' +libs = [ + '../../node_modules/@aztec-foundation/l1-artifacts/l1-contracts/lib', + '../../node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src', +] +solc = "0.8.30" +evm_version = 'prague' +optimizer = true + +remappings = [ + "@oz/=../../node_modules/@aztec-foundation/l1-artifacts/l1-contracts/lib/openzeppelin-contracts/contracts/", + "@aztec/=../../node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/", + "@aztec-blob-lib/=../../node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/rollup/" +] diff --git a/docs/lines/v6/examples/solidity/nft_bridge/NFTPortal.sol b/docs/lines/v6/examples/solidity/nft_bridge/NFTPortal.sol new file mode 100644 index 00000000000..8da7aa5742b --- /dev/null +++ b/docs/lines/v6/examples/solidity/nft_bridge/NFTPortal.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +// docs:start:portal_setup +import {IERC721} from "@oz/token/ERC721/IERC721.sol"; +import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; +import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; +import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; +import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; +import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; +import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; +import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; + +contract NFTPortal { + IRegistry public registry; + IERC721 public nftContract; + bytes32 public l2Bridge; + + IRollup public rollup; + IOutbox public outbox; + IInbox public inbox; + uint256 public rollupVersion; + + function initialize(address _registry, address _nftContract, bytes32 _l2Bridge) external { + registry = IRegistry(_registry); + nftContract = IERC721(_nftContract); + l2Bridge = _l2Bridge; + + rollup = IRollup(address(registry.getCanonicalRollup())); + outbox = rollup.getOutbox(); + inbox = rollup.getInbox(); + rollupVersion = rollup.getVersion(); + } + + // docs:end:portal_setup + + // docs:start:portal_deposit_and_withdraw + // Lock NFT and send message to L2 + function depositToAztec(uint256 tokenId, bytes32 secretHash) external returns (bytes32, uint256) { + // Lock the NFT + nftContract.transferFrom(msg.sender, address(this), tokenId); + + // Prepare L2 message - just a naive hash of our tokenId + DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion); + bytes32 contentHash = Hash.sha256ToField(abi.encode(tokenId)); + + // Send message to Aztec + (bytes32 key, uint256 index) = inbox.sendL2Message(actor, contentHash, secretHash); + return (key, index); + } + + // Unlock NFT after L2 burn + function withdraw( + uint256 tokenId, + Epoch epoch, + uint256 numCheckpointsInEpoch, + uint256 leafIndex, + bytes32[] calldata path + ) external { + // Verify message from L2 + DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2Bridge, rollupVersion), + recipient: DataStructures.L1Actor(address(this), block.chainid), + content: Hash.sha256ToField(abi.encodePacked(tokenId, msg.sender)) + }); + + outbox.consume(message, epoch, numCheckpointsInEpoch, leafIndex, path); + + // Unlock NFT + nftContract.transferFrom(address(this), msg.sender, tokenId); + } + // docs:end:portal_deposit_and_withdraw +} diff --git a/docs/lines/v6/examples/solidity/nft_bridge/SimpleNFT.sol b/docs/lines/v6/examples/solidity/nft_bridge/SimpleNFT.sol new file mode 100644 index 00000000000..254143bfdd1 --- /dev/null +++ b/docs/lines/v6/examples/solidity/nft_bridge/SimpleNFT.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// docs:start:simple_nft +pragma solidity >=0.8.27; + +import {ERC721} from "@oz/token/ERC721/ERC721.sol"; + +contract SimpleNFT is ERC721 { + uint256 private _currentTokenId; + + constructor() ERC721("SimplePunk", "SPUNK") {} + + function mint(address to) external returns (uint256) { + uint256 tokenId = _currentTokenId++; + _safeMint(to, tokenId); + return tokenId; + } +} +// docs:end:simple_nft diff --git a/docs/lines/v6/examples/ts/.gitignore b/docs/lines/v6/examples/ts/.gitignore new file mode 100644 index 00000000000..d02881e663e --- /dev/null +++ b/docs/lines/v6/examples/ts/.gitignore @@ -0,0 +1,12 @@ +# Generated by bootstrap validation (yarn init, yarn add, tsc, codegen). +# Cleanup runs via trap but may not fire if bootstrap is interrupted or parallel kills jobs. +*/.editorconfig +*/.gitattributes +*/.gitignore +*/.yarnrc.yml +*/package.json +*/README.md +*/tsconfig.json +*/codegenCache.json +*/artifacts/ +joblog.txt diff --git a/docs/lines/v6/examples/ts/aave_bridge/config.yaml b/docs/lines/v6/examples/ts/aave_bridge/config.yaml new file mode 100644 index 00000000000..1cfe3ef00db --- /dev/null +++ b/docs/lines/v6/examples/ts/aave_bridge/config.yaml @@ -0,0 +1,12 @@ +contracts: + - aave_bridge-AaveBridge + +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/stdlib" + - "@aztec-labs/foundation" + - "@aztec-labs/ethereum" + - "@aztec-labs/noir-contracts.js" + - "npm:@aztec/viem@2.38.2" diff --git a/docs/lines/v6/examples/ts/aave_bridge/index.ts b/docs/lines/v6/examples/ts/aave_bridge/index.ts new file mode 100644 index 00000000000..a1d640cffeb --- /dev/null +++ b/docs/lines/v6/examples/ts/aave_bridge/index.ts @@ -0,0 +1,445 @@ +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; +import { AztecAddress, EthAddress } from "@aztec-labs/aztec.js/addresses"; +import { SetPublicAuthwitContractInteraction } from "@aztec-labs/aztec.js/authorization"; +import { Fr } from "@aztec-labs/aztec.js/fields"; +import { createAztecNodeClient, waitForNode } from "@aztec-labs/aztec.js/node"; +import { createExtendedL1Client } from "@aztec-labs/ethereum/client"; +import { deployL1Contract } from "@aztec-labs/ethereum/deploy-l1-contract"; +import { sha256ToField } from "@aztec-labs/foundation/crypto/sha256"; +import { + computeL2ToL1MessageHash, + computeSecretHash, +} from "@aztec-labs/stdlib/hash"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { decodeEventLog, pad, toFunctionSelector } from "@aztec/viem"; +import { foundry } from "@aztec/viem/chains"; +import AavePortal from "../../../target/solidity/aave_bridge/AavePortal.sol/AavePortal.json" with { type: "json" }; +import MockERC20 from "../../../target/solidity/aave_bridge/MockERC20.sol/MockERC20.json" with { type: "json" }; +import MockAToken from "../../../target/solidity/aave_bridge/MockAToken.sol/MockAToken.json" with { type: "json" }; +import MockAavePool from "../../../target/solidity/aave_bridge/MockAavePool.sol/MockAavePool.json" with { type: "json" }; +import { TokenContract } from "@aztec-labs/noir-contracts.js/Token"; +import { AaveBridgeContract } from "./artifacts/AaveBridge.js"; + +// docs:start:setup +// Setup L1 client using anvil's default mnemonic +const MNEMONIC = "test test test test test test test test test test test junk"; +const l1Client = createExtendedL1Client( + [process.env.ETHEREUM_HOST ?? "http://localhost:8545"], + MNEMONIC, +); + +// Setup L2 using Aztec's local network +console.log("Setting up L2...\n"); +const node = createAztecNodeClient( + process.env.AZTEC_NODE_URL ?? "http://localhost:8080", +); +await waitForNode(node); +const aztecWallet = await EmbeddedWallet.create(node, { ephemeral: true }); +const [accData] = await getInitialTestAccountsData(); +const account = await aztecWallet.createSchnorrInitializerlessAccount( + accData.secret, + accData.salt, + accData.signingKey, +); +console.log(`Account: ${account.address.toString()}\n`); + +// Get node info +const nodeInfo = await node.getNodeInfo(); +const registryAddress = nodeInfo.l1ContractAddresses.registryAddress.toString(); +const inboxAddress = nodeInfo.l1ContractAddresses.inboxAddress.toString(); +// docs:end:setup + +// docs:start:deploy_l1 +console.log("Deploying L1 contracts...\n"); + +// Deploy MockERC20 (underlying token, e.g. DAI) +const { address: underlyingAddress } = await deployL1Contract( + l1Client, + MockERC20.abi, + MockERC20.bytecode.object as `0x${string}`, + ["Mock DAI", "mDAI"], +); + +// Deploy MockAToken (Aave's yield-bearing token) +const { address: aTokenAddress } = await deployL1Contract( + l1Client, + MockAToken.abi, + MockAToken.bytecode.object as `0x${string}`, + ["Aave Mock DAI", "amDAI"], +); + +// Deploy MockAavePool with 10% yield (1000 basis points) +const { address: poolAddress } = await deployL1Contract( + l1Client, + MockAavePool.abi, + MockAavePool.bytecode.object as `0x${string}`, + [underlyingAddress.toString(), aTokenAddress.toString(), 1000n], +); + +// Deploy AavePortal +const { address: portalAddress } = await deployL1Contract( + l1Client, + AavePortal.abi, + AavePortal.bytecode.object as `0x${string}`, +); + +console.log(`MockERC20 (DAI): ${underlyingAddress}`); +console.log(`MockAToken (aDAI): ${aTokenAddress}`); +console.log(`MockAavePool: ${poolAddress}`); +console.log(`AavePortal: ${portalAddress}\n`); +// docs:end:deploy_l1 + +// docs:start:deploy_l2 +console.log("Deploying L2 contracts...\n"); + +// Deploy the Token contract on L2 (this is the standard Aztec token) +const { contract: l2Token } = await TokenContract.deploy( + aztecWallet, + account.address, // admin + "Bridged DAI", + "bDAI", + 18, +).send({ from: account.address }); + +// Deploy the AaveBridge on L2 +const { contract: l2Bridge } = await AaveBridgeContract.deploy( + aztecWallet, + l2Token.address, + EthAddress.fromString(portalAddress.toString()), +).send({ from: account.address }); + +console.log(`L2 Token: ${l2Token.address.toString()}`); +console.log(`L2 Bridge: ${l2Bridge.address.toString()}\n`); +// docs:end:deploy_l2 + +// docs:start:initialize +console.log("Initializing contracts..."); + +// Initialize the L1 portal +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const initHash = await l1Client.writeContract({ + address: portalAddress.toString() as `0x${string}`, + abi: AavePortal.abi, + functionName: "initialize", + args: [ + registryAddress, + underlyingAddress.toString(), + aTokenAddress.toString(), + poolAddress.toString(), + l2Bridge.address.toString(), + ], +}); +await l1Client.waitForTransactionReceipt({ hash: initHash }); + +// Set the bridge as a minter on the L2 token so it can mint when claiming +await l2Token.methods + .set_minter(l2Bridge.address, true) + .send({ from: account.address }); + +console.log("All contracts initialized\n"); +// docs:end:initialize + +// docs:start:fund_user +// Pre-fund the portal with L1 tokens and mint L2 tokens to the user +// In a real scenario, tokens would already exist on L2 from a prior bridge +console.log("Funding user with tokens on L2..."); + +const depositAmount = 1000n * 10n ** 18n; // 1000 DAI + +// Mint underlying tokens on L1 +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const mintHash = await l1Client.writeContract({ + address: underlyingAddress.toString() as `0x${string}`, + abi: MockERC20.abi, + functionName: "mint", + args: [portalAddress.toString(), depositAmount], +}); +await l1Client.waitForTransactionReceipt({ hash: mintHash }); + +// Also mint tokens directly to the user on L2 (admin mints for simplicity) +await l2Token.methods + .mint_to_public(account.address, depositAmount) + .send({ from: account.address }); + +console.log(`User funded with ${depositAmount / 10n ** 18n} tokens on L2\n`); +// docs:end:fund_user + +// docs:start:mine_blocks +// On the local network, L2 blocks are only produced when transactions are submitted. +// L1-to-L2 messages require 2 L2 blocks before they can be consumed, so we deploy +// two dummy contracts (with random salts for unique addresses) to force block production. +async function mine2Blocks( + aztecWallet: EmbeddedWallet, + accountAddress: AztecAddress, +) { + await AaveBridgeContract.deploy( + aztecWallet, + accountAddress, + EthAddress.ZERO, + ).send({ + from: accountAddress, + }); + await AaveBridgeContract.deploy( + aztecWallet, + accountAddress, + EthAddress.ZERO, + ).send({ + from: accountAddress, + }); +} +// docs:end:mine_blocks + +// docs:start:deposit_to_aave +// ============================================================ +// STEP 1: Deposit to Aave (L2 -> L1 flow) +// ============================================================ +console.log("=== Depositing to Aave ===\n"); + +const amountToDeposit = 500n * 10n ** 18n; // 500 DAI + +// Create authwit for the bridge to burn tokens on our behalf. +// The bridge calls Token::burn_public(user, amount, nonce), where msg_sender +// is the bridge, so the token contract requires a public authwit. +const burnNonce = Fr.random(); +const burnAuthwit = await SetPublicAuthwitContractInteraction.create( + aztecWallet, + account.address, + { + caller: l2Bridge.address, + action: l2Token.methods.burn_public( + account.address, + amountToDeposit, + burnNonce, + ), + }, + true, +); +await burnAuthwit.send(); + +// On L2: burn tokens and send L2->L1 message. +// exit_to_l1_public sends tokens to the portal as the L1 recipient, +// and caller_on_l1 is set to ZERO so anyone can relay the message. +const { receipt: exitReceipt } = await l2Bridge.methods + .exit_to_l1_public( + EthAddress.fromString(portalAddress.toString()), // recipient on L1 (the portal itself) + amountToDeposit, + EthAddress.ZERO, // caller_on_l1: anyone can relay + burnNonce, // authwit nonce authorizing the bridge to burn on our behalf + ) + .send({ from: account.address }); + +console.log(`Exit sent (block: ${exitReceipt.blockNumber})`); +// docs:end:deposit_to_aave + +// docs:start:get_deposit_witness +// Compute the L2->L1 content hash for the withdrawal witness. +// This must match what the L1 portal reconstructs via abi.encodeWithSignature. +// toFunctionSelector computes keccak256 of the signature and takes the first 4 bytes. +const portalEthAddress = EthAddress.fromString(portalAddress.toString()); +const withdrawContent = sha256ToField([ + Buffer.from( + toFunctionSelector("withdraw(address,uint256,address)").substring(2), + "hex", + ), + portalEthAddress.toBuffer32(), + new Fr(amountToDeposit).toBuffer(), + EthAddress.ZERO.toBuffer32(), +]); + +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const version = (await l1Client.readContract({ + address: portalAddress.toString() as `0x${string}`, + abi: AavePortal.abi, + functionName: "rollupVersion", +})) as bigint; + +const msgLeaf = computeL2ToL1MessageHash({ + l2Sender: l2Bridge.address, + l1Recipient: portalEthAddress, + content: withdrawContent, + rollupVersion: new Fr(version), + chainId: new Fr(foundry.id), +}); + +// Wait for the block to be proven +if (!exitReceipt.blockNumber) { + throw new Error("Exit transaction was not included in a block"); +} +const exitBlockNumber = exitReceipt.blockNumber; +console.log("Waiting for block to be proven..."); +let provenBlockNumber = await node.getBlockNumber("proven"); +while (provenBlockNumber < exitBlockNumber) { + console.log( + ` Waiting... (proven: ${provenBlockNumber}, needed: ${exitBlockNumber})`, + ); + await new Promise((resolve) => setTimeout(resolve, 10000)); + provenBlockNumber = await node.getBlockNumber("proven"); +} +console.log("Block proven!\n"); + +// Compute the membership witness using the message hash and the L2 tx hash. +// The node picks the smallest partial-proof root that covers the tx's checkpoint. +const witness = await node.getL2ToL1MembershipWitness( + exitReceipt.txHash, + msgLeaf, +); +const epoch = witness!.epochNumber; +const numCheckpointsInEpoch = witness!.numCheckpointsInEpoch; + +const siblingPathHex = witness!.siblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString("hex")}` as `0x${string}`); +// docs:end:get_deposit_witness + +// docs:start:execute_deposit_l1 +// On L1: consume the outbox message and deposit into Aave +console.log("Depositing into Aave on L1..."); +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const depositToAaveHash = await l1Client.writeContract({ + address: portalAddress.toString() as `0x${string}`, + abi: AavePortal.abi, + functionName: "depositToAave", + args: [ + portalAddress.toString(), // recipient (matches L2 exit) + amountToDeposit, + false, // withCaller = false (matches caller_on_l1 = address(0)) + BigInt(epoch), + BigInt(numCheckpointsInEpoch), + BigInt(witness!.leafIndex), + siblingPathHex, + ], +}); +await l1Client.waitForTransactionReceipt({ hash: depositToAaveHash }); +console.log("Tokens deposited into Aave!\n"); +// docs:end:execute_deposit_l1 + +// docs:start:claim_from_aave_l1 +// ============================================================ +// STEP 2: Claim from Aave with yield (L1 -> L2 flow) +// ============================================================ +console.log("=== Claiming from Aave (with yield) ===\n"); + +const secret = Fr.random(); +const secretHash = await computeSecretHash(secret); + +// On L1: withdraw from Aave and send L1->L2 message +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const claimHash = await l1Client.writeContract({ + address: portalAddress.toString() as `0x${string}`, + abi: AavePortal.abi, + functionName: "claimFromAavePublic", + args: [ + amountToDeposit, // aToken amount to withdraw + pad(account.address.toString() as `0x${string}`, { dir: "left", size: 32 }), // L2 recipient + pad(secretHash.toString() as `0x${string}`, { dir: "left", size: 32 }), + ], +}); +const claimReceipt = await l1Client.waitForTransactionReceipt({ + hash: claimHash, +}); +console.log("Aave withdrawal complete, L1->L2 message sent"); +// docs:end:claim_from_aave_l1 + +// docs:start:get_claim_leaf_index +// Extract the message leaf index from the MessageSent event +const INBOX_ABI = [ + { + type: "event", + name: "MessageSent", + inputs: [ + { name: "hash", type: "bytes32", indexed: true }, + { name: "inboxRollingHash", type: "bytes32", indexed: false }, + { name: "bucketSeq", type: "uint256", indexed: false }, + { + name: "message", + type: "tuple", + indexed: false, + components: [ + { + name: "sender", + type: "tuple", + components: [ + { name: "actor", type: "address" }, + { name: "chainId", type: "uint256" }, + ], + }, + { + name: "recipient", + type: "tuple", + components: [ + { name: "actor", type: "bytes32" }, + { name: "version", type: "uint256" }, + ], + }, + { name: "content", type: "bytes32" }, + { name: "secretHash", type: "bytes32" }, + { name: "index", type: "uint256" }, + ], + }, + ], + }, +] as const; + +const messageSentLogs = claimReceipt.logs + .filter((log) => log.address.toLowerCase() === inboxAddress.toLowerCase()) + .map((log: any) => { + try { + const decoded = decodeEventLog({ + abi: INBOX_ABI, + data: log.data, + topics: log.topics, + }); + return { log, decoded }; + } catch { + return null; + } + }) + .filter( + (item): item is { log: any; decoded: any } => + item !== null && (item.decoded as any).eventName === "MessageSent", + ); + +const messageLeafIndex = new Fr(messageSentLogs[0].decoded.args.message.index); +console.log(`Message leaf index: ${messageLeafIndex}\n`); +// docs:end:get_claim_leaf_index + +// docs:start:claim_on_l2 +// Mine blocks so the L1->L2 message is available +await mine2Blocks(aztecWallet, account.address); + +// The mock Aave pool returns 10% yield, so 500 DAI becomes 550 DAI +const expectedWithYield = amountToDeposit + (amountToDeposit * 1000n) / 10000n; +console.log( + `Expected amount with yield: ${expectedWithYield / 10n ** 18n} tokens`, +); + +// On L2: consume the L1->L2 message and mint tokens (with yield) +console.log("Claiming tokens on L2..."); +await l2Bridge.methods + .claim_public(account.address, expectedWithYield, secret, messageLeafIndex) + .send({ from: account.address }); +console.log("Tokens claimed on L2!\n"); +// docs:end:claim_on_l2 + +// docs:start:verify +// Verify the user's balance includes yield +console.log("=== Verifying balances ===\n"); + +const { result: finalBalance } = await l2Token.methods + .balance_of_public(account.address) + .simulate({ from: account.address }); + +const initialRemaining = depositAmount - amountToDeposit; // 500 DAI not deposited +const expectedFinal = initialRemaining + expectedWithYield; // 500 + 550 = 1050 DAI + +console.log(`Initial deposit: ${depositAmount / 10n ** 18n} tokens`); +console.log(`Deposited to Aave: ${amountToDeposit / 10n ** 18n} tokens`); +console.log( + `Yield earned (10%): ${(expectedWithYield - amountToDeposit) / 10n ** 18n} tokens`, +); +console.log(`Expected balance: ${expectedFinal / 10n ** 18n} tokens`); +console.log(`Actual balance: ${finalBalance / 10n ** 18n} tokens`); +console.log( + `\nYield earned successfully: ${finalBalance >= expectedFinal ? "YES" : "NO"}`, +); +// docs:end:verify diff --git a/docs/lines/v6/examples/ts/aave_bridge/yarn.lock b/docs/lines/v6/examples/ts/aave_bridge/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/aztecjs_advanced/config.yaml b/docs/lines/v6/examples/ts/aztecjs_advanced/config.yaml new file mode 100644 index 00000000000..03840ddaa46 --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_advanced/config.yaml @@ -0,0 +1,16 @@ +# Configuration for aztecjs_advanced example +# Demonstrates advanced patterns: BatchCall, NO_WAIT, L1 bridge, contract reconstruction + +# No custom contracts needed - using pre-built contracts from @aztec-labs/noir-contracts.js +contracts: [] + +# Dependencies: +# - @aztec-labs/* and @aztec-foundation/* packages install at the versions docs/aztec-node.json pins +# - Use npm:package-name for external npm packages +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/noir-contracts.js" + - "@aztec-labs/ethereum" + - "@aztec-labs/stdlib" diff --git a/docs/lines/v6/examples/ts/aztecjs_advanced/index.ts b/docs/lines/v6/examples/ts/aztecjs_advanced/index.ts new file mode 100644 index 00000000000..496e80717cb --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_advanced/index.ts @@ -0,0 +1,499 @@ +import { createAztecNodeClient, waitForNode } from "@aztec-labs/aztec.js/node"; +// docs:start:no_wait_deploy_imports +import { NO_WAIT } from "@aztec-labs/aztec.js/contracts"; +import { waitForTx } from "@aztec-labs/aztec.js/node"; +// docs:end:no_wait_deploy_imports +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; +import { TokenContract, type Transfer } from "@aztec-labs/noir-contracts.js/Token"; +import { SponsoredFPCContract } from "@aztec-labs/noir-contracts.js/SponsoredFPC"; +import { Fr } from "@aztec-labs/aztec.js/fields"; +import { BatchCall, Contract } from "@aztec-labs/aztec.js/contracts"; +import { SponsoredFeePaymentMethod } from "@aztec-labs/aztec.js/fee/testing"; +import { getContractInstanceFromInstantiationParams } from "@aztec-labs/stdlib/contract"; +import { PublicKeys } from "@aztec-labs/stdlib/keys"; +import { getPublicEvents } from "@aztec-labs/aztec.js/events"; +import { Gas, GasSettings } from "@aztec-labs/stdlib/gas"; + +// Setup: connect to network +const node = createAztecNodeClient( + process.env.AZTEC_NODE_URL ?? "http://localhost:8080", +); +await waitForNode(node); +const wallet = await EmbeddedWallet.create(node, { ephemeral: true }); + +const testAccounts = await getInitialTestAccountsData(); +const [aliceAddress, bobAddress] = await Promise.all( + testAccounts.slice(0, 2).map(async (account) => { + return ( + await wallet.createSchnorrInitializerlessAccount( + account.secret, + account.salt, + account.signingKey, + ) + ).address; + }), +); + +// docs:start:deploy_basic_local +// wallet and aliceAddress are from the connection guide +// Deploy with constructor arguments +const { contract: token } = await TokenContract.deploy( + wallet, + aliceAddress, + "TestToken", + "TST", + 18, +).send({ from: aliceAddress }); // alice has fee juice and is registered in the wallet +// docs:end:deploy_basic_local + +// docs:start:deploy_sponsored_fpc_contract +// Set up the Sponsored FPC (see fees guide for full setup) +const sponsoredFPCInstance = await getContractInstanceFromInstantiationParams( + SponsoredFPCContract.artifact, + { salt: new Fr(0) }, +); +await wallet.registerContract( + sponsoredFPCInstance, + SponsoredFPCContract.artifact, +); +const sponsoredPaymentMethod = new SponsoredFeePaymentMethod( + sponsoredFPCInstance.address, +); + +// wallet is from the connection guide; sponsoredPaymentMethod is from the fees guide +const { contract: sponsoredContract } = await TokenContract.deploy( + wallet, + aliceAddress, + "SponsoredToken", + "SPT", + 18, +).send({ from: aliceAddress, fee: { paymentMethod: sponsoredPaymentMethod } }); +// docs:end:deploy_sponsored_fpc_contract + +// docs:start:deploy_custom_salt +// wallet and aliceAddress are from the connection guide +const customSalt = Fr.random(); + +const { contract: saltedContract } = await TokenContract.deploy( + wallet, + aliceAddress, + "SaltedToken", + "SALT", + 18, + { salt: customSalt }, +).send({ + from: aliceAddress, +}); +// docs:end:deploy_custom_salt + +// docs:start:calculate_address_before_deploy +// Calculate address without deploying +// wallet is from the connection guide (see prerequisites) +const deploymentSalt = Fr.random(); +const deployMethod = TokenContract.deploy( + wallet, + aliceAddress, + "PredictedToken", + "PRED", + 18, + { salt: deploymentSalt, deployer: aliceAddress }, +); +const instance = await deployMethod.getInstance(); +const predictedAddress = instance.address; + +console.log(`Contract will deploy at: ${predictedAddress}`); +// docs:end:calculate_address_before_deploy + +// docs:start:verify_contract_callable +// token is from the deployment step above; aliceAddress is from the connection guide +try { + // Try calling a view function + const { result: balance } = await token.methods + .balance_of_public(aliceAddress) + .simulate({ from: aliceAddress }); + console.log("Contract is callable, balance:", balance); +} catch (error) { + console.error("Contract not accessible:", (error as Error).message); +} +// docs:end:verify_contract_callable + +// docs:start:register_external_contract +// wallet is from the connection guide; contractAddress is the address of the deployed contract +const contractAddress = token.address; + +// Get the contract metadata from the node (includes the instance) +const metadata = await wallet.getContractMetadata(contractAddress); + +// Register the contract with the wallet +// The registerContract method takes positional parameters: +// - instance: ContractInstanceWithAddress (required) +// - artifact: ContractArtifact (optional) +// - secretKey: Fr (optional) +await wallet.registerContract(metadata.instance!, TokenContract.artifact); + +// Now you can interact with the contract +const externalContract = await TokenContract.at(contractAddress, wallet); +// docs:end:register_external_contract + +await token.methods + .mint_to_public(aliceAddress, 10000n) + .send({ from: aliceAddress }); + +await token.methods + .mint_to_private(aliceAddress, 10000n) + .send({ from: aliceAddress }); + +// docs:start:no_wait_deploy +// Use NO_WAIT to get the transaction hash immediately and track deployment +const { txHash } = await TokenContract.deploy( + wallet, + aliceAddress, + "AnotherToken", + "ATK", + 18, +).send({ + from: aliceAddress, + wait: NO_WAIT, +}); + +console.log(`Deployment tx: ${txHash}`); + +// Wait for the transaction to be mined using the node +const receipt = await waitForTx(node, txHash); +console.log(`Deployed in block ${receipt.blockNumber}`); +// docs:end:no_wait_deploy + +// docs:start:no_wait_transaction +// Use NO_WAIT for regular transactions too +const { txHash: transferTxHash } = await token.methods + .transfer(bobAddress, 100n) + .send({ from: aliceAddress, wait: NO_WAIT }); + +console.log(`Transaction sent: ${transferTxHash.toString()}`); + +// Wait for inclusion later using the node +const transferReceipt = await waitForTx(node, transferTxHash); +console.log(`Transaction mined in block ${transferReceipt.blockNumber}`); +// docs:end:no_wait_transaction + +// docs:start:batch_call +// Execute multiple calls atomically using BatchCall +const batch = new BatchCall(wallet, [ + token.methods.mint_to_public(aliceAddress, 500n), + token.methods.transfer(bobAddress, 200n), +]); + +const { receipt: batchReceipt } = await batch.send({ from: aliceAddress }); +console.log(`Batch executed in block ${batchReceipt.blockNumber}`); +// docs:end:batch_call + +// docs:start:reconstruct_contract_instance +// Reconstruct a contract instance from deployment parameters +// Use this when you need to register a contract deployed by someone else +const reconstructedInstance = await getContractInstanceFromInstantiationParams( + TokenContract.artifact, + { + publicKeys: PublicKeys.default(), + constructorArtifact: "constructor", + constructorArgs: [aliceAddress, "ReconstructedToken", "RTK", 18], + deployer: aliceAddress, + salt: new Fr(12345), // The original deployment salt + }, +); + +// Register the reconstructed contract with the wallet +await wallet.registerContract(reconstructedInstance, TokenContract.artifact); +console.log( + `Reconstructed contract address: ${reconstructedInstance.address.toString()}`, +); +// docs:end:reconstruct_contract_instance + +// docs:start:query_tx_status +// Query transaction status after sending without waiting +const { txHash: statusTxHash } = await token.methods + .transfer(bobAddress, 10n) + .send({ from: aliceAddress, wait: NO_WAIT }); + +// Check status using the node +const txReceipt = await node.getTxReceipt(statusTxHash); + +console.log(`Status: ${txReceipt.status}`); +console.log(`Block number: ${txReceipt.blockNumber}`); +console.log(`Transaction fee: ${txReceipt.transactionFee}`); +// docs:end:query_tx_status + +// docs:start:deploy_with_dependencies +// Deploy contracts with dependencies - deploy sequentially and pass addresses +const { contract: baseToken } = await TokenContract.deploy( + wallet, + aliceAddress, + "BaseToken", + "BASE", + 18, +).send({ from: aliceAddress }); + +// A second contract could reference the first (example pattern) +const { contract: derivedToken } = await TokenContract.deploy( + wallet, + baseToken.address, // Use first contract's address as admin + "DerivedToken", + "DERIV", + 18, +).send({ from: aliceAddress }); + +console.log(`Base token at: ${baseToken.address.toString()}`); +console.log(`Derived token at: ${derivedToken.address.toString()}`); +// docs:end:deploy_with_dependencies + +// docs:start:parallel_deploy +// Deploy contracts in parallel using Promise.all +const contracts = await Promise.all([ + TokenContract.deploy(wallet, aliceAddress, "Token1", "T1", 18) + .send({ + from: aliceAddress, + }) + .then(({ contract }) => contract), + TokenContract.deploy(wallet, aliceAddress, "Token2", "T2", 18) + .send({ + from: aliceAddress, + }) + .then(({ contract }) => contract), + TokenContract.deploy(wallet, aliceAddress, "Token3", "T3", 18) + .send({ + from: aliceAddress, + }) + .then(({ contract }) => contract), +]); + +console.log(`Contract 1 at: ${contracts[0].address}`); +console.log(`Contract 2 at: ${contracts[1].address}`); +console.log(`Contract 3 at: ${contracts[2].address}`); +// docs:end:parallel_deploy + +// docs:start:skip_initialization +// Deploy without running the constructor using skipInitialization +const { contract: delayedToken } = await TokenContract.deploy( + wallet, + aliceAddress, + "DelayedToken", + "DLY", + 18, +).send({ + from: aliceAddress, + skipInitialization: true, +}); + +console.log(`Contract deployed at: ${delayedToken.address}`); + +// Initialize later by calling the constructor manually +await delayedToken.methods + .constructor(aliceAddress, "DelayedToken", "DLY", 18) + .send({ from: aliceAddress }); + +console.log("Contract initialized"); +// docs:end:skip_initialization + +// docs:start:poll_for_events +// Poll for new events at regular intervals +let lastProcessedBlock = await node.getBlockNumber(); + +async function pollForTransferEvents() { + const currentBlock = await node.getBlockNumber(); + + if (currentBlock > lastProcessedBlock) { + const { events } = await getPublicEvents<Transfer>( + node, + TokenContract.events.Transfer, + { + contractAddress: token.address, + fromBlock: BlockNumber(lastProcessedBlock + 1), + toBlock: BlockNumber(currentBlock + 1), // toBlock is exclusive + }, + ); + + for (const { event, metadata } of events) { + // Process each transfer event + console.log( + `Transfer: ${event.amount} from ${event.from} to ${event.to}`, + ); + console.log( + ` in block ${metadata.l2BlockNumber}, tx ${metadata.txHash}`, + ); + } + + lastProcessedBlock = currentBlock; + } +} + +// Example: poll once (in production, use setInterval) +await pollForTransferEvents(); +// docs:end:poll_for_events + +// docs:start:connect_to_contract +// wallet is from the connection guide; token is the contract deployed in the deploy guide +const contract = await Contract.at( + token.address, + TokenContract.artifact, + wallet, +); +// docs:end:connect_to_contract + +// docs:start:basic_send_transaction +// contract is from the step above; aliceAddress is from the connection guide +const { receipt: sendReceipt } = await contract.methods + .transfer_in_public(aliceAddress, bobAddress, 100n, 0n) + .send({ from: aliceAddress }); +console.log(`Transaction mined in block ${sendReceipt.blockNumber}`); +console.log(`Transaction fee: ${sendReceipt.transactionFee}`); +// docs:end:basic_send_transaction + +// docs:start:set_gas_padding +wallet.setEstimatedGasPadding(0.2); // 20% padding instead of the default 10% +// docs:end:set_gas_padding + +// docs:start:simulate_with_metadata +const metaResult = await token.methods + .balance_of_public(aliceAddress) + .simulate({ from: aliceAddress, includeMetadata: true }); +console.log("Balance:", metaResult.result); +// `gasUsed` is the raw gas the simulation consumed; derive your own limits from it (see below). +console.log("L2 gas used:", metaResult.gasUsed!.totalGas.l2Gas); +console.log("DA gas used:", metaResult.gasUsed!.totalGas.daGas); +// docs:end:simulate_with_metadata + +// docs:start:read_public_logs +// Raw public logs are carried on each block's transaction effects. +const latestBlockNumber = await node.getBlockNumber(); +const block = await node.getBlock(latestBlockNumber, { + includeTransactions: true, +}); +const publicLogs = block?.body.txEffects.flatMap((tx) => tx.publicLogs) ?? []; +if (publicLogs.length > 0) { + const rawFields = publicLogs[0].getEmittedFields(); // Fr[] + console.log("Raw log fields:", rawFields.length); +} +// docs:end:read_public_logs + +// docs:start:estimate_mana +const { gasUsed } = await token.methods + .transfer_in_public(aliceAddress, bobAddress, 1n, 0n) + .simulate({ from: aliceAddress, includeMetadata: true }); +// Pad the raw usage yourself to leave headroom for variance, e.g. 10%. +const estimatedGasLimits = gasUsed!.totalGas.mul(1.1); +// docs:end:estimate_mana + +// docs:start:compute_fee_from_estimate +const currentFees = await node.getCurrentMinFees(); +const estimatedFee = estimatedGasLimits.computeFee(currentFees).toBigInt(); +console.log("Estimated fee:", estimatedFee); +// docs:end:compute_fee_from_estimate + +// docs:start:get_fee_from_receipt +const { receipt: feeReceipt } = await token.methods + .mint_to_public(aliceAddress, 1n) + .send({ from: aliceAddress }); +console.log("Transaction fee:", feeReceipt.transactionFee); +// docs:end:get_fee_from_receipt + +// docs:start:check_receipt_status +console.log("Succeeded:", feeReceipt.hasExecutionSucceeded()); +console.log("Block:", feeReceipt.blockNumber); +console.log("Fee paid:", feeReceipt.transactionFee); +// docs:end:check_receipt_status + +// docs:start:pay_with_fee_juice +// contract is a deployed contract instance; aliceAddress is from the connection guide +const { receipt: feeJuiceReceipt } = await token.methods + .mint_to_public(aliceAddress, 1n) + .send({ + from: aliceAddress, + // no fee payment method needed; Fee Juice is used automatically + }); +console.log("Transaction fee:", feeJuiceReceipt.transactionFee); +// docs:end:pay_with_fee_juice + +// docs:start:custom_gas_settings +// Query current network fees to set realistic limits +const networkFees = await node.getCurrentMinFees(); +// Declare at most what the network admits per tx; these limits vary by network geometry, +// so read them from the node rather than hardcoding values that may exceed a given network's maximum. +const { txsLimits } = await node.getNodeInfo(); +const gasLimits = Gas.from(txsLimits.gas); +const gasSettings = GasSettings.from({ + gasLimits, + // Teardown must be strictly less than the total limits so app logic has gas to run. + teardownGasLimits: { + daGas: Math.floor(gasLimits.daGas / 2), + l2Gas: Math.floor(gasLimits.l2Gas / 8), + }, + maxFeesPerGas: { + feePerDaGas: networkFees.feePerDaGas * 2n, + feePerL2Gas: networkFees.feePerL2Gas * 2n, + }, + maxPriorityFeesPerGas: { feePerDaGas: 0n, feePerL2Gas: 0n }, +}); +// docs:end:custom_gas_settings + +// docs:start:send_with_gas_settings +const { receipt: gsReceipt } = await token.methods + .mint_to_public(aliceAddress, 1n) + .send({ + from: aliceAddress, + fee: { gasSettings }, + }); +// docs:end:send_with_gas_settings + +// docs:start:read_logs_by_filter +// Get raw public logs for a specific transaction by locating its block and tx effect. +const txReceiptForLogs = await node.getTxReceipt(gsReceipt.txHash); +const txBlock = await node.getBlock(txReceiptForLogs.blockNumber!, { + includeTransactions: true, +}); +const txLogs = + txBlock?.body.txEffects + .filter((tx) => tx.txHash.equals(gsReceipt.txHash)) + .flatMap((tx) => tx.publicLogs) ?? []; + +// Get raw public logs for a block range by reading each block's tx effects. +const tipBlockNumber = await node.getBlockNumber(); +const rangeLogs = ( + await Promise.all( + Array.from({ length: tipBlockNumber }, (_, i) => BlockNumber(i + 1)).map( + async (blockNumber) => { + const rangeBlock = await node.getBlock(blockNumber, { + includeTransactions: true, + }); + return rangeBlock?.body.txEffects.flatMap((tx) => tx.publicLogs) ?? []; + }, + ), + ) +).flat(); +// docs:end:read_logs_by_filter + +// docs:start:auto_gas_estimation +// Read the gas a transaction would consume before sending, and pad it yourself. +const { gasUsed: autoGasUsed } = await token.methods + .mint_to_public(aliceAddress, 1n) + .simulate({ from: aliceAddress, includeMetadata: true }); +const autoEstimate = autoGasUsed!.totalGas.mul(1.2); // 20% padding +console.log("Auto-estimated L2 gas:", autoEstimate.l2Gas); +// docs:end:auto_gas_estimation + +// docs:start:import_get_public_events +import { getPublicEvents as _importCheck } from "@aztec-labs/aztec.js/events"; +// docs:end:import_get_public_events + +// docs:start:import_private_event_types +import type { PrivateEventFilter } from "@aztec-labs/aztec.js/wallet"; +import { BlockNumber } from "@aztec-labs/aztec.js/fields"; +// docs:end:import_private_event_types + +// docs:start:simulate_private_access +// This works if aliceAddress owns the notes +const { result: privateBalance } = await token.methods + .balance_of_private(aliceAddress) + .simulate({ from: aliceAddress }); +// docs:end:simulate_private_access + +console.log("All advanced examples completed successfully"); diff --git a/docs/lines/v6/examples/ts/aztecjs_advanced/yarn.lock b/docs/lines/v6/examples/ts/aztecjs_advanced/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/aztecjs_authwit/config.yaml b/docs/lines/v6/examples/ts/aztecjs_authwit/config.yaml new file mode 100644 index 00000000000..6be659378ab --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_authwit/config.yaml @@ -0,0 +1,14 @@ +# Configuration for aztecjs_authwit example +# Demonstrates private and public authentication witnesses + +# No custom contracts needed - using pre-built contracts from @aztec-labs/noir-contracts.js +contracts: [] + +# Dependencies: +# - @aztec-labs/* and @aztec-foundation/* packages install at the versions docs/aztec-node.json pins +# - Use npm:package-name for external npm packages +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/noir-contracts.js" diff --git a/docs/lines/v6/examples/ts/aztecjs_authwit/index.ts b/docs/lines/v6/examples/ts/aztecjs_authwit/index.ts new file mode 100644 index 00000000000..5cde79df200 --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_authwit/index.ts @@ -0,0 +1,147 @@ +import { createAztecNodeClient, waitForNode } from "@aztec-labs/aztec.js/node"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; +import { TokenContract } from "@aztec-labs/noir-contracts.js/Token"; +import { Fr } from "@aztec-labs/aztec.js/fields"; +import { SetPublicAuthwitContractInteraction } from "@aztec-labs/aztec.js/authorization"; + +// Setup: connect to network and deploy a token contract +const node = createAztecNodeClient( + process.env.AZTEC_NODE_URL ?? "http://localhost:8080", +); +await waitForNode(node); +const wallet = await EmbeddedWallet.create(node, { ephemeral: true }); + +const testAccounts = await getInitialTestAccountsData(); +const [aliceAddress, bobAddress] = await Promise.all( + testAccounts.slice(0, 2).map(async (account) => { + return ( + await wallet.createSchnorrInitializerlessAccount( + account.secret, + account.salt, + account.signingKey, + ) + ).address; + }), +); + +const { contract: tokenContract } = await TokenContract.deploy( + wallet, + aliceAddress, + "TestToken", + "TST", + 18, +).send({ from: aliceAddress }); + +// Mint tokens to Alice for the examples +await tokenContract.methods + .mint_to_private(aliceAddress, 1000n) + .send({ from: aliceAddress }); + +await tokenContract.methods + .mint_to_public(aliceAddress, 1000n) + .send({ from: aliceAddress }); + +// docs:start:private_authwit +// Alice wants to allow Bob to transfer tokens from her account (private) +const privateNonce = Fr.random(); + +// Define the action Bob will execute +const privateAction = tokenContract.methods.transfer_in_private( + aliceAddress, // from + bobAddress, // to + 100n, // amount + privateNonce, // authwit nonce for replay protection +); + +// Alice creates an authwit authorizing Bob to call this function +const privateWitness = await wallet.createAuthWit(aliceAddress, { + caller: bobAddress, + call: await privateAction.getFunctionCall(), +}); + +// Bob executes the transfer, providing the authwit +// additionalScopes lets the PXE access Alice's private state +// during authwit verification +await privateAction.send({ + from: bobAddress, + authWitnesses: [privateWitness], + additionalScopes: [aliceAddress], +}); +// docs:end:private_authwit + +// docs:start:public_authwit +// Alice wants to allow Bob to transfer tokens from her account (public) +const publicNonce = Fr.random(); + +// Define the action Bob will execute +const publicAction = tokenContract.methods.transfer_in_public( + aliceAddress, // from + bobAddress, // to + 100n, // amount + publicNonce, // authwit nonce +); + +// Alice sets the public authwit (this requires a transaction) +const authwit = await SetPublicAuthwitContractInteraction.create( + wallet, + aliceAddress, + { caller: bobAddress, action: publicAction }, + true, // authorized +); +await authwit.send(); + +// Now Bob can execute the transfer +await publicAction.send({ from: bobAddress }); +// docs:end:public_authwit + +// docs:start:arbitrary_authwit +import { computeInnerAuthWitHash } from "@aztec-labs/aztec.js/authorization"; + +// Create hash of arbitrary data +const innerHash = await computeInnerAuthWitHash([ + Fr.fromHexString("0xcafe"), + Fr.fromHexString("0xbeef"), +]); + +// Create an intent with the consumer contract address +const intent = { + consumer: tokenContract.address, + innerHash, +}; + +// Create the authwit for arbitrary data +const arbitraryWitness = await wallet.createAuthWit(aliceAddress, intent); +console.log("Arbitrary authwit created:", arbitraryWitness); +// docs:end:arbitrary_authwit + +// docs:start:revoke_authwit +// Revoke a public authwit by setting authorized to false +const revokeNonce = Fr.random(); +const revokeAction = tokenContract.methods.transfer_in_public( + aliceAddress, + bobAddress, + 50n, + revokeNonce, +); + +// First, set the authwit +const setAuthwit = await SetPublicAuthwitContractInteraction.create( + wallet, + aliceAddress, + { caller: bobAddress, action: revokeAction }, + true, +); +await setAuthwit.send(); + +// Later, revoke it +const revokeInteraction = await SetPublicAuthwitContractInteraction.create( + wallet, + aliceAddress, + { caller: bobAddress, action: revokeAction }, + false, // revoke authorization +); +await revokeInteraction.send(); +// docs:end:revoke_authwit + +console.log("All authwit examples completed successfully"); diff --git a/docs/lines/v6/examples/ts/aztecjs_authwit/yarn.lock b/docs/lines/v6/examples/ts/aztecjs_authwit/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/aztecjs_connection/config.yaml b/docs/lines/v6/examples/ts/aztecjs_connection/config.yaml new file mode 100644 index 00000000000..c9cfa346d70 --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_connection/config.yaml @@ -0,0 +1,16 @@ +# Configuration for aztecjs_connection example +# Demonstrates connecting to network, creating wallets, and basic operations + +# No custom contracts needed - using pre-built contracts from @aztec-labs/noir-contracts.js +contracts: [] + +# Dependencies: +# - @aztec-labs/* and @aztec-foundation/* packages install at the versions docs/aztec-node.json pins +# - Use npm:package-name for external npm packages +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/noir-contracts.js" + - "@aztec-labs/stdlib" + - "@aztec-labs/ethereum" diff --git a/docs/lines/v6/examples/ts/aztecjs_connection/index.ts b/docs/lines/v6/examples/ts/aztecjs_connection/index.ts new file mode 100644 index 00000000000..f9d6d72b963 --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_connection/index.ts @@ -0,0 +1,197 @@ +// docs:start:connect_to_network +import { createAztecNodeClient, waitForNode } from "@aztec-labs/aztec.js/node"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; + +const nodeUrl = process.env.AZTEC_NODE_URL ?? "http://localhost:8080"; +const node = createAztecNodeClient(nodeUrl); + +// Wait for the network to be ready +await waitForNode(node); + +// Create an EmbeddedWallet connected to the node +const wallet = await EmbeddedWallet.create(node, { ephemeral: true }); +// docs:end:connect_to_network + +// docs:start:verify_connection +const nodeInfo = await node.getNodeInfo(); +console.log("Connected to local network version:", nodeInfo.nodeVersion); +console.log("Chain ID:", nodeInfo.l1ChainId); +// docs:end:verify_connection + +// docs:start:load_accounts +const testAccounts = await getInitialTestAccountsData(); +const [aliceAddress, bobAddress] = await Promise.all( + testAccounts.slice(0, 2).map(async (account) => { + return ( + await wallet.createSchnorrInitializerlessAccount( + account.secret, + account.salt, + account.signingKey, + ) + ).address; + }), +); + +console.log(`Alice's address: ${aliceAddress.toString()}`); +console.log(`Bob's address: ${bobAddress.toString()}`); +// docs:end:load_accounts + +// docs:start:check_fee_juice +import { getFeeJuiceBalance } from "@aztec-labs/aztec.js/utils"; + +const aliceBalance = await getFeeJuiceBalance(aliceAddress, node); +console.log(`Alice's fee juice balance: ${aliceBalance}`); +// docs:end:check_fee_juice + +// docs:start:create_account +import { Fr, GrumpkinScalar } from "@aztec-labs/aztec.js/fields"; + +const secret = Fr.random(); +const salt = Fr.random(); +const signingKey = GrumpkinScalar.random(); +const newAccount = await wallet.createSchnorrAccount(secret, salt, signingKey); +console.log("New account address:", newAccount.address.toString()); +// docs:end:create_account + +// docs:start:deploy_account_sponsored_fpc +// Additional imports needed for account deployment examples +import { NO_FROM } from "@aztec-labs/aztec.js/account"; +import { SponsoredFeePaymentMethod } from "@aztec-labs/aztec.js/fee/testing"; +import { SponsoredFPCContract } from "@aztec-labs/noir-contracts.js/SponsoredFPC"; +import { getContractInstanceFromInstantiationParams } from "@aztec-labs/stdlib/contract"; + +// Set up the Sponsored FPC payment method (see fees guide for details) +const sponsoredFPCInstance = await getContractInstanceFromInstantiationParams( + SponsoredFPCContract.artifact, + { salt: new Fr(0) }, +); +await wallet.registerContract( + sponsoredFPCInstance, + SponsoredFPCContract.artifact, +); +const sponsoredPaymentMethod = new SponsoredFeePaymentMethod( + sponsoredFPCInstance.address, +); + +// newAccount is the account created in the previous section +const deployMethod = await newAccount.getDeployMethod(); +await deployMethod.send({ + from: NO_FROM, + fee: { paymentMethod: sponsoredPaymentMethod }, +}); +// docs:end:deploy_account_sponsored_fpc + +// docs:start:create_fee_juice_account +// `feeJuiceAccount` is just another Schnorr account, the same kind as +// `newAccount` above. It gets its own name here so both deploy paths +// can coexist in one example; in your own code, pick whichever name fits. +const feeJuiceSecret = Fr.random(); +const feeJuiceSalt = Fr.random(); +const feeJuiceSigningKey = GrumpkinScalar.random(); +const feeJuiceAccount = await wallet.createSchnorrAccount( + feeJuiceSecret, + feeJuiceSalt, + feeJuiceSigningKey, +); +// docs:end:create_fee_juice_account + +// docs:start:bridge_fee_juice_setup +import { createExtendedL1Client } from "@aztec-labs/ethereum/client"; +import { L1FeeJuicePortalManager } from "@aztec-labs/aztec.js/ethereum"; +import { createLogger } from "@aztec-labs/aztec.js/log"; + +// Create an L1 client (accepts a mnemonic or 0x-prefixed private key) +const l1RpcUrl = process.env.ETHEREUM_HOST ?? "http://localhost:8545"; +const l1Mnemonic = + "test test test test test test test test test test test junk"; +const l1Client = createExtendedL1Client([l1RpcUrl], l1Mnemonic); + +// Create a portal manager to interact with the L1 fee juice portal +const logger = createLogger("docs:fee-juice-bridge"); +const portalManager = await L1FeeJuicePortalManager.new(node, l1Client, logger); +// docs:end:bridge_fee_juice_setup + +// docs:start:bridge_fee_juice_execute +// portalManager is from the L1FeeJuicePortalManager setup above +// feeJuiceAccount.address is an Aztec address from createSchnorrAccount +const claim = await portalManager.bridgeTokensPublic( + feeJuiceAccount.address, // the L2 address + 1000000000000000000000n, // the amount to send to the L1 portal + true, // whether to mint or not (set to false if your L1 account already has fee juice!) +); + +console.log("Claim secret:", claim.claimSecret); +console.log("Claim amount:", claim.claimAmount); +// docs:end:bridge_fee_juice_execute + +// docs:start:deploy_contract +import { TokenContract } from "@aztec-labs/noir-contracts.js/Token"; + +const { contract: token } = await TokenContract.deploy( + wallet, + aliceAddress, + "TestToken", + "TST", + 18, +).send({ from: aliceAddress }); + +console.log(`Token deployed at: ${token.address.toString()}`); +// docs:end:deploy_contract + +// docs:start:send_transaction +const { receipt } = await token.methods + .mint_to_public(aliceAddress, 1000n) + .send({ from: aliceAddress }); + +console.log(`Transaction mined in block ${receipt.blockNumber}`); +console.log(`Transaction fee: ${receipt.transactionFee}`); +// docs:end:send_transaction + +// docs:start:simulate_function +const { result: balance } = await token.methods + .balance_of_public(aliceAddress) + .simulate({ from: aliceAddress }); + +console.log(`Alice's token balance: ${balance}`); +// docs:end:simulate_function + +// The bridged Fee Juice claim only becomes consumable once the network's inbox lag (2 checkpoints) +// has elapsed since the L1->L2 message was inserted. The token deploy and mint above produced two +// blocks; mine one more here so the claim is available when we pay with it below. +await token.methods + .mint_to_public(aliceAddress, 1n) + .send({ from: aliceAddress }); + +// docs:start:bridge_fee_juice_claim +import { FeeJuicePaymentMethodWithClaim } from "@aztec-labs/aztec.js/fee"; + +// claim is from the bridgeTokensPublic step above +// Create a payment method that claims the bridged Fee Juice and uses it to pay +const bridgePaymentMethod = new FeeJuicePaymentMethodWithClaim( + feeJuiceAccount.address, + claim, +); + +// Use it to pay for any transaction; here we deploy the account in one step +const deployMethodBridged = await feeJuiceAccount.getDeployMethod(); +await deployMethodBridged.send({ + from: NO_FROM, + fee: { paymentMethod: bridgePaymentMethod }, +}); +// docs:end:bridge_fee_juice_claim + +// docs:start:verify_account_deployment +// `newAccount` refers to whichever account you just deployed, +// either the Sponsored FPC account or `feeJuiceAccount` from the Fee Juice path. +const metadata = await wallet.getContractMetadata(newAccount.address); +console.log("Account deployed:", metadata.initializationStatus); +// docs:end:verify_account_deployment + +const feeJuiceMetadata = await wallet.getContractMetadata( + feeJuiceAccount.address, +); +console.log( + "Fee Juice account deployed:", + feeJuiceMetadata.initializationStatus, +); diff --git a/docs/lines/v6/examples/ts/aztecjs_connection/yarn.lock b/docs/lines/v6/examples/ts/aztecjs_connection/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/aztecjs_getting_started/config.yaml b/docs/lines/v6/examples/ts/aztecjs_getting_started/config.yaml new file mode 100644 index 00000000000..0b68b9d0094 --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_getting_started/config.yaml @@ -0,0 +1,14 @@ +# Configuration for aztecjs_getting_started example +# This tutorial uses pre-built TokenContract from @aztec-labs/noir-contracts.js + +# No custom contracts needed - using pre-built contracts from @aztec-labs/noir-contracts.js +contracts: [] + +# Dependencies: +# - @aztec-labs/* and @aztec-foundation/* packages install at the versions docs/aztec-node.json pins +# - Use npm:package-name for external npm packages +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/noir-contracts.js" diff --git a/docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts b/docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts new file mode 100644 index 00000000000..cbe1948546b --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts @@ -0,0 +1,70 @@ +// docs:start:setup +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; + +const nodeUrl = process.env.AZTEC_NODE_URL ?? "http://localhost:8080"; +const wallet = await EmbeddedWallet.create(nodeUrl, { ephemeral: true }); + +const [alice, bob] = await getInitialTestAccountsData(); +await wallet.createSchnorrInitializerlessAccount( + alice.secret, + alice.salt, + alice.signingKey, +); +await wallet.createSchnorrInitializerlessAccount( + bob.secret, + bob.salt, + bob.signingKey, +); +// docs:end:setup + +// docs:start:deploy +import { TokenContract } from "@aztec-labs/noir-contracts.js/Token"; + +const { contract: token } = await TokenContract.deploy( + wallet, + alice.address, + "TokenName", + "TKN", + 18, +).send({ from: alice.address }); +// docs:end:deploy + +// docs:start:mint +await token.methods + .mint_to_private(alice.address, 100) + .send({ from: alice.address }); +// docs:end:mint + +// docs:start:check_balances +let { result: aliceBalance } = await token.methods + .balance_of_private(alice.address) + .simulate({ from: alice.address }); +console.log(`Alice's balance: ${aliceBalance}`); +let { result: bobBalance } = await token.methods + .balance_of_private(bob.address) + .simulate({ from: bob.address }); +console.log(`Bob's balance: ${bobBalance}`); +// docs:end:check_balances + +// docs:start:transfer +await token.methods.transfer(bob.address, 10).send({ from: alice.address }); +({ result: bobBalance } = await token.methods + .balance_of_private(bob.address) + .simulate({ from: bob.address })); +console.log(`Bob's balance: ${bobBalance}`); +// docs:end:transfer + +// docs:start:set_minter +await token.methods.set_minter(bob.address, true).send({ from: alice.address }); +// docs:end:set_minter + +// docs:start:bob_mints +await token.methods + .mint_to_private(bob.address, 100) + .send({ from: bob.address }); +({ result: bobBalance } = await token.methods + .balance_of_private(bob.address) + .simulate({ from: bob.address })); +console.log(`Bob's balance: ${bobBalance}`); +// docs:end:bob_mints diff --git a/docs/lines/v6/examples/ts/aztecjs_getting_started/yarn.lock b/docs/lines/v6/examples/ts/aztecjs_getting_started/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/config.yaml b/docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/config.yaml new file mode 100644 index 00000000000..52970f481fe --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/config.yaml @@ -0,0 +1,16 @@ +# Configuration for aztecjs_kernelless_simulation example +# Demonstrates kernelless simulation with stub account overrides: +# - Calling .simulate() on a view function without signing prompts +# - Harvesting CallAuthorizationRequest offchain effects to build authwits + +# No custom contracts needed - using pre-built contracts from @aztec-labs/noir-contracts.js +contracts: [] + +# Dependencies: +# - @aztec-labs/* and @aztec-foundation/* packages install at the versions docs/aztec-node.json pins +# - Use npm:package-name for external npm packages +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/noir-contracts.js" diff --git a/docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/index.ts b/docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/index.ts new file mode 100644 index 00000000000..f79845b7db0 --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/index.ts @@ -0,0 +1,167 @@ +import { createAztecNodeClient, waitForNode } from "@aztec-labs/aztec.js/node"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; +import { TokenContract } from "@aztec-labs/noir-contracts.js/Token"; +import { CrowdfundingContract } from "@aztec-labs/noir-contracts.js/Crowdfunding"; +import { Fr } from "@aztec-labs/aztec.js/fields"; +import { deriveKeys } from "@aztec-labs/aztec.js/keys"; +import { CallAuthorizationRequest } from "@aztec-labs/aztec.js/authorization"; + +// Setup: connect to network, create alice and bob, deploy a token, mint to alice. +// EmbeddedWallet runs every .simulate() call in kernelless mode with a stub-account +// override applied automatically. The examples below rely on that default. +const node = createAztecNodeClient( + process.env.AZTEC_NODE_URL ?? "http://localhost:8080", +); +await waitForNode(node); +const wallet = await EmbeddedWallet.create(node, { ephemeral: true }); + +const testAccounts = await getInitialTestAccountsData(); +const [aliceAddress, bobAddress] = await Promise.all( + testAccounts.slice(0, 2).map(async (account) => { + return ( + await wallet.createSchnorrInitializerlessAccount( + account.secret, + account.salt, + account.signingKey, + ) + ).address; + }), +); + +const { contract: tokenContract } = await TokenContract.deploy( + wallet, + aliceAddress, + "TestToken", + "TST", + 18, +).send({ from: aliceAddress }); + +await tokenContract.methods + .mint_to_private(aliceAddress, 1000n) + .send({ from: aliceAddress }); + +// docs:start:simulate-view-without-signing +// Reading a private view function would normally route through the account +// contract's entrypoint, whose is_valid check would prompt the user to sign. +// With EmbeddedWallet, .simulate() runs kernelless with a stub-account +// override applied to alice's account, so no signing prompt is triggered. +const { result: decimals } = await tokenContract.methods + .private_get_decimals() + .simulate({ from: aliceAddress }); + +console.log("Token decimals (read via private view):", decimals); +// docs:end:simulate-view-without-signing + +// Deploy a Crowdfunding contract with Bob as the operator and the previously +// deployed token as the donation token. The contract receives donation notes +// for donors, so it needs its own keys (a contract-account-style deployment). +const crowdfundingSecretKey = Fr.random(); +const crowdfundingPublicKeys = (await deriveKeys(crowdfundingSecretKey)) + .publicKeys; +const deadline = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60; + +const crowdfundingDeployment = CrowdfundingContract.deploy( + wallet, + tokenContract.address, + bobAddress, + deadline, + { publicKeys: crowdfundingPublicKeys, deployer: aliceAddress }, +); +const crowdfundingInstance = await crowdfundingDeployment.getInstance(); +await wallet.registerContract( + crowdfundingInstance, + CrowdfundingContract.artifact, + crowdfundingSecretKey, +); +const { contract: crowdfundingContract } = await crowdfundingDeployment.send({ + from: aliceAddress, + additionalScopes: [crowdfundingInstance.address], +}); + +// docs:start:simulate-and-collect-effects +// Alice calls Crowdfunding.donate(amount). Internally the contract calls +// transfer_in_private(alice, crowdfunding, amount, 0) with msg_sender = +// crowdfunding, so the token's #[authorize_once] macro requires an authwit +// from Alice authorizing the crowdfunding contract. Alice is the sender, so +// her PXE has her notes and nullifier key — no cross-wallet state sharing +// is required. +// +// With kernelless + stub override (default in EmbeddedWallet), the simulation +// runs without prompting Alice to sign; the macro emits a +// CallAuthorizationRequest as an offchain effect that the wallet can turn +// into a real authwit before .send(). +const donationAmount = 100n; +const donateAction = crowdfundingContract.methods.donate(donationAmount); + +const { offchainEffects } = await donateAction.simulate({ + from: aliceAddress, + includeMetadata: true, +}); + +// Filter offchain effects for authwit requests by selector. +const authwitSelector = await CallAuthorizationRequest.getSelector(); +const authwitEffects = offchainEffects.filter( + (effect) => + effect.data.length > 0 && effect.data[0].equals(authwitSelector.toField()), +); + +if (authwitEffects.length !== 1) { + throw new Error( + `Expected exactly one CallAuthorizationRequest, got ${authwitEffects.length}`, + ); +} +// docs:end:simulate-and-collect-effects + +// docs:start:decode-call-authorization +// Decode each effect into a CallAuthorizationRequest. The inner hash is the +// piece the authorizing account (Alice) needs to sign. +const authorizationRequests = await Promise.all( + authwitEffects.map((effect) => + CallAuthorizationRequest.fromFields(effect.data), + ), +); + +for (const request of authorizationRequests) { + console.log("Authwit needed:", { + onBehalfOf: request.onBehalfOf.toString(), + msgSender: request.msgSender.toString(), + functionSelector: request.functionSelector.toString(), + }); +} + +if (!authorizationRequests[0].onBehalfOf.equals(aliceAddress)) { + throw new Error( + `Expected onBehalfOf to be alice (${aliceAddress.toString()}), got ${authorizationRequests[0].onBehalfOf.toString()}`, + ); +} +// docs:end:decode-call-authorization + +// docs:start:build-authwits-and-send +// Alice creates a real authentication witness from each inner hash. The +// `consumer` is the contract that consumes the authwit — here, the token, +// because that's where the inner transfer_in_private (and its #[authorize_once] +// site) runs. +const authWitnesses = await Promise.all( + authorizationRequests.map((request) => + wallet.createAuthWit(request.onBehalfOf, { + consumer: tokenContract.address, + innerHash: request.innerHash, + }), + ), +); + +// Alice now sends the real donate transaction with the collected authwits +// attached. +// +// Note: EmbeddedWallet.sendTx already runs this pre-simulation + authwit +// collection internally, so passing `authWitnesses` here is redundant for +// EmbeddedWallet. We pass them explicitly anyway because this is the pattern +// a wallet that does not auto-collect needs to follow. +await donateAction.send({ + from: aliceAddress, + authWitnesses, +}); +// docs:end:build-authwits-and-send + +console.log("Kernelless simulation example completed successfully"); diff --git a/docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/yarn.lock b/docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/aztecjs_runner/config.yaml b/docs/lines/v6/examples/ts/aztecjs_runner/config.yaml new file mode 100644 index 00000000000..7c4d47b92bf --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_runner/config.yaml @@ -0,0 +1,15 @@ +# Configuration for aztecjs_runner - runs all aztec.js example snippets +# This project executes the examples against a live local network + +# No custom contracts needed - using pre-built contracts from @aztec-labs/noir-contracts.js +contracts: [] + +# Dependencies - union of all aztecjs example dependencies +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/noir-contracts.js" + - "@aztec-labs/ethereum" + - "@aztec-labs/stdlib" + - "@aztec-labs/foundation" diff --git a/docs/lines/v6/examples/ts/aztecjs_runner/index.ts b/docs/lines/v6/examples/ts/aztecjs_runner/index.ts new file mode 100644 index 00000000000..8baff33adfa --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_runner/index.ts @@ -0,0 +1,23 @@ +/** + * Placeholder for aztecjs_runner + * + * This project is a test runner for aztec.js documentation examples. + * Use ./run.sh to execute the examples against a live local network. + * + * The actual example files are in sibling directories: + * - aztecjs_connection/index.ts + * - aztecjs_advanced/index.ts + * - aztecjs_authwit/index.ts + * - aztecjs_testing/index.ts + */ + +// Import dependencies to verify they resolve correctly +import { createAztecNodeClient } from "@aztec-labs/aztec.js/node"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { TokenContract } from "@aztec-labs/noir-contracts.js/Token"; +import { Fr } from "@aztec-labs/aztec.js/fields"; + +// Export to satisfy noUnusedLocals +export { createAztecNodeClient, EmbeddedWallet, TokenContract, Fr }; + +console.log("aztecjs_runner: Use ./run.sh to execute examples against a live network"); diff --git a/docs/lines/v6/examples/ts/aztecjs_runner/run.sh b/docs/lines/v6/examples/ts/aztecjs_runner/run.sh new file mode 100755 index 00000000000..11ede045f43 --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_runner/run.sh @@ -0,0 +1,282 @@ +#!/usr/bin/env bash +# Run aztec.js documentation examples against a live local network +# +# Prerequisites: +# - Local Aztec network running on localhost:8080 +# - The pinned toolchain provisioned and exported: in docs/, `node scripts/aztec_node.mts provision cli` +# and `eval "$(node scripts/aztec_node.mts env)"` (AZTEC_VERSION, BB_VERSION; AZTEC defaults to +# the provisioned CLI) +# +# Usage: +# ./run.sh # Run all examples +# ./run.sh connection # Run specific example +# ./run.sh getting_started advanced # Run multiple examples +# +# Available examples: connection, getting_started, advanced, authwit, testing, swap, aave_bridge, recursive_verification + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXAMPLES_DIR="$(dirname "$SCRIPT_DIR")" +source "$EXAMPLES_DIR/lib.sh" +# The docs tree from this script's known path (<docs>/examples/ts/aztecjs_runner), not git, which +# fails safe.directory inside Docker; a line's stage (examples/bootstrap.sh --line) mirrors docs/. +DOCS_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +export AZTEC_NODE_URL="${AZTEC_NODE_URL:-http://localhost:8080}" +# In the compose run the host's CLI path does not exist, so the default names it inside the mount. +AZTEC="${AZTEC:-$DOCS_ROOT/.aztec-node/cli/node_modules/.bin/aztec}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Aztec.js Documentation Examples Test Runner ║${NC}" +echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}" +echo "" + +# Check if network is running +echo -e "${YELLOW}Checking network connection at $AZTEC_NODE_URL...${NC}" +if ! curl -s "$AZTEC_NODE_URL" > /dev/null 2>&1; then + echo -e "${RED}ERROR: Cannot connect to Aztec network at $AZTEC_NODE_URL${NC}" + echo "Please start the network with: aztec start --local-network" + exit 1 +fi +echo -e "${GREEN}✓ Network is running${NC}" +echo "" + +# Run `yarn add` with retries, keeping every attempt's output so transient flakes and +# install warnings stay visible under set -euo pipefail. +yarn_add_with_retry() { + local max_attempts=3 + local attempt=1 + while [ "$attempt" -lt "$max_attempts" ]; do + if yarn add "$@"; then + return 0 + fi + echo -e "${YELLOW} yarn add attempt $attempt failed, retrying in 5s...${NC}" + sleep 5 + attempt=$((attempt + 1)) + done + yarn add "$@" +} + +# Setup function for a project +setup_project() { + local project_name=$1 + local project_dir="$EXAMPLES_DIR/$project_name" + + echo -e "${YELLOW}Setting up $project_name...${NC}" + + cd "$project_dir" + + # Clean up any previous setup (include codegenCache.json so codegen re-generates artifacts/) + rm -rf node_modules .yarn package.json tsconfig.json artifacts codegenCache.json 2>/dev/null || true + + # Run codegen for custom contracts if specified in config.yaml + local contract_count + contract_count="$(yq eval '.contracts | length' config.yaml 2>/dev/null || echo "0")" + + if [ "$contract_count" -gt 0 ]; then + local ARTIFACTS_DIR="$DOCS_ROOT/target" + + while IFS= read -r contract_name; do + local artifact="$ARTIFACTS_DIR/${contract_name}.json" + if [ ! -f "$artifact" ]; then + echo -e "${RED}ERROR: Compiled artifact not found: ${artifact}${NC}" + echo -e "${RED} The Noir compile step may have failed. Check 'Compile (Noir contracts)' output.${NC}" + return 1 + fi + echo -e " Generating TS interface for ${contract_name}..." + "$AZTEC" codegen "$artifact" -o artifacts + done < <(yq eval '.contracts[]' config.yaml) + + # Verify codegen produced the expected files + echo -e " Generated artifacts:" + ls -la artifacts/ 2>/dev/null || echo -e "${RED} ERROR: artifacts/ directory does not exist after codegen${NC}" + fi + + # Initialize yarn + yarn init -y + yarn config set nodeLinker node-modules + + # Set package type to module for ESM + node -e "const pkg = require('./package.json'); pkg.type = 'module'; require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));" + + # Read dependencies from config.yaml and install + parse_dependencies config.yaml + if [ "$PARSED_DEPS_FOUND" = true ]; then + [ ${#PINNED_DEPS[@]} -gt 0 ] && yarn_add_with_retry "${PINNED_DEPS[@]}" + [ ${#NPM_DEPS[@]} -gt 0 ] && yarn_add_with_retry "${NPM_DEPS[@]}" + fi + + # Pin typescript to the 5.x line used across the monorepo. An unpinned + # `yarn add typescript` now resolves to the 7.x native port, whose package + # layout has no lib/_tsc.js, so yarn 4's builtin compat patch fails to apply. + yarn_add_with_retry -D "typescript@^5.3.3" tsx + + # Copy tsconfig + cp "$EXAMPLES_DIR/tsconfig.template.json" tsconfig.json + + echo -e "${GREEN}✓ $project_name ready${NC}" +} + +# Run function for a project +run_project() { + local project_name=$1 + local project_dir="$EXAMPLES_DIR/$project_name" + + echo "" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE}▶ Running: $project_name${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + + cd "$project_dir" + + # Run setup command if specified in config.yaml (e.g., proof generation) + local setup_cmd + setup_cmd="$(yq eval '.setup // ""' config.yaml 2>/dev/null)" + if [ -n "$setup_cmd" ]; then + echo -e "${YELLOW}Running setup: $setup_cmd${NC}" + if ! eval "$setup_cmd"; then + echo -e "${RED}✗ FAIL - $project_name setup failed${NC}" + return 1 + fi + echo -e "${GREEN}✓ Setup complete${NC}" + fi + + local start_time=$(date +%s) + local max_retries=5 + + for attempt in $(seq 1 $max_retries); do + if npx tsx index.ts; then + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + echo "" + echo -e "${GREEN}✓ PASS - $project_name (${duration}s)${NC}" + return 0 + fi + + if [ "$attempt" -lt "$max_retries" ]; then + echo -e "${YELLOW} Attempt $attempt/$max_retries failed, retrying in 10s...${NC}" + # Clean up PXE data between retries to avoid stale state + rm -rf "$project_dir/pxe_data_"* "$project_dir/wallet_data_"* 2>/dev/null || true + sleep 10 + fi + done + + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + echo "" + echo -e "${RED}✗ FAIL - $project_name after $max_retries attempts (${duration}s)${NC}" + return 1 +} + +# Cleanup function +cleanup_project() { + local project_name=$1 + local project_dir="$EXAMPLES_DIR/$project_name" + + rm -rf "$project_dir/node_modules" \ + "$project_dir/.yarn" \ + "$project_dir/package.json" \ + "$project_dir/tsconfig.json" \ + "$project_dir/.yarnrc.yml" \ + "$project_dir/artifacts" \ + "$project_dir/codegenCache.json" \ + "$project_dir/data.json" 2>/dev/null || true + # Keep yarn.lock empty + > "$project_dir/yarn.lock" +} + +# Determine which examples to run +# Note: bob_token_contract and other custom contract examples require verification keys +# which aren't generated during docs compilation, so they're not included by default +if [ $# -eq 0 ]; then + # aave_bridge disabled: timing out on merge queue (~600s), blocked on proving block 64. + # See http://ci.aztec-labs.com/aabf2c7e271636a0 + EXAMPLES=("aztecjs_connection" "aztecjs_getting_started" "aztecjs_advanced" "aztecjs_authwit" "aztecjs_testing" "example_swap" "recursive_verification") +else + EXAMPLES=() + for arg in "$@"; do + case "$arg" in + connection) EXAMPLES+=("aztecjs_connection") ;; + getting_started) EXAMPLES+=("aztecjs_getting_started") ;; + advanced) EXAMPLES+=("aztecjs_advanced") ;; + authwit) EXAMPLES+=("aztecjs_authwit") ;; + testing) EXAMPLES+=("aztecjs_testing") ;; + swap) EXAMPLES+=("example_swap") ;; + aave_bridge) EXAMPLES+=("aave_bridge") ;; + recursive_verification) EXAMPLES+=("recursive_verification") ;; + *) + if [ -d "$EXAMPLES_DIR/aztecjs_$arg" ]; then + EXAMPLES+=("aztecjs_$arg") + elif [ -d "$EXAMPLES_DIR/$arg" ]; then + EXAMPLES+=("$arg") + else + echo -e "${RED}Unknown example: $arg${NC}" + exit 1 + fi + ;; + esac + done +fi + +echo "Running ${#EXAMPLES[@]} example(s): ${EXAMPLES[*]}" +echo "" + +# Track results +PASSED=0 +FAILED=0 +FAILED_EXAMPLES=() + +# Setup all projects first +echo -e "${YELLOW}Setting up projects...${NC}" +for example in "${EXAMPLES[@]}"; do + setup_project "$example" +done +echo "" + +# Run all projects +for example in "${EXAMPLES[@]}"; do + if run_project "$example"; then + PASSED=$((PASSED + 1)) + else + FAILED=$((FAILED + 1)) + FAILED_EXAMPLES+=("$example") + fi +done + +# Cleanup +echo "" +echo -e "${YELLOW}Cleaning up...${NC}" +for example in "${EXAMPLES[@]}"; do + cleanup_project "$example" +done + +# Summary +echo "" +echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} SUMMARY ${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" +echo "" +echo -e " Passed: ${GREEN}$PASSED${NC}" +echo -e " Failed: ${RED}$FAILED${NC}" +echo "" + +if [ $FAILED -gt 0 ]; then + echo -e "${RED}Failed examples:${NC}" + for ex in "${FAILED_EXAMPLES[@]}"; do + echo -e " - $ex" + done + echo "" + exit 1 +else + echo -e "${GREEN}✅ All examples passed!${NC}" + echo "" + exit 0 +fi diff --git a/docs/lines/v6/examples/ts/aztecjs_runner/yarn.lock b/docs/lines/v6/examples/ts/aztecjs_runner/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/aztecjs_testing/config.yaml b/docs/lines/v6/examples/ts/aztecjs_testing/config.yaml new file mode 100644 index 00000000000..c3557fc152c --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_testing/config.yaml @@ -0,0 +1,14 @@ +# Configuration for aztecjs_testing example +# Demonstrates complete Jest test structure for Aztec contracts + +# No custom contracts needed - using pre-built contracts from @aztec-labs/noir-contracts.js +contracts: [] + +# Dependencies: +# - @aztec-labs/* and @aztec-foundation/* packages install at the versions docs/aztec-node.json pins +# - Use npm:package-name for external npm packages +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/noir-contracts.js" diff --git a/docs/lines/v6/examples/ts/aztecjs_testing/index.ts b/docs/lines/v6/examples/ts/aztecjs_testing/index.ts new file mode 100644 index 00000000000..4b2413a3033 --- /dev/null +++ b/docs/lines/v6/examples/ts/aztecjs_testing/index.ts @@ -0,0 +1,158 @@ +// docs:start:complete_test_example +import { createAztecNodeClient, waitForNode } from "@aztec-labs/aztec.js/node"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; +import { TokenContract } from "@aztec-labs/noir-contracts.js/Token"; +import { AztecAddress } from "@aztec-labs/aztec.js/addresses"; + +// This file demonstrates a complete Jest test structure. +// In a real test file, wrap this in describe() and it() blocks. + +// Test setup variables +let wallet: EmbeddedWallet; +let aliceAddress: AztecAddress; +let bobAddress: AztecAddress; +let token: TokenContract; + +// beforeAll equivalent - setup +async function setup() { + const node = createAztecNodeClient( + process.env.AZTEC_NODE_URL ?? "http://localhost:8080", + ); + await waitForNode(node); + wallet = await EmbeddedWallet.create(node, { ephemeral: true }); + const testAccounts = await getInitialTestAccountsData(); + [aliceAddress, bobAddress] = await Promise.all( + testAccounts.slice(0, 2).map(async (account) => { + return ( + await wallet.createSchnorrInitializerlessAccount( + account.secret, + account.salt, + account.signingKey, + ) + ).address; + }), + ); + + ({ contract: token } = await TokenContract.deploy( + wallet, + aliceAddress, + "Test", + "TST", + 18, + ).send({ + from: aliceAddress, + })); +} + +// Test: mints tokens to an account +async function testMintTokens() { + await token.methods + .mint_to_public(aliceAddress, 1000n) + .send({ from: aliceAddress }); + + const { result: balance } = await token.methods + .balance_of_public(aliceAddress) + .simulate({ from: aliceAddress }); + + if (balance !== 1000n) { + throw new Error(`Expected balance 1000n, got ${balance}`); + } + console.log("✓ Mint tokens test passed"); +} + +// Test: transfers tokens between accounts +async function testTransferTokens() { + // First mint some tokens + await token.methods + .mint_to_public(aliceAddress, 1000n) + .send({ from: aliceAddress }); + + // Transfer to bob using public transfer + await token.methods + .transfer_in_public(aliceAddress, bobAddress, 100n, 0n) + .send({ from: aliceAddress }); + + const { result: aliceBalance } = await token.methods + .balance_of_public(aliceAddress) + .simulate({ from: aliceAddress }); + const { result: bobBalance } = await token.methods + .balance_of_public(bobAddress) + .simulate({ from: bobAddress }); + + // Note: balances accumulate from previous test + console.log(`Alice balance: ${aliceBalance}, Bob balance: ${bobBalance}`); + console.log("✓ Transfer tokens test passed"); +} + +// Test: reverts when transferring more than balance +async function testRevertOnOverTransfer() { + const { result: balance } = await token.methods + .balance_of_public(aliceAddress) + .simulate({ from: aliceAddress }); + + try { + await token.methods + .transfer_in_public(aliceAddress, bobAddress, balance + 1n, 0n) + .simulate({ from: aliceAddress }); + throw new Error("Expected simulation to throw"); + } catch (error) { + // Expected to throw + console.log("✓ Revert on over-transfer test passed"); + } +} + +// Run all tests +async function runTests() { + await setup(); + await testMintTokens(); + await testTransferTokens(); + await testRevertOnOverTransfer(); + console.log("\n✓ All tests passed"); +} + +await runTests(); +// docs:end:complete_test_example + +// docs:start:load_test_accounts +import { registerInitialLocalNetworkAccountsInWallet } from "@aztec-labs/wallets/testing"; + +// wallet is the EmbeddedWallet from the setup section above +const [alice, bob] = await registerInitialLocalNetworkAccountsInWallet(wallet); +// docs:end:load_test_accounts + +// docs:start:deploy_test_contract +// wallet is from the setup section; alice is from registerInitialLocalNetworkAccountsInWallet +const { contract: testToken } = await TokenContract.deploy( + wallet, + alice, // admin + "TestToken", + "TST", + 18, +).send({ from: alice }); +// docs:end:deploy_test_contract + +// docs:start:test_revert_case +async function testRevertExample() { + // testToken and alice are from the deploy/load sections above + const { result: balance } = await testToken.methods + .balance_of_public(alice) + .simulate({ from: alice }); + + let reverted = false; + try { + await testToken.methods + .transfer_in_public(alice, bob, balance + 1n, 0n) + .simulate({ from: alice }); + } catch (error) { + reverted = true; + } + + if (!reverted) { + throw new Error("Expected simulation to revert for over-transfer"); + } + console.log("✓ Revert on over-transfer test passed"); +} + +await testRevertExample(); +// docs:end:test_revert_case diff --git a/docs/lines/v6/examples/ts/aztecjs_testing/yarn.lock b/docs/lines/v6/examples/ts/aztecjs_testing/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/bob_token_contract/config.yaml b/docs/lines/v6/examples/ts/bob_token_contract/config.yaml new file mode 100644 index 00000000000..c5a2a5ddb52 --- /dev/null +++ b/docs/lines/v6/examples/ts/bob_token_contract/config.yaml @@ -0,0 +1,17 @@ +# Configuration for bob_token_contract example + +# Contract artifacts to import for codegen (names without .json extension) +# These must already be compiled to docs/target/ directory +contracts: + - bob_token_contract-BobToken + +# Dependencies: +# - @aztec-labs/* and @aztec-foundation/* packages install at the versions docs/aztec-node.json pins +# - Use npm:package-name for external npm packages +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/kv-store" + # Example external package (uncomment if needed): + # - "npm:viem" diff --git a/docs/lines/v6/examples/ts/bob_token_contract/index.ts b/docs/lines/v6/examples/ts/bob_token_contract/index.ts new file mode 100644 index 00000000000..858917a63d7 --- /dev/null +++ b/docs/lines/v6/examples/ts/bob_token_contract/index.ts @@ -0,0 +1,124 @@ +// docs:start:imports +import { BobTokenContract } from "./artifacts/BobToken.js"; +import { AztecAddress } from "@aztec-labs/aztec.js/addresses"; +import { createAztecNodeClient } from "@aztec-labs/aztec.js/node"; +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +// docs:end:imports + +// docs:start:get_balances +async function getBalances( + contract: BobTokenContract, + aliceAddress: AztecAddress, + bobAddress: AztecAddress, +) { + await Promise.all([ + contract.methods + .public_balance_of(aliceAddress) + .simulate({ from: aliceAddress }) + .then(({ result }) => result), + contract.methods + .private_balance_of(aliceAddress) + .simulate({ from: aliceAddress }) + .then(({ result }) => result), + contract.methods + .public_balance_of(bobAddress) + .simulate({ from: bobAddress }) + .then(({ result }) => result), + contract.methods + .private_balance_of(bobAddress) + .simulate({ from: bobAddress }) + .then(({ result }) => result), + ]).then( + ([ + alicePublicBalance, + alicePrivateBalance, + bobPublicBalance, + bobPrivateBalance, + ]) => { + console.log( + `📊 Alice has ${alicePublicBalance} public BOB tokens and ${alicePrivateBalance} private BOB tokens`, + ); + console.log( + `📊 Bob's Clinic has ${bobPublicBalance} public BOB tokens and ${bobPrivateBalance} private BOB tokens`, + ); + }, + ); +} +// docs:end:get_balances + +// docs:start:checkpoint_1 +async function main() { + // Connect to local network + const node = createAztecNodeClient("http://localhost:8080"); + + // `ephemeral: true` keeps PXE state in memory, so restarting the local + // network won't leave this script pointing at stale block hashes. + const wallet = await EmbeddedWallet.create(node, { ephemeral: true }); + + const [giggleWalletData, aliceWalletData, bobClinicWalletData] = + await getInitialTestAccountsData(); + const giggleAccountManager = await wallet.createSchnorrInitializerlessAccount( + giggleWalletData.secret, + giggleWalletData.salt, + giggleWalletData.signingKey, + ); + const aliceAccountManager = await wallet.createSchnorrInitializerlessAccount( + aliceWalletData.secret, + aliceWalletData.salt, + aliceWalletData.signingKey, + ); + const bobClinicAccountManager = + await wallet.createSchnorrInitializerlessAccount( + bobClinicWalletData.secret, + bobClinicWalletData.salt, + bobClinicWalletData.signingKey, + ); + + const giggleAddress = giggleAccountManager.address; + const aliceAddress = aliceAccountManager.address; + const bobClinicAddress = bobClinicAccountManager.address; + + const { contract: bobToken } = await BobTokenContract.deploy(wallet).send({ + from: giggleAddress, + }); + + await bobToken.methods + .mint_public(aliceAddress, 100n) + .send({ from: giggleAddress }); + + await bobToken.methods + .transfer_public(bobClinicAddress, 10n) + .send({ from: aliceAddress }); + // docs:end:checkpoint_1 + + // docs:start:checkpoint_2 + await bobToken.methods + .mint_public(aliceAddress, 100n) + .send({ from: giggleAddress }); + await getBalances(bobToken, aliceAddress, bobClinicAddress); + + await bobToken.methods + .transfer_public(bobClinicAddress, 10n) + .send({ from: aliceAddress }); + await getBalances(bobToken, aliceAddress, bobClinicAddress); + + await bobToken.methods.public_to_private(90n).send({ from: aliceAddress }); + await getBalances(bobToken, aliceAddress, bobClinicAddress); + + await bobToken.methods + .transfer_private(bobClinicAddress, 50n) + .send({ from: aliceAddress }); + await getBalances(bobToken, aliceAddress, bobClinicAddress); + + await bobToken.methods.private_to_public(10n).send({ from: aliceAddress }); + await getBalances(bobToken, aliceAddress, bobClinicAddress); + + await bobToken.methods + .mint_private(aliceAddress, 100n) + .send({ from: giggleAddress }); + await getBalances(bobToken, aliceAddress, bobClinicAddress); +} + +main().catch(console.error); +// docs:end:checkpoint_2 diff --git a/docs/lines/v6/examples/ts/bob_token_contract/yarn.lock b/docs/lines/v6/examples/ts/bob_token_contract/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/example_swap/config.yaml b/docs/lines/v6/examples/ts/example_swap/config.yaml new file mode 100644 index 00000000000..463a6e01e32 --- /dev/null +++ b/docs/lines/v6/examples/ts/example_swap/config.yaml @@ -0,0 +1,14 @@ +# Configuration for example_swap + +contracts: + - example_uniswap-ExampleUniswap + +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/stdlib" + - "@aztec-labs/foundation" + - "@aztec-labs/ethereum" + - "@aztec-labs/noir-contracts.js" + - "npm:@aztec/viem@2.38.2" diff --git a/docs/lines/v6/examples/ts/example_swap/index.ts b/docs/lines/v6/examples/ts/example_swap/index.ts new file mode 100644 index 00000000000..41c55834b82 --- /dev/null +++ b/docs/lines/v6/examples/ts/example_swap/index.ts @@ -0,0 +1,658 @@ +// docs:start:setup +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; +import { AztecAddress, EthAddress } from "@aztec-labs/aztec.js/addresses"; +import { SetPublicAuthwitContractInteraction } from "@aztec-labs/aztec.js/authorization"; +import { Fr } from "@aztec-labs/aztec.js/fields"; +import { createAztecNodeClient, waitForNode } from "@aztec-labs/aztec.js/node"; +import { createExtendedL1Client } from "@aztec-labs/ethereum/client"; +import { deployL1Contract } from "@aztec-labs/ethereum/deploy-l1-contract"; +import { sha256ToField } from "@aztec-labs/foundation/crypto/sha256"; +import { TokenContract } from "@aztec-labs/noir-contracts.js/Token"; +import { TokenBridgeContract } from "@aztec-labs/noir-contracts.js/TokenBridge"; +import { + computeL2ToL1MessageHash, + computeSecretHash, +} from "@aztec-labs/stdlib/hash"; +import { createAztecNodeDebugClient } from "@aztec-labs/stdlib/interfaces/client"; +import { decodeEventLog, encodeFunctionData, pad } from "@aztec/viem"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { foundry } from "@aztec/viem/chains"; +import ExampleERC20 from "../../../target/solidity/example_swap/ExampleERC20.sol/ExampleERC20.json" with { type: "json" }; +import ExampleTokenPortal from "../../../target/solidity/example_swap/ExampleTokenPortal.sol/ExampleTokenPortal.json" with { type: "json" }; +import ExampleUniswapPortal from "../../../target/solidity/example_swap/ExampleUniswapPortal.sol/ExampleUniswapPortal.json" with { type: "json" }; +import { ExampleUniswapContract } from "./artifacts/ExampleUniswap.js"; + +// Setup L1 client +const MNEMONIC = "test test test test test test test test test test test junk"; +const l1RpcUrl = process.env.ETHEREUM_HOST ?? "http://localhost:8545"; +const l1Client = createExtendedL1Client([l1RpcUrl], MNEMONIC); +const ownerEthAddress = l1Client.account.address; + +// Setup L2 client +console.log("Setting up L2...\n"); +const nodeUrl = process.env.AZTEC_NODE_URL ?? "http://localhost:8080"; +const node = createAztecNodeClient(nodeUrl); +await waitForNode(node); +const wallet = await EmbeddedWallet.create(node, { ephemeral: true }); +const [accData] = await getInitialTestAccountsData(); +const account = await wallet.createSchnorrInitializerlessAccount( + accData.secret, + accData.salt, + accData.signingKey, +); +console.log(`Account: ${account.address.toString()}\n`); + +const nodeInfo = await node.getNodeInfo(); +const registryAddress = nodeInfo.l1ContractAddresses.registryAddress.toString(); +const inboxAddress = nodeInfo.l1ContractAddresses.inboxAddress.toString(); +// docs:end:setup + +// docs:start:deploy_l1 +console.log("Deploying L1 contracts...\n"); + +// Deploy two ERC20 tokens: WETH (input) and DAI (output) +const { address: wethAddress } = await deployL1Contract( + l1Client, + ExampleERC20.abi, + ExampleERC20.bytecode.object as `0x${string}`, + ["Wrapped Ether", "WETH"], +); + +const { address: daiAddress } = await deployL1Contract( + l1Client, + ExampleERC20.abi, + ExampleERC20.bytecode.object as `0x${string}`, + ["Dai Stablecoin", "DAI"], +); + +// Deploy two token portals (one per token) +const { address: wethPortalAddress } = await deployL1Contract( + l1Client, + ExampleTokenPortal.abi, + ExampleTokenPortal.bytecode.object as `0x${string}`, +); + +const { address: daiPortalAddress } = await deployL1Contract( + l1Client, + ExampleTokenPortal.abi, + ExampleTokenPortal.bytecode.object as `0x${string}`, +); + +// Deploy the uniswap portal +const { address: uniswapPortalAddress } = await deployL1Contract( + l1Client, + ExampleUniswapPortal.abi, + ExampleUniswapPortal.bytecode.object as `0x${string}`, +); + +console.log(`WETH: ${wethAddress}`); +console.log(`DAI: ${daiAddress}`); +console.log(`WETH Portal: ${wethPortalAddress}`); +console.log(`DAI Portal: ${daiPortalAddress}`); +console.log(`Uniswap Portal: ${uniswapPortalAddress}\n`); +// docs:end:deploy_l1 + +// docs:start:deploy_l2 +console.log("Deploying L2 contracts...\n"); + +// Deploy L2 tokens (using the standard TokenContract from @aztec-labs/noir-contracts.js) +const { contract: l2Weth } = await TokenContract.deploy( + wallet, + account.address, + "Wrapped Ether", + "WETH", + 18, +).send({ from: account.address }); + +const { contract: l2Dai } = await TokenContract.deploy( + wallet, + account.address, + "Dai Stablecoin", + "DAI", + 18, +).send({ from: account.address }); + +// Deploy L2 token bridges +const { contract: l2WethBridge } = await TokenBridgeContract.deploy( + wallet, + l2Weth.address, + wethPortalAddress, +).send({ from: account.address }); + +const { contract: l2DaiBridge } = await TokenBridgeContract.deploy( + wallet, + l2Dai.address, + daiPortalAddress, +).send({ from: account.address }); + +// Deploy L2 uniswap contract +const { contract: l2Uniswap } = await ExampleUniswapContract.deploy( + wallet, + EthAddress.fromString(uniswapPortalAddress.toString()), +).send({ from: account.address }); + +console.log(`L2 WETH: ${l2Weth.address}`); +console.log(`L2 DAI: ${l2Dai.address}`); +console.log(`L2 WETH Bridge: ${l2WethBridge.address}`); +console.log(`L2 DAI Bridge: ${l2DaiBridge.address}`); +console.log(`L2 Uniswap: ${l2Uniswap.address}\n`); +// docs:end:deploy_l2 + +// docs:start:initialize +console.log("Initializing contracts...\n"); + +// Make bridges minters on their respective tokens +await l2Weth.methods + .set_minter(l2WethBridge.address, true) + .send({ from: account.address }); +await l2Dai.methods + .set_minter(l2DaiBridge.address, true) + .send({ from: account.address }); + +// Initialize L1 portals with registry, underlying token, and L2 bridge addresses +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const initWethPortal = await l1Client.writeContract({ + address: wethPortalAddress.toString() as `0x${string}`, + abi: ExampleTokenPortal.abi, + functionName: "initialize", + args: [ + registryAddress, + wethAddress.toString(), + l2WethBridge.address.toString(), + ], +}); +await l1Client.waitForTransactionReceipt({ hash: initWethPortal }); + +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const initDaiPortal = await l1Client.writeContract({ + address: daiPortalAddress.toString() as `0x${string}`, + abi: ExampleTokenPortal.abi, + functionName: "initialize", + args: [ + registryAddress, + daiAddress.toString(), + l2DaiBridge.address.toString(), + ], +}); +await l1Client.waitForTransactionReceipt({ hash: initDaiPortal }); + +// Initialize uniswap portal +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const initUniswapPortal = await l1Client.writeContract({ + address: uniswapPortalAddress.toString() as `0x${string}`, + abi: ExampleUniswapPortal.abi, + functionName: "initialize", + args: [registryAddress, l2Uniswap.address.toString()], +}); +await l1Client.waitForTransactionReceipt({ hash: initUniswapPortal }); + +console.log("All contracts initialized\n"); +// docs:end:initialize + +// docs:start:fund +console.log("Funding accounts...\n"); + +const SWAP_AMOUNT = 100n * 10n ** 18n; // 100 tokens + +// Mint WETH on L1 for the user +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const mintWethHash = await l1Client.writeContract({ + address: wethAddress.toString() as `0x${string}`, + abi: ExampleERC20.abi, + functionName: "mint", + args: [ownerEthAddress, SWAP_AMOUNT], +}); +await l1Client.waitForTransactionReceipt({ hash: mintWethHash }); + +// Pre-fund the uniswap portal with DAI (for the mock 1:1 swap) +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const mintDaiHash = await l1Client.writeContract({ + address: daiAddress.toString() as `0x${string}`, + abi: ExampleERC20.abi, + functionName: "mint", + args: [uniswapPortalAddress.toString(), SWAP_AMOUNT * 2n], +}); +await l1Client.waitForTransactionReceipt({ hash: mintDaiHash }); + +console.log(`Minted ${SWAP_AMOUNT} WETH to user`); +console.log(`Pre-funded uniswap portal with ${SWAP_AMOUNT * 2n} DAI\n`); +// docs:end:fund + +// docs:start:deposit_to_l2 +console.log("Depositing WETH to Aztec (L1 -> L2)...\n"); + +const depositSecret = Fr.random(); +const depositSecretHash = await computeSecretHash(depositSecret); + +// Approve WETH portal to take tokens +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const approveHash = await l1Client.writeContract({ + address: wethAddress.toString() as `0x${string}`, + abi: ExampleERC20.abi, + functionName: "approve", + args: [wethPortalAddress.toString(), SWAP_AMOUNT], +}); +await l1Client.waitForTransactionReceipt({ hash: approveHash }); + +// Deposit to Aztec publicly +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const depositHash = await l1Client.writeContract({ + address: wethPortalAddress.toString() as `0x${string}`, + abi: ExampleTokenPortal.abi, + functionName: "depositToAztecPublic", + args: [ + account.address.toString(), + SWAP_AMOUNT, + pad(depositSecretHash.toString() as `0x${string}`, { + dir: "left", + size: 32, + }), + ], +}); +const depositReceipt = await l1Client.waitForTransactionReceipt({ + hash: depositHash, +}); + +// Extract message leaf index from Inbox event +const INBOX_ABI = [ + { + type: "event", + name: "MessageSent", + inputs: [ + { name: "hash", type: "bytes32", indexed: true }, + { name: "inboxRollingHash", type: "bytes32", indexed: false }, + { name: "bucketSeq", type: "uint256", indexed: false }, + { + name: "message", + type: "tuple", + indexed: false, + components: [ + { + name: "sender", + type: "tuple", + components: [ + { name: "actor", type: "address" }, + { name: "chainId", type: "uint256" }, + ], + }, + { + name: "recipient", + type: "tuple", + components: [ + { name: "actor", type: "bytes32" }, + { name: "version", type: "uint256" }, + ], + }, + { name: "content", type: "bytes32" }, + { name: "secretHash", type: "bytes32" }, + { name: "index", type: "uint256" }, + ], + }, + ], + }, +] as const; + +const messageSentLogs = depositReceipt.logs + .filter((log) => log.address.toLowerCase() === inboxAddress.toLowerCase()) + .map((log: any) => { + try { + const decoded = decodeEventLog({ + abi: INBOX_ABI, + data: log.data, + topics: log.topics, + }); + return { log, decoded }; + } catch { + return null; + } + }) + .filter( + (item): item is { log: any; decoded: any } => + item !== null && (item.decoded as any).eventName === "MessageSent", + ); + +if (messageSentLogs.length === 0) { + throw new Error("No MessageSent events found in deposit transaction"); +} +const depositLeafIndex = new Fr(messageSentLogs[0].decoded.args.message.index); +console.log(`Deposit message leaf index: ${depositLeafIndex}\n`); +// docs:end:deposit_to_l2 + +// docs:start:mine_blocks +// Utility: mine 2 blocks (required before L1->L2 messages can be consumed) +async function mine2Blocks( + wallet: EmbeddedWallet, + accountAddress: AztecAddress, +) { + await TokenContract.deploy(wallet, accountAddress, "T", "T", 18).send({ + from: accountAddress, + }); + await TokenContract.deploy(wallet, accountAddress, "T", "T", 18).send({ + from: accountAddress, + }); +} +// docs:end:mine_blocks + +// docs:start:claim_on_l2 +console.log("Claiming WETH on L2...\n"); + +await mine2Blocks(wallet, account.address); + +await l2WethBridge.methods + .claim_public(account.address, SWAP_AMOUNT, depositSecret, depositLeafIndex) + .send({ from: account.address }); + +const { result: wethBalanceBefore } = await l2Weth.methods + .balance_of_public(account.address) + .simulate({ from: account.address }); +console.log(`L2 WETH balance after claim: ${wethBalanceBefore}\n`); +if (wethBalanceBefore !== SWAP_AMOUNT) { + throw new Error( + `Expected WETH balance ${SWAP_AMOUNT}, got ${wethBalanceBefore}`, + ); +} +console.log("✓ WETH claimed successfully on L2\n"); +// docs:end:claim_on_l2 + +// docs:start:public_swap +console.log("=== PUBLIC SWAP FLOW ===\n"); +console.log("Initiating public swap on L2 (WETH -> DAI)...\n"); + +// Force L2 block production so the claim message is included in a block before the swap +await mine2Blocks(wallet, account.address); + +const swapSecret = Fr.random(); +const swapSecretHash = await computeSecretHash(swapSecret); + +// Create authwit for the uniswap contract to transfer WETH on our behalf +const transferAction = l2Weth.methods.transfer_in_public( + account.address, + l2Uniswap.address, + SWAP_AMOUNT, + 0xdeadbeefn, +); +const authwit = await SetPublicAuthwitContractInteraction.create( + wallet, + account.address, + { caller: l2Uniswap.address, action: transferAction }, + true, +); +await authwit.send(); + +// Call swap_public on the L2 uniswap contract +const { receipt: swapReceipt } = await l2Uniswap.methods + .swap_public( + account.address, + l2WethBridge.address, + SWAP_AMOUNT, + l2DaiBridge.address, + 3000n, // fee tier + 0n, // minimum output + account.address, // recipient + swapSecretHash, + ) + .send({ from: account.address }); + +console.log(`Swap tx sent (block: ${swapReceipt.blockNumber})\n`); + +// Verify WETH was spent (balance should be 0 after swap) +const { result: wethAfterSwap } = await l2Weth.methods + .balance_of_public(account.address) + .simulate({ from: account.address }); +if (wethAfterSwap !== 0n) { + throw new Error(`Expected WETH balance 0 after swap, got ${wethAfterSwap}`); +} +console.log("✓ WETH transferred to bridge for swap\n"); +// docs:end:public_swap + +// docs:start:wait_for_proof +const isLocalNetwork = + nodeUrl.includes("localhost") || + nodeUrl.includes("127.0.0.1") || + nodeUrl.includes("local-network"); +const nodeDebug = isLocalNetwork + ? createAztecNodeDebugClient(nodeUrl) + : undefined; + +console.log("Waiting for block to be proven...\n"); + +let provenBlockNumber = await node.getBlockNumber("proven"); +while (provenBlockNumber < swapReceipt.blockNumber!) { + console.log( + ` Waiting... (proven: ${provenBlockNumber}, needed: ${swapReceipt.blockNumber})`, + ); + if (nodeDebug) { + await nodeDebug.mineBlock(); + } + await new Promise((resolve) => setTimeout(resolve, 10000)); + provenBlockNumber = await node.getBlockNumber("proven"); +} + +console.log("Block proven!\n"); +// docs:end:wait_for_proof + +// docs:start:consume_l1_messages_setup +console.log("Consuming L2->L1 messages on L1...\n"); + +// The swap generates 2 L2->L1 messages: +// 1. Token bridge exit (withdraw WETH to uniswap portal) +// 2. Uniswap swap intent + +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const portalRollupVersion = (await l1Client.readContract({ + address: wethPortalAddress.toString() as `0x${string}`, + abi: ExampleTokenPortal.abi, + functionName: "rollupVersion", +})) as bigint; + +// Compute message 1: token bridge exit +// Encode using the same approach as Solidity's abi.encodeWithSignature("withdraw(address,uint256,address)", ...) +const withdrawContentEncoded = encodeFunctionData({ + abi: [ + { + name: "withdraw", + type: "function", + inputs: [ + { name: "", type: "address" }, + { name: "", type: "uint256" }, + { name: "", type: "address" }, + ], + outputs: [], + }, + ], + args: [ + uniswapPortalAddress.toString() as `0x${string}`, + SWAP_AMOUNT, + uniswapPortalAddress.toString() as `0x${string}`, + ], +}); +const withdrawContentHash = sha256ToField([ + Buffer.from(withdrawContentEncoded.slice(2), "hex"), +]); + +// Message 1: Token bridge exit message +const exitMsgLeaf = computeL2ToL1MessageHash({ + l2Sender: l2WethBridge.address, + l1Recipient: wethPortalAddress, + content: withdrawContentHash, + rollupVersion: new Fr(portalRollupVersion), + chainId: new Fr(foundry.id), +}); +// docs:end:consume_l1_messages_setup + +// docs:start:consume_l1_messages_witnesses +// The node picks the smallest partial-proof root that covers each tx's checkpoint. +const waitForL2ToL1MembershipWitness = async ( + messageName: string, + messageLeaf: Fr, +) => { + const maxAttempts = 30; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const witness = await node.getL2ToL1MembershipWitness( + swapReceipt.txHash, + messageLeaf, + ); + if (witness) { + return witness; + } + + console.log( + ` Waiting for ${messageName} L2->L1 witness (${attempt}/${maxAttempts})...`, + ); + await new Promise((resolve) => setTimeout(resolve, 10000)); + } + + throw new Error(`Timed out waiting for ${messageName} L2->L1 witness`); +}; + +const exitWitness = await waitForL2ToL1MembershipWitness( + "token bridge exit", + exitMsgLeaf, +); +const exitSiblingPath = exitWitness.siblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString("hex")}` as `0x${string}`); + +// Message 2: Uniswap swap intent message +// Compute using the same encoding as ExampleUniswapPortal.sol +const swapContentEncoded = encodeFunctionData({ + abi: [ + { + name: "swap_public", + type: "function", + inputs: [ + { name: "", type: "address" }, + { name: "", type: "uint256" }, + { name: "", type: "uint24" }, + { name: "", type: "address" }, + { name: "", type: "uint256" }, + { name: "", type: "bytes32" }, + { name: "", type: "bytes32" }, + ], + outputs: [], + }, + ], + args: [ + wethPortalAddress.toString() as `0x${string}`, + SWAP_AMOUNT, + 3000, + daiPortalAddress.toString() as `0x${string}`, + 0n, + account.address.toString() as `0x${string}`, + pad(swapSecretHash.toString() as `0x${string}`, { + dir: "left", + size: 32, + }), + ], +}); + +const swapContentHash = sha256ToField([ + Buffer.from(swapContentEncoded.slice(2), "hex"), +]); + +const swapMsgLeaf = computeL2ToL1MessageHash({ + l2Sender: l2Uniswap.address, + l1Recipient: uniswapPortalAddress, + content: swapContentHash, + rollupVersion: new Fr(portalRollupVersion), + chainId: new Fr(foundry.id), +}); + +const swapWitness = await waitForL2ToL1MembershipWitness( + "swap intent", + swapMsgLeaf, +); +const swapSiblingPath = swapWitness.siblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString("hex")}` as `0x${string}`); +// docs:end:consume_l1_messages_witnesses + +// docs:start:consume_l1_messages_execute +// Execute the swap on L1 (consumes both messages) +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const l1SwapHash = await l1Client.writeContract({ + address: uniswapPortalAddress.toString() as `0x${string}`, + abi: ExampleUniswapPortal.abi, + functionName: "swapPublic", + args: [ + wethPortalAddress.toString(), + SWAP_AMOUNT, + 3000, + daiPortalAddress.toString(), + 0n, + account.address.toString(), + pad(swapSecretHash.toString() as `0x${string}`, { + dir: "left", + size: 32, + }), + [BigInt(exitWitness.epochNumber), BigInt(swapWitness.epochNumber)], + [ + BigInt(exitWitness.numCheckpointsInEpoch), + BigInt(swapWitness.numCheckpointsInEpoch), + ], + [BigInt(exitWitness.leafIndex), BigInt(swapWitness.leafIndex)], + [exitSiblingPath, swapSiblingPath], + ], +}); +const l1SwapReceipt = await l1Client.waitForTransactionReceipt({ + hash: l1SwapHash, +}); +console.log(`L1 swap executed! Tx: ${l1SwapHash}\n`); +// docs:end:consume_l1_messages_execute + +// docs:start:claim_output +console.log("Claiming DAI output on L2...\n"); + +// Extract the deposit message leaf index from the L1 swap receipt +const daiDepositLogs = l1SwapReceipt.logs + .filter((log) => log.address.toLowerCase() === inboxAddress.toLowerCase()) + .map((log: any) => { + try { + const decoded = decodeEventLog({ + abi: INBOX_ABI, + data: log.data, + topics: log.topics, + }); + return { log, decoded }; + } catch { + return null; + } + }) + .filter( + (item): item is { log: any; decoded: any } => + item !== null && (item.decoded as any).eventName === "MessageSent", + ); + +if (daiDepositLogs.length === 0) { + throw new Error("No MessageSent events found in L1 swap transaction"); +} +const daiDepositLeafIndex = new Fr( + daiDepositLogs[0].decoded.args.message.index, +); + +// Mine blocks and claim +await mine2Blocks(wallet, account.address); + +await l2DaiBridge.methods + .claim_public(account.address, SWAP_AMOUNT, swapSecret, daiDepositLeafIndex) + .send({ from: account.address }); + +const { result: daiBalance } = await l2Dai.methods + .balance_of_public(account.address) + .simulate({ from: account.address }); + +const { result: wethBalanceAfter } = await l2Weth.methods + .balance_of_public(account.address) + .simulate({ from: account.address }); + +console.log(`L2 WETH balance: ${wethBalanceAfter}`); +console.log(`L2 DAI balance: ${daiBalance}`); + +if (wethBalanceAfter !== 0n) { + throw new Error(`Expected final WETH balance 0, got ${wethBalanceAfter}`); +} +if (daiBalance !== SWAP_AMOUNT) { + throw new Error(`Expected DAI balance ${SWAP_AMOUNT}, got ${daiBalance}`); +} +console.log("\n✓ All checks passed — public swap complete!\n"); +// docs:end:claim_output diff --git a/docs/lines/v6/examples/ts/example_swap/yarn.lock b/docs/lines/v6/examples/ts/example_swap/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/lib.sh b/docs/lines/v6/examples/ts/lib.sh new file mode 100644 index 00000000000..804fef63742 --- /dev/null +++ b/docs/lines/v6/examples/ts/lib.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Shared utilities for docs/examples/ts scripts. +# Source this file — it has no shebang-executable purpose on its own. + +# parse_dependencies <config_file> +# +# Reads a config.yaml via yq and classifies each dependency entry into one of +# two global arrays: +# PINNED_DEPS — @aztec-labs/* at $AZTEC_VERSION and @aztec-foundation/* at $BB_VERSION, the versions +# docs/aztec-node.json pins (`node docs/scripts/aztec_node.mts env` exports both) +# NPM_DEPS — npm: packages as written (npm:@aztec/viem@2.38.2 -> @aztec/viem@2.38.2) +# +# Also sets PARSED_DEPS_FOUND to "true" if any dependency was found, "false" otherwise. +parse_dependencies() { + local config_file=$1 + local fix="eval the output of node docs/scripts/aztec_node.mts env first" + : "${AZTEC_VERSION:?$fix}" "${BB_VERSION:?$fix}" + + PINNED_DEPS=() + NPM_DEPS=() + PARSED_DEPS_FOUND=false + + local pkg + while IFS= read -r pkg; do + PARSED_DEPS_FOUND=true + # Remove quotes and whitespace + pkg="${pkg//\"/}" + pkg="${pkg#"${pkg%%[![:space:]]*}"}" # ltrim + pkg="${pkg%"${pkg##*[![:space:]]}"}" # rtrim + + if [ -z "$pkg" ]; then + continue + fi + + if [[ "$pkg" =~ ^npm: ]]; then + NPM_DEPS+=("${pkg#npm:}") + elif [[ "$pkg" == @aztec-labs/* ]]; then + PINNED_DEPS+=("${pkg}@${AZTEC_VERSION}") + elif [[ "$pkg" == @aztec-foundation/* ]]; then + PINNED_DEPS+=("${pkg}@${BB_VERSION}") + else + echo "Warning: Unknown dependency format '$pkg' (use '@aztec-labs/pkg', '@aztec-foundation/pkg', or 'npm:pkg')" >&2 + fi + done < <(yq eval '.dependencies[]' "$config_file") +} diff --git a/docs/lines/v6/examples/ts/recursive_verification/config.yaml b/docs/lines/v6/examples/ts/recursive_verification/config.yaml new file mode 100644 index 00000000000..2ec1c30a1f1 --- /dev/null +++ b/docs/lines/v6/examples/ts/recursive_verification/config.yaml @@ -0,0 +1,22 @@ +# Configuration for recursive_verification example + +# Setup script to run before index.ts (generates proof data) +setup: "node --preserve-symlinks --preserve-symlinks-main --import tsx/esm scripts/generate_data.ts" + +# Contract artifacts to import for codegen (names without .json extension) +# These must already be compiled to docs/target/ directory +contracts: + - recursive_verification_contract-ValueNotEqual + +# Dependencies: +# - @aztec-labs/* and @aztec-foundation/* packages install at the versions docs/aztec-node.json pins +# - Use npm:package-name or npm:package-name@range for external npm packages +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/kv-store" + - "@aztec-labs/pxe" + - "@aztec-labs/noir-contracts.js" + - "@aztec-foundation/bb.js" + - "@aztec-foundation/noir-noir_js" diff --git a/docs/lines/v6/examples/ts/recursive_verification/index.ts b/docs/lines/v6/examples/ts/recursive_verification/index.ts new file mode 100644 index 00000000000..55b3d3918a9 --- /dev/null +++ b/docs/lines/v6/examples/ts/recursive_verification/index.ts @@ -0,0 +1,125 @@ +// docs:start:run_recursion +import { SponsoredFeePaymentMethod } from "@aztec-labs/aztec.js/fee"; +import type { FieldLike } from "@aztec-labs/aztec.js/abi"; +import { getSponsoredFPCInstance } from "./scripts/sponsored_fpc.js"; +import { SponsoredFPCContract } from "@aztec-labs/noir-contracts.js/SponsoredFPC"; +import { ValueNotEqualContract } from "./artifacts/ValueNotEqual.js"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { NO_FROM } from "@aztec-labs/aztec.js/account"; +import { Fr, GrumpkinScalar } from "@aztec-labs/aztec.js/fields"; +import assert from "node:assert"; +import fs from "node:fs"; + +if (!fs.existsSync("data.json")) { + console.error( + "data.json not found. Run 'yarn data' first to generate proof data.", + ); + process.exit(1); +} +const data = JSON.parse(fs.readFileSync("data.json", "utf-8")); + +export const NODE_URL = process.env.AZTEC_NODE_URL ?? "http://localhost:8080"; + +// Setup sponsored fee payment - the FPC pays transaction fees for us +const sponsoredFPC = await getSponsoredFPCInstance(); +const sponsoredPaymentMethod = new SponsoredFeePaymentMethod( + sponsoredFPC.address, +); + +// Initialize wallet and connect to local network +// The wallet manages accounts and sends transactions through the PXE +export const setupWallet = async (): Promise<EmbeddedWallet> => { + try { + // Create wallet with embedded PXE + // The wallet manages accounts and connects to the node + let wallet = await EmbeddedWallet.create(NODE_URL, { ephemeral: true }); + + // Register the sponsored FPC so the wallet knows about it + await wallet.registerContract(sponsoredFPC, SponsoredFPCContract.artifact); + return wallet; + } catch (error) { + console.error("Failed to setup local network:", error); + throw error; + } +}; + +async function main() { + // Step 1: Setup wallet and create account + // Accounts in Aztec are smart contracts (account abstraction) + const wallet = await setupWallet(); + const manager = await wallet.createSchnorrAccount( + Fr.random(), + Fr.random(), + GrumpkinScalar.random(), + ); + + // Deploy the account contract + const deployMethod = await manager.getDeployMethod(); + await deployMethod.send({ + from: NO_FROM, + fee: { paymentMethod: sponsoredPaymentMethod }, + }); + + const accounts = await wallet.getAccounts(); + + // Step 2: Deploy ValueNotEqual contract + // Constructor args: initial counter (10), owner, VK hash + const { contract: valueNotEqual } = await ValueNotEqualContract.deploy( + wallet, + 10, // Initial counter value + accounts[0].item, // Owner address + data.vkHash as unknown as FieldLike, // VK hash for verification + ).send({ + from: accounts[0].item, + fee: { paymentMethod: sponsoredPaymentMethod }, + }); + + console.log(`Contract deployed at: ${valueNotEqual.address}`); + + const opts = { + from: accounts[0].item, + fee: { paymentMethod: sponsoredPaymentMethod }, + }; + + // Step 3: Read initial counter value + // simulate() executes without submitting a transaction + let counterValue = ( + await valueNotEqual.methods + .get_counter(accounts[0].item) + .simulate({ from: accounts[0].item }) + ).result; + console.log(`Counter value: ${counterValue}`); // Should be 10 + + // Step 4: Call increment() with proof data + // This creates a transaction that: + // 1. Executes the private increment() function (client-side) + // 2. Generates a ZK proof of correct execution + // 3. Submits the proof to the network + // 4. Network verifies the proof + // 5. Executes enqueued _increment_public() + const interaction = await valueNotEqual.methods.increment( + accounts[0].item, + data.vkAsFields as unknown as FieldLike[], // 115 field VK + data.proofAsFields as unknown as FieldLike[], // 458 field proof + data.publicInputs as unknown as FieldLike[], // Public inputs + ); + + // Step 5: Send transaction and wait for inclusion + await interaction.send(opts); + + // Step 6: Read updated counter + counterValue = ( + await valueNotEqual.methods + .get_counter(accounts[0].item) + .simulate({ from: accounts[0].item }) + ).result; + console.log(`Counter value: ${counterValue}`); // Should be 11 + + assert(counterValue === 11n, "Counter should be 11 after verification"); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); +// docs:end:run_recursion diff --git a/docs/lines/v6/examples/ts/recursive_verification/scripts/generate_data.ts b/docs/lines/v6/examples/ts/recursive_verification/scripts/generate_data.ts new file mode 100644 index 00000000000..9926acb3580 --- /dev/null +++ b/docs/lines/v6/examples/ts/recursive_verification/scripts/generate_data.ts @@ -0,0 +1,74 @@ +import circuitJson from "../../../../target/hello_circuit.json" with { type: "json" }; +// docs:start:generate_data +import { Noir } from "@aztec-foundation/noir-noir_js"; +import { Barretenberg, UltraHonkBackend, deflattenFields } from "@aztec-foundation/bb.js"; +import fs from "fs"; +import { exit } from "process"; + +// Step 1: Initialize Barretenberg API (the proving system backend) +// Barretenberg is the C++ library that implements UltraHonk +// threads: 1 uses single-threaded mode (increase for faster proofs on multi-core machines) +const barretenbergAPI = await Barretenberg.new({ threads: 1 }); + +// Step 2: Create Noir circuit instance from compiled bytecode +// This loads the circuit definition so we can execute it +const helloWorld = new Noir(circuitJson as any); + +// Step 3: Execute circuit with inputs to generate witness +// The witness is all intermediate values computed during circuit execution +// x=1 (private), y=2 (public) - proves that 1 != 2 +const { witness: mainWitness } = await helloWorld.execute({ x: 1, y: 2 }); + +// Step 4: Create UltraHonk backend with circuit bytecode +// The backend handles proof generation and verification +const mainBackend = new UltraHonkBackend(circuitJson.bytecode, barretenbergAPI); + +// Step 5: Generate proof targeting the noir-recursive verifier +// verifierTarget: 'noir-recursive' creates a proof format suitable for +// verification inside another Noir circuit (which is what Aztec contracts are) +const mainProofData = await mainBackend.generateProof(mainWitness, { + verifierTarget: "noir-recursive", +}); + +// Step 6: Verify proof locally before saving +// This catches errors early - if verification fails here, it will fail onchain too +const isValid = await mainBackend.verifyProof(mainProofData, { + verifierTarget: "noir-recursive", +}); +console.log(`Proof verification: ${isValid ? "SUCCESS" : "FAILED"}`); + +// Step 7: Generate recursive artifacts for onchain use +// This converts the proof and VK into field element arrays that can be +// passed to the Aztec contract +const recursiveArtifacts = await mainBackend.generateRecursiveProofArtifacts( + mainProofData.proof, + mainProofData.publicInputs.length, +); + +// Step 8: Convert proof to field elements if needed +// Some versions return empty proofAsFields, requiring manual conversion +let proofAsFields = recursiveArtifacts.proofAsFields; +if (proofAsFields.length === 0) { + console.log("Using deflattenFields to convert proof..."); + proofAsFields = deflattenFields(mainProofData.proof).map((f) => f.toString()); +} + +const vkAsFields = recursiveArtifacts.vkAsFields; + +console.log(`VK size: ${vkAsFields.length}`); // Should be 115 +console.log(`Proof size: ${proofAsFields.length}`); // Should be 458 +console.log(`Public inputs: ${mainProofData.publicInputs.length}`); // Should be 1 + +// Step 9: Save all data to JSON for contract interaction +const data = { + vkAsFields: vkAsFields, // 115 field elements - the verification key + vkHash: recursiveArtifacts.vkHash, // Hash of VK - stored in contract + proofAsFields: proofAsFields, // 458 field elements - the proof + publicInputs: mainProofData.publicInputs.map((p: string) => p.toString()), +}; + +fs.writeFileSync("data.json", JSON.stringify(data, null, 2)); +await barretenbergAPI.destroy(); +console.log("Done"); +exit(); +// docs:end:generate_data diff --git a/docs/lines/v6/examples/ts/recursive_verification/scripts/sponsored_fpc.ts b/docs/lines/v6/examples/ts/recursive_verification/scripts/sponsored_fpc.ts new file mode 100644 index 00000000000..29b052a8b23 --- /dev/null +++ b/docs/lines/v6/examples/ts/recursive_verification/scripts/sponsored_fpc.ts @@ -0,0 +1,16 @@ +// docs:start:sponsored_fpc +import { getContractInstanceFromInstantiationParams } from "@aztec-labs/aztec.js/contracts"; +import { Fr } from "@aztec-labs/aztec.js/fields"; +import { SponsoredFPCContract } from "@aztec-labs/noir-contracts.js/SponsoredFPC"; + +const SPONSORED_FPC_SALT = new Fr(BigInt(0)); + +export async function getSponsoredFPCInstance() { + return await getContractInstanceFromInstantiationParams( + SponsoredFPCContract.artifact, + { + salt: SPONSORED_FPC_SALT, + }, + ); +} +// docs:end:sponsored_fpc diff --git a/docs/lines/v6/examples/ts/recursive_verification/yarn.lock b/docs/lines/v6/examples/ts/recursive_verification/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/token_bridge/config.yaml b/docs/lines/v6/examples/ts/token_bridge/config.yaml new file mode 100644 index 00000000000..81a67d08aae --- /dev/null +++ b/docs/lines/v6/examples/ts/token_bridge/config.yaml @@ -0,0 +1,19 @@ +# Configuration for token_bridge example + +# Contract artifacts to import for codegen (names without .json extension) +# These must already be compiled to docs/target/ directory +contracts: + - nft-NFTPunk + - nft_bridge-NFTBridge + +# Dependencies: +# - @aztec-labs/* and @aztec-foundation/* packages install at the versions docs/aztec-node.json pins +# - Use npm:package-name for external npm packages +dependencies: + - "@aztec-labs/aztec.js" + - "@aztec-labs/accounts" + - "@aztec-labs/wallets" + - "@aztec-labs/stdlib" + - "@aztec-labs/foundation" + - "@aztec-labs/ethereum" + - "npm:@aztec/viem@2.38.2" diff --git a/docs/lines/v6/examples/ts/token_bridge/index.ts b/docs/lines/v6/examples/ts/token_bridge/index.ts new file mode 100644 index 00000000000..af000adc054 --- /dev/null +++ b/docs/lines/v6/examples/ts/token_bridge/index.ts @@ -0,0 +1,363 @@ +// docs:start:setup +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; +import { AztecAddress, EthAddress } from "@aztec-labs/aztec.js/addresses"; +import { Fr } from "@aztec-labs/aztec.js/fields"; +import { createAztecNodeClient } from "@aztec-labs/aztec.js/node"; +import { createExtendedL1Client } from "@aztec-labs/ethereum/client"; +import { deployL1Contract } from "@aztec-labs/ethereum/deploy-l1-contract"; +import { sha256ToField } from "@aztec-labs/foundation/crypto/sha256"; +import { + computeL2ToL1MessageHash, + computeSecretHash, +} from "@aztec-labs/stdlib/hash"; +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { decodeEventLog, pad } from "@aztec/viem"; +import { foundry } from "@aztec/viem/chains"; +import NFTPortal from "../../../target/solidity/nft_bridge/NFTPortal.sol/NFTPortal.json" with { type: "json" }; +import SimpleNFT from "../../../target/solidity/nft_bridge/SimpleNFT.sol/SimpleNFT.json" with { type: "json" }; +import { NFTBridgeContract } from "./artifacts/NFTBridge.js"; +import { NFTPunkContract } from "./artifacts/NFTPunk.js"; + +// Setup L1 client using anvil's default mnemonic (same as e2e tests) +const MNEMONIC = "test test test test test test test test test test test junk"; +const l1Client = createExtendedL1Client(["http://localhost:8545"], MNEMONIC); +const ownerEthAddress = l1Client.account.address; + +// Setup L2 using Aztec's local network and one of its initial accounts +console.log("Setting up L2...\n"); +const node = createAztecNodeClient("http://localhost:8080"); +const aztecWallet = await EmbeddedWallet.create(node); +const [accData] = await getInitialTestAccountsData(); +const account = await aztecWallet.createSchnorrInitializerlessAccount( + accData.secret, + accData.salt, + accData.signingKey, +); +console.log(`Account: ${account.address.toString()}\n`); + +// Get node info +const nodeInfo = await node.getNodeInfo(); +const registryAddress = nodeInfo.l1ContractAddresses.registryAddress.toString(); +const inboxAddress = nodeInfo.l1ContractAddresses.inboxAddress.toString(); +// docs:end:setup + +// docs:start:deploy_l1_contracts +console.log("Deploying L1 contracts...\n"); + +const { address: nftAddress } = await deployL1Contract( + l1Client, + SimpleNFT.abi, + SimpleNFT.bytecode.object as `0x${string}`, +); + +const { address: portalAddress } = await deployL1Contract( + l1Client, + NFTPortal.abi, + NFTPortal.bytecode.object as `0x${string}`, +); + +console.log(`SimpleNFT: ${nftAddress}`); +console.log(`NFTPortal: ${portalAddress}\n`); +// docs:end:deploy_l1_contracts + +// docs:start:deploy_l2_contracts +console.log("Deploying L2 contracts...\n"); + +const { contract: l2Nft } = await NFTPunkContract.deploy( + aztecWallet, + account.address, +).send({ + from: account.address, +}); + +const { contract: l2Bridge } = await NFTBridgeContract.deploy( + aztecWallet, + l2Nft.address, +).send({ from: account.address }); + +console.log(`L2 NFT: ${l2Nft.address.toString()}`); +console.log(`L2 Bridge: ${l2Bridge.address.toString()}\n`); +// docs:end:deploy_l2_contracts + +// docs:start:initialize_portal +console.log("Initializing portal..."); + +// Initialize the portal contract +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const initHash = await l1Client.writeContract({ + address: portalAddress.toString() as `0x${string}`, + abi: NFTPortal.abi, + functionName: "initialize", + args: [registryAddress, nftAddress.toString(), l2Bridge.address.toString()], +}); +await l1Client.waitForTransactionReceipt({ hash: initHash }); + +console.log("Portal initialized\n"); +// docs:end:initialize_portal + +// docs:start:initialize_l2_bridge +console.log("Setting up L2 bridge..."); + +await l2Bridge.methods + .set_portal(EthAddress.fromString(portalAddress.toString())) + .send({ from: account.address }); + +await l2Nft.methods + .set_minter(l2Bridge.address) + .send({ from: account.address }); + +console.log("Bridge configured\n"); +// docs:end:initialize_l2_bridge + +// docs:start:mint_nft_l1 +console.log("Minting NFT on L1..."); + +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const mintHash = await l1Client.writeContract({ + address: nftAddress.toString() as `0x${string}`, + abi: SimpleNFT.abi, + functionName: "mint", + args: [ownerEthAddress], +}); +await l1Client.waitForTransactionReceipt({ hash: mintHash }); + +// no need to parse logs, this will be tokenId 0 since it's a fresh contract +const tokenId = 0n; + +console.log(`Minted tokenId: ${tokenId}\n`); +// docs:end:mint_nft_l1 + +// docs:start:deposit_to_aztec +console.log("Depositing NFT to Aztec..."); + +const secret = Fr.random(); +const secretHash = await computeSecretHash(secret); + +// Approve portal to transfer the NFT +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const approveHash = await l1Client.writeContract({ + address: nftAddress.toString() as `0x${string}`, + abi: SimpleNFT.abi, + functionName: "approve", + args: [portalAddress.toString(), tokenId], +}); +await l1Client.waitForTransactionReceipt({ hash: approveHash }); + +// Deposit to Aztec +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const depositHash = await l1Client.writeContract({ + address: portalAddress.toString() as `0x${string}`, + abi: NFTPortal.abi, + functionName: "depositToAztec", + args: [ + tokenId, + pad(secretHash.toString() as `0x${string}`, { dir: "left", size: 32 }), + ], +}); +const depositReceipt = await l1Client.waitForTransactionReceipt({ + hash: depositHash, +}); +// docs:end:deposit_to_aztec + +// docs:start:get_message_leaf_index +const INBOX_ABI = [ + { + type: "event", + name: "MessageSent", + inputs: [ + { name: "hash", type: "bytes32", indexed: true }, + { name: "inboxRollingHash", type: "bytes32", indexed: false }, + { name: "bucketSeq", type: "uint256", indexed: false }, + { + name: "message", + type: "tuple", + indexed: false, + components: [ + { + name: "sender", + type: "tuple", + components: [ + { name: "actor", type: "address" }, + { name: "chainId", type: "uint256" }, + ], + }, + { + name: "recipient", + type: "tuple", + components: [ + { name: "actor", type: "bytes32" }, + { name: "version", type: "uint256" }, + ], + }, + { name: "content", type: "bytes32" }, + { name: "secretHash", type: "bytes32" }, + { name: "index", type: "uint256" }, + ], + }, + ], + }, +] as const; + +// Find and decode the MessageSent event from the Inbox contract +const messageSentLogs = depositReceipt.logs + .filter((log) => log.address.toLowerCase() === inboxAddress.toLowerCase()) + .map((log: any) => { + try { + const decoded = decodeEventLog({ + abi: INBOX_ABI, + data: log.data, + topics: log.topics, + }); + return { log, decoded }; + } catch { + // Not a decodable event from this ABI + return null; + } + }) + .filter( + (item): item is { log: any; decoded: any } => + item !== null && (item.decoded as any).eventName === "MessageSent", + ); + +const messageLeafIndex = new Fr(messageSentLogs[0].decoded.args.message.index); +// docs:end:get_message_leaf_index + +// docs:start:mine_blocks +async function mine2Blocks( + aztecWallet: EmbeddedWallet, + accountAddress: AztecAddress, +) { + await NFTPunkContract.deploy(aztecWallet, accountAddress).send({ + from: accountAddress, + }); + await NFTPunkContract.deploy(aztecWallet, accountAddress).send({ + from: accountAddress, + }); +} +// docs:end:mine_blocks + +// docs:start:claim_on_l2 +// Mine blocks +await mine2Blocks(aztecWallet, account.address); + +// Check notes before claiming (should be 0) +console.log("Checking notes before claim..."); +const { result: notesBefore } = await l2Nft.methods + .notes_of(account.address) + .simulate({ from: account.address }); +console.log(` Notes count: ${notesBefore}`); + +console.log("Claiming NFT on L2..."); +await l2Bridge.methods + .claim(account.address, new Fr(Number(tokenId)), secret, messageLeafIndex) + .send({ from: account.address }); +console.log("NFT claimed on L2\n"); + +// Check notes after claiming (should be 1) +console.log("Checking notes after claim..."); +const { result: notesAfterClaim } = await l2Nft.methods + .notes_of(account.address) + .simulate({ from: account.address }); +console.log(` Notes count: ${notesAfterClaim}\n`); +// docs:end:claim_on_l2 + +// docs:start:exit_from_l2 +// L2 -> L1 flow +console.log("Exiting NFT from L2..."); +// Mine blocks, not necessary on devnet, but must wait for 2 blocks +await mine2Blocks(aztecWallet, account.address); + +const recipientEthAddress = EthAddress.fromString(ownerEthAddress); + +const { receipt: exitReceipt } = await l2Bridge.methods + .exit(new Fr(Number(tokenId)), recipientEthAddress) + .send({ from: account.address }); + +console.log(`Exit message sent (block: ${exitReceipt.blockNumber})\n`); + +// Check notes after burning (should be 0 again) +console.log("Checking notes after burn..."); +const { result: notesAfterBurn } = await l2Nft.methods + .notes_of(account.address) + .simulate({ from: account.address }); +console.log(` Notes count: ${notesAfterBurn}\n`); +// docs:end:exit_from_l2 + +// docs:start:get_withdrawal_witness +// Compute the message hash directly from known parameters +// This matches what the portal contract expects: Hash.sha256ToField(abi.encodePacked(tokenId, recipient)) +const tokenIdBuffer = new Fr(Number(tokenId)).toBuffer(); +const recipientBuffer = Buffer.from( + recipientEthAddress.toString().slice(2), + "hex", +); +const content = sha256ToField([tokenIdBuffer, recipientBuffer]); + +// Get rollup version from the portal contract (it stores it during initialize) +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const version = (await l1Client.readContract({ + address: portalAddress.toString() as `0x${string}`, + abi: NFTPortal.abi, + functionName: "rollupVersion", +})) as bigint; + +// Compute the L2->L1 message hash +const msgLeaf = computeL2ToL1MessageHash({ + l2Sender: l2Bridge.address, + l1Recipient: EthAddress.fromString(portalAddress.toString()), + content, + rollupVersion: new Fr(version), + chainId: new Fr(foundry.id), +}); + +// Wait for the block to be proven before withdrawing +// Waiting for the block to be proven is not necessary on the local network, but it is necessary on devnet +console.log("Waiting for block to be proven..."); +console.log(` Exit block number: ${exitReceipt.blockNumber}`); + +let provenBlockNumber = await node.getBlockNumber("proven"); +console.log(` Current proven block: ${provenBlockNumber}`); + +while (provenBlockNumber < exitReceipt.blockNumber!) { + console.log( + ` Waiting... (proven: ${provenBlockNumber}, needed: ${exitReceipt.blockNumber})`, + ); + await new Promise((resolve) => setTimeout(resolve, 10000)); // Wait 10 seconds + provenBlockNumber = await node.getBlockNumber("proven"); +} + +console.log("Block proven!\n"); + +// Compute the membership witness using the message hash and the L2 tx hash. +// The node picks the smallest partial-proof root that covers the tx's checkpoint. +const witness = await node.getL2ToL1MembershipWitness( + exitReceipt.txHash, + msgLeaf, +); +const epoch = witness!.epochNumber; +const numCheckpointsInEpoch = witness!.numCheckpointsInEpoch; +console.log(` Epoch for block ${exitReceipt.blockNumber}: ${epoch}`); + +const siblingPathHex = witness!.siblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString("hex")}` as `0x${string}`); +// docs:end:get_withdrawal_witness + +// docs:start:withdraw_on_l1 +console.log("Withdrawing NFT on L1..."); +// @ts-expect-error - viem type inference doesn't work with JSON-imported ABIs +const withdrawHash = await l1Client.writeContract({ + address: portalAddress.toString() as `0x${string}`, + abi: NFTPortal.abi, + functionName: "withdraw", + args: [ + tokenId, + BigInt(epoch), + BigInt(numCheckpointsInEpoch), + BigInt(witness!.leafIndex), + siblingPathHex, + ], +}); +await l1Client.waitForTransactionReceipt({ hash: withdrawHash }); +console.log("NFT withdrawn to L1\n"); +// docs:end:withdraw_on_l1 diff --git a/docs/lines/v6/examples/ts/token_bridge/yarn.lock b/docs/lines/v6/examples/ts/token_bridge/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/lines/v6/examples/ts/tsconfig.template.json b/docs/lines/v6/examples/ts/tsconfig.template.json new file mode 100644 index 00000000000..e55579242ad --- /dev/null +++ b/docs/lines/v6/examples/ts/tsconfig.template.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2022", + "lib": ["dom", "esnext", "es2017.object"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true + }, + "include": ["index.ts", "artifacts/**/*.ts"] +} diff --git a/docs/lines/v6/examples/webapp-tutorial/.env.example b/docs/lines/v6/examples/webapp-tutorial/.env.example new file mode 100644 index 00000000000..d448d14c2b2 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/.env.example @@ -0,0 +1 @@ +AZTEC_NODE_URL=http://localhost:8080 diff --git a/docs/lines/v6/examples/webapp-tutorial/.gitignore b/docs/lines/v6/examples/webapp-tutorial/.gitignore new file mode 100644 index 00000000000..cdb6d405497 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/.gitignore @@ -0,0 +1,13 @@ +# Generated contract artifacts (created by `yarn prep`) +src/artifacts/ + +# Extension build outputs (created by `yarn build:test-extension`) +test-extension/dist/ +test-extension/wasm/ + +# Build cache artifacts +codegenCache.json +*.tsbuildinfo + +# Lock file — CI regenerates with link: paths; external users generate via setup.sh +yarn.lock diff --git a/docs/lines/v6/examples/webapp-tutorial/README.md b/docs/lines/v6/examples/webapp-tutorial/README.md new file mode 100644 index 00000000000..94a1219276c --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/README.md @@ -0,0 +1,157 @@ +# Pod Racing Webapp Tutorial + +A sample Aztec webapp demonstrating wallet connection, contract deployment, and private state management. + +## Prerequisites + +- Node.js 22+ +- Yarn +- The Aztec CLI installed (`aztec` command available) + +## Quick Start + +### 1. Install dependencies + +```bash +yarn install +``` + +### 2. Compile the contract + +```bash +yarn compile +``` + +### 3. Generate TypeScript bindings + +```bash +yarn codegen +``` + +### 4. Start the local Aztec network + +```bash +aztec start --local-network +``` + +Wait for the network to be ready (check `curl http://localhost:8080/status`). + +### 5. Start the development server + +```bash +yarn dev +``` + +Open http://localhost:5173 in your browser. + +## Testing with the Test Wallet Extension + +The project includes a test wallet extension for development and E2E testing. + +### What the Test Extension Does + +The test extension is a fully functional wallet that: + +- **Creates and stores encrypted accounts** using PBKDF2 + AES-GCM key storage +- **Deploys account contracts** using SponsoredFPC (no fee tokens needed) +- **Signs and submits real transactions** with approval popups +- **Connects to dApps** via the wallet SDK protocol with ECDH key exchange + +This wallet works on the local network. + +### Building the Test Extension + +Before loading the extension, build it first: + +```bash +node esbuild.extension.mjs +``` + +### Loading the Test Extension in Chrome + +1. Open Chrome and navigate to `chrome://extensions/` +2. Enable **Developer mode** (toggle in the top right) +3. Click **Load unpacked** +4. Select the folder: `test-extension/` + +If you modify the extension source files, rebuild with the command above and reload the extension in Chrome (`chrome://extensions/` > click the refresh icon). + +## Project Structure + +```text +webapp-tutorial/ +├── contracts/ # Noir smart contract source +│ └── src/ +│ ├── main.nr # Main contract (PodRacing) +│ ├── race.nr # Race struct and logic +│ └── game_round_note.nr # Private note for round data +├── src/ +│ ├── artifacts/ # Generated TypeScript bindings +│ ├── components/ # React components +│ │ ├── AccountInfo.tsx +│ │ ├── ErrorBoundary.tsx +│ │ ├── GameBoard.tsx +│ │ ├── GameLobby.tsx +│ │ ├── GameStatus.tsx +│ │ ├── NetworkPicker.tsx +│ │ ├── TransactionLog.tsx +│ │ ├── TwoPlayerLocal.tsx +│ │ ├── TxStatus.tsx +│ │ └── WalletConnect.tsx +│ ├── App.tsx # Main app component +│ ├── config.ts # Network/PXE configuration +│ ├── contract.ts # Contract interaction helpers +│ ├── embedded-wallet.ts # Embedded wallet for local dev +│ ├── fees.ts # Fee payment helpers +│ ├── game-constants.ts # Shared game constants +│ ├── main.tsx # Entry point +│ └── wallet-connection.ts # Wallet SDK connection +├── test-extension/ # Functional wallet extension +│ ├── manifest.json +│ ├── popup/ +│ │ ├── popup.html +│ │ └── popup.css +│ └── src/ +│ ├── background.ts # Service worker - protocol + routing +│ ├── content-script.ts # Page <-> background relay +│ ├── config.ts # Constants and message types +│ ├── account-utils.ts # Shared account instantiation +│ ├── aztec-imports.ts # Lazy import caching +│ ├── utils.ts # Chrome runtime helpers +│ ├── offscreen/ +│ │ ├── offscreen.html +│ │ └── offscreen.ts # PXE host + wallet implementation +│ ├── popup/ +│ │ └── popup.tsx # React popup component +│ └── wallet/ +│ ├── wallet-impl.ts # ExtensionWalletManager +│ └── storage.ts # Encrypted key storage +└── tests/ # E2E tests +``` + +## Connecting to Different Networks + +- **Local**: Connects to `http://localhost:8080` (local network via `aztec start --local-network`) +- **Browser Wallet**: Same local network, but uses the wallet extension for account management and transaction signing + +## Troubleshooting + +### "Looking for wallet extensions..." hangs + +Make sure you have a wallet extension loaded: +- Load the test extension (see above), or +- Install the Aztec Keychain browser extension + +### SharedArrayBuffer errors + +The app requires `SharedArrayBuffer` for the WASM proving engine. The dev server is configured with the correct headers. If you see errors, ensure you're accessing via `localhost` (not `127.0.0.1`). + +### Contract compilation fails + +Ensure you have the correct version of `aztec` CLI installed: + +```bash +aztec --version +``` + +The version should match the `aztec-packages` repository version. diff --git a/docs/lines/v6/examples/webapp-tutorial/contracts/Nargo.toml b/docs/lines/v6/examples/webapp-tutorial/contracts/Nargo.toml new file mode 100644 index 00000000000..31a9adb0e48 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/contracts/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "pod_racing_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" } diff --git a/docs/lines/v6/examples/webapp-tutorial/contracts/src/game_round_note.nr b/docs/lines/v6/examples/webapp-tutorial/contracts/src/game_round_note.nr new file mode 100644 index 00000000000..79813c4c582 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/contracts/src/game_round_note.nr @@ -0,0 +1,24 @@ +// docs:start:game-round-note +use aztec::{macros::notes::note, protocol::{traits::Packable, address::AztecAddress}}; + +/// A private note storing a player's point allocation for one round. +/// These notes remain private until the player calls finish_game to reveal totals. +#[derive(Eq, Packable)] +#[note] +pub struct GameRoundNote { + pub track1: u8, + pub track2: u8, + pub track3: u8, + pub track4: u8, + pub track5: u8, + pub round: u8, + pub owner: AztecAddress, +} + +impl GameRoundNote { + pub fn new(track1: u8, track2: u8, track3: u8, track4: u8, track5: u8, round: u8, owner: AztecAddress) -> Self { + Self { track1, track2, track3, track4, track5, round, owner } + } + +} +// docs:end:game-round-note diff --git a/docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr b/docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr new file mode 100644 index 00000000000..9e010b50b50 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr @@ -0,0 +1,182 @@ +// Pod Racing Game Contract +// +// A two-player competitive racing game where players allocate points across 5 tracks +// over multiple rounds. The game flow: +// 1. Player 1 creates a game with a time limit +// 2. Player 2 joins the game +// 3. Both players play rounds privately (allocating points across tracks) +// 4. After all rounds, players reveal their total scores per track +// 5. Winner is determined by who won more tracks (best of 5) +// +// Key mechanics: +// - Each round, players distribute up to 9 points across 5 tracks +// - Round choices are private until the finish phase +// - The player with higher total points on a track wins that track +// - The player who wins 3+ tracks wins the game + +mod game_round_note; +mod race; + +use aztec::macros::aztec; + +#[aztec] +pub contract PodRacing { + use aztec::{ + macros::{functions::{external, initializer, only_self}, storage::storage}, + messages::delivery::MessageDelivery, + note::note_getter_options::NoteGetterOptions, + }; + use aztec::protocol::address::AztecAddress; + use aztec::state_vars::{Map, Owned, PrivateSet, PublicMutable}; + + use crate::{game_round_note::GameRoundNote, race::Race}; + + global TOTAL_ROUNDS: u8 = 3; + global GAME_LENGTH: u32 = 2; // Set to 2 for demo purposes + + // docs:start:storage + #[storage] + struct Storage<Context> { + admin: PublicMutable<AztecAddress, Context>, + races: Map<Field, PublicMutable<Race, Context>, Context>, + progress: Map<Field, Owned<PrivateSet<GameRoundNote, Context>, Context>, Context>, + win_history: Map<AztecAddress, PublicMutable<u64, Context>, Context>, + } + // docs:end:storage + + #[external("public")] + #[initializer] + fn constructor(admin: AztecAddress) { + self.storage.admin.write(admin); + } + + // docs:start:create-game + #[external("public")] + fn create_game(game_id: Field) { + assert(self.storage.races.at(game_id).read().player1.eq(AztecAddress::zero())); + let game = Race::new( + self.msg_sender(), + TOTAL_ROUNDS, + self.context.block_number() + GAME_LENGTH, + ); + self.storage.races.at(game_id).write(game); + } + // docs:end:create-game + + // docs:start:join-game + #[external("public")] + fn join_game(game_id: Field) { + let maybe_existing_game = self.storage.races.at(game_id).read(); + let joined_game = maybe_existing_game.join(self.msg_sender()); + self.storage.races.at(game_id).write(joined_game); + } + // docs:end:join-game + + // docs:start:play-round + /// Allocates points across 5 tracks for a round. + /// This is a PRIVATE function - the allocation remains hidden from the opponent. + #[external("private")] + fn play_round( + game_id: Field, + round: u8, + track1: u8, + track2: u8, + track3: u8, + track4: u8, + track5: u8, + ) { + assert(track1 + track2 + track3 + track4 + track5 < 10); + + let player = self.msg_sender(); + + self + .storage + .progress + .at(game_id) + .at(player) + .insert(GameRoundNote::new(track1, track2, track3, track4, track5, round, player)) + .deliver(MessageDelivery::onchain_unconstrained()); + + self.enqueue(PodRacing::at(self.context.this_address()).validate_and_play_round( + player, + game_id, + round, + )); + } + // docs:end:play-round + + #[external("public")] + #[only_self] + fn validate_and_play_round(player: AztecAddress, game_id: Field, round: u8) { + let game_in_progress = self.storage.races.at(game_id).read(); + self.storage.races.at(game_id).write(game_in_progress.increment_player_round(player, round)); + } + + // docs:start:finish-game + /// Reveals a player's total scores per track. + /// Reads private round notes and publishes aggregated totals. + #[external("private")] + fn finish_game(game_id: Field) { + let player = self.msg_sender(); + let totals = + self.storage.progress.at(game_id).at(player).get_notes(NoteGetterOptions::new()); + + let mut total_track1: u64 = 0; + let mut total_track2: u64 = 0; + let mut total_track3: u64 = 0; + let mut total_track4: u64 = 0; + let mut total_track5: u64 = 0; + + for i in 0..TOTAL_ROUNDS { + total_track1 += totals.get(i as u32).note.track1 as u64; + total_track2 += totals.get(i as u32).note.track2 as u64; + total_track3 += totals.get(i as u32).note.track3 as u64; + total_track4 += totals.get(i as u32).note.track4 as u64; + total_track5 += totals.get(i as u32).note.track5 as u64; + } + + self.enqueue(PodRacing::at(self.context.this_address()).validate_finish_game_and_reveal( + player, + game_id, + total_track1, + total_track2, + total_track3, + total_track4, + total_track5, + )); + } + // docs:end:finish-game + + #[external("public")] + #[only_self] + fn validate_finish_game_and_reveal( + player: AztecAddress, + game_id: Field, + total_track1: u64, + total_track2: u64, + total_track3: u64, + total_track4: u64, + total_track5: u64, + ) { + let game_in_progress = self.storage.races.at(game_id).read(); + self.storage.races.at(game_id).write(game_in_progress.set_player_scores( + player, + total_track1, + total_track2, + total_track3, + total_track4, + total_track5, + )); + } + + // docs:start:finalize-game + /// Determines the winner after both players have revealed and the game has expired. + #[external("public")] + fn finalize_game(game_id: Field) { + let game_in_progress = self.storage.races.at(game_id).read(); + let winner = game_in_progress.calculate_winner(self.context.block_number()); + let previous_wins = self.storage.win_history.at(winner).read(); + self.storage.win_history.at(winner).write(previous_wins + 1); + } + // docs:end:finalize-game +} diff --git a/docs/lines/v6/examples/webapp-tutorial/contracts/src/race.nr b/docs/lines/v6/examples/webapp-tutorial/contracts/src/race.nr new file mode 100644 index 00000000000..05cb3efd792 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/contracts/src/race.nr @@ -0,0 +1,166 @@ +// docs:start:race +use aztec::protocol::{ + address::AztecAddress, + traits::{Deserialize, Serialize, Packable}, +}; + +/// Public game state: player addresses, round progress, and final track scores. +#[derive(Deserialize, Serialize, Eq, Packable)] +pub struct Race { + pub player1: AztecAddress, + pub player2: AztecAddress, + pub total_rounds: u8, + pub player1_round: u8, + pub player2_round: u8, + pub player1_track1_final: u64, + pub player1_track2_final: u64, + pub player1_track3_final: u64, + pub player1_track4_final: u64, + pub player1_track5_final: u64, + pub player2_track1_final: u64, + pub player2_track2_final: u64, + pub player2_track3_final: u64, + pub player2_track4_final: u64, + pub player2_track5_final: u64, + pub end_block: u32, +} + +impl Race { + pub fn new(player1: AztecAddress, total_rounds: u8, end_block: u32) -> Race { + Self { + player1, + player2: AztecAddress::zero(), + total_rounds, + player1_round: 0, + player2_round: 0, + player1_track1_final: 0, + player1_track2_final: 0, + player1_track3_final: 0, + player1_track4_final: 0, + player1_track5_final: 0, + player2_track1_final: 0, + player2_track2_final: 0, + player2_track3_final: 0, + player2_track4_final: 0, + player2_track5_final: 0, + end_block, + } + } + + pub fn join(self, player2: AztecAddress) -> Race { + assert(!self.player1.eq(AztecAddress::zero()) & self.player2.eq(AztecAddress::zero())); + assert(!self.player1.eq(player2)); + Self { + player1: self.player1, + player2, + total_rounds: self.total_rounds, + player1_round: self.player1_round, + player2_round: self.player2_round, + player1_track1_final: 0, + player1_track2_final: 0, + player1_track3_final: 0, + player1_track4_final: 0, + player1_track5_final: 0, + player2_track1_final: 0, + player2_track2_final: 0, + player2_track3_final: 0, + player2_track4_final: 0, + player2_track5_final: 0, + end_block: self.end_block, + } + } + + pub fn increment_player_round(self, player: AztecAddress, round: u8) -> Race { + assert(round < self.total_rounds + 1); + let ret = if player.eq(self.player1) { + assert(round == self.player1_round + 1); + Option::some(Self { + player1: self.player1, player2: self.player2, + total_rounds: self.total_rounds, + player1_round: round, player2_round: self.player2_round, + player1_track1_final: self.player1_track1_final, player1_track2_final: self.player1_track2_final, + player1_track3_final: self.player1_track3_final, player1_track4_final: self.player1_track4_final, + player1_track5_final: self.player1_track5_final, + player2_track1_final: self.player2_track1_final, player2_track2_final: self.player2_track2_final, + player2_track3_final: self.player2_track3_final, player2_track4_final: self.player2_track4_final, + player2_track5_final: self.player2_track5_final, + end_block: self.end_block, + }) + } else if player.eq(self.player2) { + assert(round == self.player2_round + 1); + Option::some(Self { + player1: self.player1, player2: self.player2, + total_rounds: self.total_rounds, + player1_round: self.player1_round, player2_round: round, + player1_track1_final: self.player1_track1_final, player1_track2_final: self.player1_track2_final, + player1_track3_final: self.player1_track3_final, player1_track4_final: self.player1_track4_final, + player1_track5_final: self.player1_track5_final, + player2_track1_final: self.player2_track1_final, player2_track2_final: self.player2_track2_final, + player2_track3_final: self.player2_track3_final, player2_track4_final: self.player2_track4_final, + player2_track5_final: self.player2_track5_final, + end_block: self.end_block, + }) + } else { + Option::none() + }; + ret.unwrap() + } + + pub fn set_player_scores(self, player: AztecAddress, track1_final: u64, track2_final: u64, track3_final: u64, track4_final: u64, track5_final: u64) -> Race { + let ret = if player.eq(self.player1) { + assert( + self.player1_track1_final + self.player1_track2_final + + self.player1_track3_final + self.player1_track4_final + + self.player1_track5_final == 0 + ); + Option::some(Self { + player1: self.player1, player2: self.player2, + total_rounds: self.total_rounds, + player1_round: self.player1_round, player2_round: self.player2_round, + player1_track1_final: track1_final, player1_track2_final: track2_final, + player1_track3_final: track3_final, player1_track4_final: track4_final, + player1_track5_final: track5_final, + player2_track1_final: self.player2_track1_final, player2_track2_final: self.player2_track2_final, + player2_track3_final: self.player2_track3_final, player2_track4_final: self.player2_track4_final, + player2_track5_final: self.player2_track5_final, + end_block: self.end_block, + }) + } else if player.eq(self.player2) { + assert( + self.player2_track1_final + self.player2_track2_final + + self.player2_track3_final + self.player2_track4_final + + self.player2_track5_final == 0 + ); + Option::some(Self { + player1: self.player1, player2: self.player2, + total_rounds: self.total_rounds, + player1_round: self.player1_round, player2_round: self.player2_round, + player1_track1_final: self.player1_track1_final, player1_track2_final: self.player1_track2_final, + player1_track3_final: self.player1_track3_final, player1_track4_final: self.player1_track4_final, + player1_track5_final: self.player1_track5_final, + player2_track1_final: track1_final, player2_track2_final: track2_final, + player2_track3_final: track3_final, player2_track4_final: track4_final, + player2_track5_final: track5_final, + end_block: self.end_block, + }) + } else { + Option::none() + }; + ret.unwrap() + } + + pub fn calculate_winner(self, current_block_number: u32) -> AztecAddress { + assert(current_block_number > self.end_block); + let mut player1_wins = 0; + let mut player2_wins = 0; + + if self.player1_track1_final > self.player2_track1_final { player1_wins += 1; } else { player2_wins += 1; }; + if self.player1_track2_final > self.player2_track2_final { player1_wins += 1; } else { player2_wins += 1; }; + if self.player1_track3_final > self.player2_track3_final { player1_wins += 1; } else { player2_wins += 1; }; + if self.player1_track4_final > self.player2_track4_final { player1_wins += 1; } else { player2_wins += 1; }; + if self.player1_track5_final > self.player2_track5_final { player1_wins += 1; } else { player2_wins += 1; }; + + if (player1_wins > player2_wins) { self.player1 } else { self.player2 } + } +} +// docs:end:race diff --git a/docs/lines/v6/examples/webapp-tutorial/esbuild.extension.mjs b/docs/lines/v6/examples/webapp-tutorial/esbuild.extension.mjs new file mode 100644 index 00000000000..575d6924d63 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/esbuild.extension.mjs @@ -0,0 +1,100 @@ +/** + * Build script for the test extension. + * Runs Vite first for main bundles, then esbuild for content-script (IIFE format). + */ +import * as esbuild from 'esbuild'; +import { createRequire } from 'module'; +import path from 'path'; +import { copyFileSync, mkdirSync, existsSync } from 'fs'; +import { execSync } from 'child_process'; + +const localRequire = createRequire(import.meta.url); + +// Node.js built-ins to mark as external (will be polyfilled at runtime) +const nodeBuiltins = new Set([ + 'assert', 'buffer', 'crypto', 'events', 'fs', 'http', 'https', 'net', + 'os', 'path', 'process', 'querystring', 'stream', 'string_decoder', + 'tls', 'url', 'util', 'zlib', 'worker_threads', 'perf_hooks', + 'node:assert', 'node:buffer', 'node:crypto', 'node:events', 'node:fs', + 'node:http', 'node:https', 'node:net', 'node:os', 'node:path', + 'node:process', 'node:querystring', 'node:stream', 'node:string_decoder', + 'node:tls', 'node:url', 'node:util', 'node:zlib', 'node:worker_threads', + 'node:perf_hooks', +]); + +const nodeModulesPlugin = { + name: 'node-modules-resolver', + setup(build) { + // Handle Node.js built-ins + build.onResolve({ filter: /.*/ }, (args) => { + if (nodeBuiltins.has(args.path)) { + // Return empty module for browser + return { path: args.path, namespace: 'node-builtin' }; + } + if (args.path.startsWith('.') || args.path.startsWith('/')) { + return undefined; + } + try { + const resolved = localRequire.resolve(args.path, { + paths: [path.resolve('node_modules')], + }); + return { path: resolved }; + } catch { + return undefined; + } + }); + + // Return empty modules for Node built-ins + build.onLoad({ filter: /.*/, namespace: 'node-builtin' }, () => { + return { contents: 'export default {}; export const inspect = (x) => String(x);', loader: 'js' }; + }); + }, +}; + +// Step 1: Run Vite build +console.log('Step 1: Running Vite build...'); +execSync('npx vite build --config vite.extension.config.ts', { stdio: 'inherit' }); + +// Step 2: Build content-script as IIFE (Chrome content scripts don't support ES modules) +console.log(''); +console.log('Step 2: Building content-script (IIFE format)...'); +await esbuild.build({ + entryPoints: ['test-extension/src/content-script.ts'], + bundle: true, + outfile: 'test-extension/dist/content-script.js', + format: 'iife', + platform: 'browser', + plugins: [nodeModulesPlugin], + logLevel: 'info', +}); + +// Step 3: Copy static files +console.log(''); +console.log('Step 3: Copying static files...'); +copyFileSync( + 'test-extension/src/offscreen/offscreen-dist.html', + 'test-extension/dist/offscreen.html' +); +console.log('Copied offscreen.html to dist/'); + +// Step 4: Copy WASM files to extension root so /wasm/ paths resolve correctly. +// The built JS references WASM via `new URL("/wasm/...", import.meta.url)` which resolves +// to the extension root, not the dist/ subdirectory. +const wasmSrcDir = 'test-extension/dist/wasm'; +const wasmDestDir = 'test-extension/wasm'; +if (existsSync(wasmSrcDir)) { + mkdirSync(wasmDestDir, { recursive: true }); + const wasmFiles = ['noirc_abi_wasm_bg.wasm', 'acvm_js_bg.wasm']; + for (const file of wasmFiles) { + const src = path.join(wasmSrcDir, file); + const dest = path.join(wasmDestDir, file); + if (existsSync(src)) { + copyFileSync(src, dest); + console.log(`Copied ${file} to ${wasmDestDir}/`); + } + } +} + +console.log(''); +console.log('Extension build complete!'); +console.log('Load the extension from: test-extension/'); diff --git a/docs/lines/v6/examples/webapp-tutorial/index.html b/docs/lines/v6/examples/webapp-tutorial/index.html new file mode 100644 index 00000000000..1d2cde23ebf --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Pod Racing on Aztec + + +
+ + + diff --git a/docs/lines/v6/examples/webapp-tutorial/package.json b/docs/lines/v6/examples/webapp-tutorial/package.json new file mode 100644 index 00000000000..3c2fc6ce9f0 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/package.json @@ -0,0 +1,53 @@ +{ + "name": "webapp-tutorial-pod-racing", + "private": true, + "version": "0.1.0", + "type": "module", + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "clean": "rm -rf ../../target src/artifacts/*.json src/artifacts/*.ts dist", + "compile": "cd ../.. && ${AZTEC:-aztec} compile --package pod_racing_contract --force && mkdir -p examples/webapp-tutorial/src/artifacts && cp target/pod_racing_contract-PodRacing.json examples/webapp-tutorial/src/artifacts", + "codegen": "cd ../.. && ${AZTEC:-aztec} codegen ./target -o ./target && cp target/*.ts examples/webapp-tutorial/src/artifacts", + "prep": "yarn compile && yarn codegen", + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "test:unit": "vitest run tests/unit/", + "test:e2e": "vitest run tests/e2e/ --test-timeout=120000", + "build:test-extension": "node esbuild.extension.mjs", + "build:wallet-extension": "vite build --config vite.extension.config.ts && node esbuild.extension.mjs", + "interact": "npx tsx scripts/deploy-and-interact.ts", + "test": "yarn test:unit && yarn build:test-extension && yarn test:e2e" + }, + "dependencies": { + "@aztec-labs/accounts": "#include_aztec_version", + "@aztec-labs/aztec.js": "#include_aztec_version", + "@aztec-labs/constants": "#include_aztec_version", + "@aztec-labs/entrypoints": "#include_aztec_version", + "@aztec-labs/foundation": "#include_aztec_version", + "@aztec-labs/kv-store": "#include_aztec_version", + "@aztec-labs/noir-contracts.js": "#include_aztec_version", + "@aztec-labs/protocol-contracts": "#include_aztec_version", + "@aztec-labs/pxe": "#include_aztec_version", + "@aztec-labs/standard-contracts": "#include_aztec_version", + "@aztec-labs/stdlib": "#include_aztec_version", + "@aztec-labs/wallet-sdk": "#include_aztec_version", + "@aztec-labs/wallets": "#include_aztec_version", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react-swc": "^3.7.2", + "esbuild": "^0.24.0", + "jsdom": "^25.0.0", + "puppeteer": "^24.40.0", + "typescript": "^5.6.0", + "vite": "^6.0.0", + "vite-plugin-node-polyfills": "^0.28.0", + "vitest": "^2.0.0" + } +} diff --git a/docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts b/docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts new file mode 100644 index 00000000000..c3ff5974f3e --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts @@ -0,0 +1,72 @@ +// docs:start:script-setup +import { EmbeddedWallet } from "@aztec-labs/wallets/embedded"; +import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing"; +// @ts-ignore — generated artifact, may not exist until compiled +import { PodRacingContract } from "../src/artifacts/PodRacing.js"; + +const nodeUrl = process.env.AZTEC_NODE_URL ?? "http://localhost:8080"; +const wallet = await EmbeddedWallet.create(nodeUrl, { ephemeral: true }); + +const [alice, bob] = await getInitialTestAccountsData(); +await wallet.createSchnorrInitializerlessAccount(alice.secret, alice.salt, alice.signingKey); +await wallet.createSchnorrInitializerlessAccount(bob.secret, bob.salt, bob.signingKey); +console.log( + "Accounts ready:", + alice.address.toString(), + bob.address.toString(), +); +// docs:end:script-setup + +// docs:start:script-deploy +const { contract } = await PodRacingContract.deploy(wallet, alice.address).send( + { + from: alice.address, + }, +); +console.log("Contract deployed at:", contract.address.toString()); +// docs:end:script-deploy + +// docs:start:script-create-join +const gameId = 1n; +await contract.methods.create_game(gameId).send({ from: alice.address }); +console.log("Game created"); + +await contract.methods.join_game(gameId).send({ from: bob.address }); +console.log("Bob joined the game"); +// docs:end:script-create-join + +// docs:start:script-play-rounds +// Round 1 +await contract.methods + .play_round(gameId, 1, 3, 2, 1, 2, 1) + .send({ from: alice.address }); +await contract.methods + .play_round(gameId, 1, 1, 1, 3, 2, 2) + .send({ from: bob.address }); + +// Round 2 +await contract.methods + .play_round(gameId, 2, 2, 3, 1, 1, 2) + .send({ from: alice.address }); +await contract.methods + .play_round(gameId, 2, 2, 2, 2, 2, 1) + .send({ from: bob.address }); + +// Round 3 +await contract.methods + .play_round(gameId, 3, 1, 1, 2, 3, 2) + .send({ from: alice.address }); +await contract.methods + .play_round(gameId, 3, 3, 1, 1, 1, 3) + .send({ from: bob.address }); +console.log("All rounds played"); +// docs:end:script-play-rounds + +// docs:start:script-finish-finalize +await contract.methods.finish_game(gameId).send({ from: alice.address }); +await contract.methods.finish_game(gameId).send({ from: bob.address }); +console.log("Both players revealed scores"); + +await contract.methods.finalize_game(gameId).send({ from: alice.address }); +console.log("Game finalized! Winner determined."); +// docs:end:script-finish-finalize diff --git a/docs/lines/v6/examples/webapp-tutorial/setup.sh b/docs/lines/v6/examples/webapp-tutorial/setup.sh new file mode 100755 index 00000000000..fd832577d5f --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/setup.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Setup script for the webapp-tutorial example. +# Resolves #include_aztec_version macros in package.json and installs dependencies. +set -euo pipefail + +cd "$(dirname "$0")" + +# Determine version from the checked-out git tag (readers check out a release tag) +VERSION="${1:-$(git describe --tags --exact-match 2>/dev/null || echo "")}" + +if [ -z "$VERSION" ]; then + echo "Error: Could not determine Aztec version from git tag." + echo "Usage: ./setup.sh (e.g., ./setup.sh 0.87.0)" + echo "Or check out a release tag first: git checkout " + exit 1 +fi + +echo "Resolving #include_aztec_version to $VERSION ..." + +node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + for (const section of ['dependencies', 'devDependencies']) { + for (const [name, ver] of Object.entries(pkg[section] || {})) { + if (ver === '#include_aztec_version') { + pkg[section][name] = '$VERSION'; + } + } + } + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); +" + +echo "Installing dependencies..." +# Without its own lockfile, yarn 4 treats this directory as part of the docs/ workspace and refuses to install. +[ -f yarn.lock ] || touch yarn.lock +yarn install + +echo "Setup complete!" diff --git a/docs/lines/v6/examples/webapp-tutorial/src/App.css b/docs/lines/v6/examples/webapp-tutorial/src/App.css new file mode 100644 index 00000000000..1cabbc31e74 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/App.css @@ -0,0 +1,533 @@ +/* docs:start:app-css */ +:root { + --bg: #0d1117; + --surface: #161b22; + --border: #30363d; + --text: #e6edf3; + --text-muted: #8b949e; + --accent: #58a6ff; + --accent-hover: #79c0ff; + --error: #f85149; + --success: #3fb950; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; + background: var(--bg); + color: var(--text); + min-height: 100vh; +} + +.app { + max-width: 800px; + margin: 0 auto; + padding: 2rem 1rem; +} + +header { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border); +} + +header h1 { + font-size: 1.5rem; + flex: 1; +} + +.network-picker { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.account-info { + display: flex; + gap: 0.5rem; + font-size: 0.875rem; +} + +.account-label { + color: var(--text-muted); +} + +.account-address { + font-family: monospace; + color: var(--accent); +} + +main { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.wallet-connect, +.game-lobby, +.game-board, +.game-status { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1.5rem; +} + +h2 { font-size: 1.25rem; margin-bottom: 1rem; } +h3 { font-size: 1rem; margin-bottom: 0.75rem; } + +label { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + color: var(--text-muted); +} + +input, select { + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + padding: 0.5rem; + border-radius: 4px; + font-size: 0.875rem; +} + +input[type="text"] { + width: 100%; + max-width: 400px; +} + +button { + background: var(--accent); + color: var(--bg); + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-weight: 600; + font-size: 0.875rem; + margin-top: 0.5rem; +} + +button:hover:not(:disabled) { + background: var(--accent-hover); +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.status { + color: var(--text-muted); + font-size: 0.875rem; + margin-bottom: 0.75rem; +} + +.lobby-section { + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border); +} + +.lobby-section:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.tracks { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1rem; +} + +.track { + display: flex; + align-items: center; + gap: 1rem; +} + +.track label { + min-width: 120px; + margin-bottom: 0; +} + +.track input[type="range"] { + flex: 1; +} + +.track span { + min-width: 60px; + text-align: right; + font-family: monospace; +} + +.total { + font-weight: 600; + margin-bottom: 0.5rem; +} + +.privacy-note { + color: var(--text-muted); + font-size: 0.8rem; + font-style: italic; +} + +.emoji-grid { + font-size: 2rem; + letter-spacing: 0.5rem; + margin: 1rem 0; +} + +.tx-status { + padding: 0.75rem; + border-radius: 4px; + font-size: 0.875rem; +} + +.tx-status-sending, +.tx-status-proving { + background: rgba(88, 166, 255, 0.1); + border: 1px solid var(--accent); +} + +.tx-status-confirmed { + background: rgba(63, 185, 80, 0.1); + border: 1px solid var(--success); +} + +.tx-status-error { + background: rgba(248, 81, 73, 0.1); + border: 1px solid var(--error); +} + +.tx-hash { + font-family: monospace; + font-size: 0.8rem; + color: var(--text-muted); +} + +.game-area { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Transaction Log Styles */ +.transaction-log { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + margin-top: 1.5rem; +} + +.log-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} + +.log-header h3 { + margin: 0; + font-size: 0.875rem; + color: var(--text-muted); +} + +.clear-log-btn { + background: transparent; + border: 1px solid var(--border); + color: var(--text-muted); + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + margin: 0; +} + +.clear-log-btn:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); + background: transparent; +} + +.log-entries { + max-height: 200px; + overflow-y: auto; + font-family: monospace; + font-size: 0.8rem; +} + +.log-entry { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 0.25rem 0; + border-bottom: 1px solid rgba(48, 54, 61, 0.5); +} + +.log-entry:last-child { + border-bottom: none; +} + +.log-time { + color: var(--text-muted); + flex-shrink: 0; +} + +.log-level-icon { + flex-shrink: 0; + width: 1rem; + text-align: center; +} + +.log-info .log-level-icon { color: var(--text-muted); } +.log-success .log-level-icon { color: var(--success); } +.log-error .log-level-icon { color: var(--error); } +.log-pending .log-level-icon { + color: var(--accent); + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.log-message { + flex: 1; + word-break: break-word; +} + +.log-info .log-message { color: var(--text); } +.log-success .log-message { color: var(--success); } +.log-error .log-message { color: var(--error); } +.log-pending .log-message { color: var(--accent); } + +.log-tx-hash { + color: var(--text-muted); + font-size: 0.75rem; + flex-shrink: 0; + cursor: help; +} + +/* Two-Player Local Mode Styles */ +.two-player-local { + width: 100%; +} + +.game-header { + text-align: center; + margin-bottom: 1.5rem; +} + +.game-header h2 { + margin-bottom: 0.75rem; +} + +.game-info { + display: flex; + justify-content: center; + gap: 1.5rem; + flex-wrap: wrap; + font-size: 0.875rem; + color: var(--text-muted); +} + +.game-info span { + font-family: monospace; +} + +.phase-badge { + background: var(--accent); + color: var(--bg); + padding: 0.125rem 0.5rem; + border-radius: 4px; + font-weight: 600; + font-family: inherit !important; +} + +.players-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +@media (max-width: 768px) { + .players-container { + grid-template-columns: 1fr; + } +} + +.player-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + transition: opacity 0.2s; +} + +.player-panel.loading { + opacity: 0.7; +} + +.player-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} + +.player-header h3 { + margin: 0; + font-size: 1rem; +} + +.player-address { + font-family: monospace; + font-size: 0.75rem; + color: var(--accent); +} + +.player-status { + font-size: 0.8rem; + color: var(--text-muted); + margin: 0; +} + +.player-game-board { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.round-indicator { + font-weight: 600; + text-align: center; + color: var(--accent); + font-size: 0.875rem; +} + +.tracks-compact { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.track-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.track-name { + width: 70px; + font-size: 0.75rem; + color: var(--text-muted); +} + +.track-row input[type="range"] { + flex: 1; + height: 4px; +} + +.track-value { + width: 20px; + text-align: right; + font-family: monospace; + font-size: 0.8rem; +} + +.total-display { + text-align: center; + font-weight: 600; + font-size: 0.875rem; + padding: 0.25rem; + border-radius: 4px; + background: rgba(88, 166, 255, 0.1); +} + +.total-display.over-limit { + background: rgba(248, 81, 73, 0.1); + color: var(--error); +} + +.waiting-message, +.finished-message { + text-align: center; + color: var(--text-muted); + font-style: italic; + padding: 1rem; +} + +.revealed-badge, +.winner-badge { + text-align: center; + padding: 0.5rem; + border-radius: 4px; + font-weight: 600; +} + +.revealed-badge { + background: rgba(63, 185, 80, 0.1); + color: var(--success); + border: 1px solid var(--success); +} + +.winner-badge { + background: rgba(88, 166, 255, 0.1); + color: var(--accent); + border: 1px solid var(--accent); +} + +.provider-button { + display: flex; + align-items: center; + gap: 8px; +} + +.provider-icon { + width: 20px; + height: 20px; +} + +.error-boundary { + background: var(--surface); + border: 1px solid var(--error); + border-radius: 8px; + padding: 2rem; + text-align: center; +} + +.error-boundary h2 { + color: var(--error); +} + +.error-boundary pre { + text-align: left; + background: var(--bg); + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.8rem; + color: var(--text-muted); + margin-top: 1rem; +} +/* docs:end:app-css */ diff --git a/docs/lines/v6/examples/webapp-tutorial/src/App.tsx b/docs/lines/v6/examples/webapp-tutorial/src/App.tsx new file mode 100644 index 00000000000..825d385b60c --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/App.tsx @@ -0,0 +1,131 @@ +// docs:start:app-imports +import React, { useState } from 'react'; +import type { Wallet } from '@aztec-labs/aztec.js/wallet'; +import { AztecAddress } from '@aztec-labs/aztec.js/addresses'; +import type { NetworkType } from './config'; +import type { PodRacingContract } from './artifacts/PodRacing'; +import { NetworkPicker } from './components/NetworkPicker'; +import { WalletConnect } from './components/WalletConnect'; +import { AccountInfo } from './components/AccountInfo'; +import { GameLobby } from './components/GameLobby'; +import { GameBoard } from './components/GameBoard'; +import { GameStatus } from './components/GameStatus'; +import { ErrorBoundary } from './components/ErrorBoundary'; +import { LogProvider, TransactionLog } from './components/TransactionLog'; +import { TwoPlayerLocal } from './components/TwoPlayerLocal'; +import { EmbeddedWallet } from './embedded-wallet'; +// docs:end:app-imports + +// docs:start:app-state +type AppPhase = 'connect' | 'lobby' | 'playing'; + +function App() { + const [network, setNetwork] = useState('local'); + const [wallet, setWallet] = useState(null); + const [account, setAccount] = useState(null); + const [phase, setPhase] = useState('connect'); + const [contract, setContract] = useState(null); + const [gameId, setGameId] = useState(BigInt(0)); + const [currentRound, setCurrentRound] = useState(1); +// docs:end:app-state + + // docs:start:app-handlers + async function handleWalletConnected(w: Wallet | EmbeddedWallet) { + setWallet(w); + if (w instanceof EmbeddedWallet) { + setAccount(w.getConnectedAccount()); + setPhase('lobby'); + } else { + // Extension wallet — getAccounts returns the active account(s) + try { + const accounts = await w.getAccounts(); + console.log('Accounts received:', accounts); + if (accounts && accounts.length > 0) { + const addr = accounts[0].item; + console.log('Setting account:', addr); + setAccount(addr); + setPhase('lobby'); + } else { + alert('Please create an account in the wallet extension first, then refresh the page.'); + } + } catch (err: unknown) { + console.error('Error getting accounts:', err); + alert(`Error connecting to wallet: ${err}`); + } + } + } + + function handleGameJoined(c: PodRacingContract, gId: bigint) { + setContract(c); + setGameId(gId); + setCurrentRound(1); + setPhase('playing'); + } + + function handleRoundPlayed() { + setCurrentRound((r) => r + 1); + } + // docs:end:app-handlers + + // docs:start:app-render + return ( + + +
+
+

Pod Racing on Aztec

+ + {network === 'remote' && account && } +
+ +
+ {/* Local network: Two-player split-screen mode */} + {network === 'local' && } + + {/* Remote: Single-player mode with wallet extension */} + {network === 'remote' && phase === 'connect' && ( + + )} + + {network === 'remote' && phase === 'lobby' && wallet && account && ( + + )} + + {network === 'remote' && phase === 'playing' && wallet && account && contract && ( +
+ + +
+ )} + + +
+
+
+
+ ); + // docs:end:app-render +} + +export { App }; diff --git a/docs/lines/v6/examples/webapp-tutorial/src/components/AccountInfo.tsx b/docs/lines/v6/examples/webapp-tutorial/src/components/AccountInfo.tsx new file mode 100644 index 00000000000..74ea0f3c206 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/components/AccountInfo.tsx @@ -0,0 +1,23 @@ +// docs:start:account-info +import React from 'react'; +import { AztecAddress } from '@aztec-labs/aztec.js/addresses'; + +interface AccountInfoProps { + address: AztecAddress; +} + +/** + * Displays the connected account address (truncated). + */ +export function AccountInfo({ address }: AccountInfoProps) { + const addr = address.toString(); + const display = `${addr.slice(0, 10)}...${addr.slice(-6)}`; + + return ( +
+ Connected: + {display} +
+ ); +} +// docs:end:account-info diff --git a/docs/lines/v6/examples/webapp-tutorial/src/components/ErrorBoundary.tsx b/docs/lines/v6/examples/webapp-tutorial/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000000..57deabff998 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/components/ErrorBoundary.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +/** + * Catches unhandled errors during rendering and shows a fallback UI + * instead of crashing the entire application with a blank screen. + */ +export class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + ErrorBoundaryState +> { + constructor(props: { children: React.ReactNode }) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

The application encountered an unexpected error.

+ + {this.state.error && ( +
{this.state.error.message}
+ )} +
+ ); + } + + return this.props.children; + } +} diff --git a/docs/lines/v6/examples/webapp-tutorial/src/components/GameBoard.tsx b/docs/lines/v6/examples/webapp-tutorial/src/components/GameBoard.tsx new file mode 100644 index 00000000000..1ef95ebac19 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/components/GameBoard.tsx @@ -0,0 +1,151 @@ +import React, { useState } from 'react'; +import { AztecAddress } from '@aztec-labs/aztec.js/addresses'; +import type { PodRacingContract } from '../artifacts/PodRacing'; +import { playRound, finishGame, finalizeGame } from '../contract'; +import { TRACK_NAMES, MAX_POINTS_PER_ROUND, TOTAL_ROUNDS } from '../game-constants'; +import { useTransactionLog } from './TransactionLog'; + +interface GameBoardProps { + contract: PodRacingContract; + account: AztecAddress; + gameId: bigint; + currentRound: number; + onRoundPlayed: () => void; +} + +export function GameBoard({ + contract, + account, + gameId, + currentRound, + onRoundPlayed, +}: GameBoardProps) { + const [allocations, setAllocations] = useState<[number, number, number, number, number]>([2, 2, 2, 2, 1]); + const [loading, setLoading] = useState(false); + const [status, setStatus] = useState(''); + const { addLog } = useTransactionLog(); + + function updateAllocation(trackIndex: number, value: number) { + const newAllocations = [...allocations] as [number, number, number, number, number]; + newAllocations[trackIndex] = value; + setAllocations(newAllocations); + } + + const total = allocations.reduce((sum, v) => sum + v, 0); + + // docs:start:submit-round + async function handleSubmitRound() { + if (total >= 10) { + setStatus(`Points must sum to less than 10 (currently ${total})`); + return; + } + + setLoading(true); + setStatus('Submitting your allocation (private transaction)...'); + addLog(`Round ${currentRound}: Submitting allocation [${allocations.join(', ')}]...`, 'pending'); + try { + addLog('Building private transaction proof...', 'pending'); + const receipt = await playRound(contract, account, gameId, currentRound, allocations); + addLog(`Round ${currentRound} submitted successfully`, 'success', receipt.receipt.txHash?.toString()); + setStatus('Round submitted!'); + setAllocations([2, 2, 2, 2, 1]); + onRoundPlayed(); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Error submitting round: ${msg}`, 'error'); + } finally { + setLoading(false); + } + } + // docs:end:submit-round + + // docs:start:finish-and-finalize + async function handleFinishGame() { + setLoading(true); + setStatus('Revealing your total scores...'); + addLog('Revealing scores (finish_game)...', 'pending'); + try { + addLog('Reading private notes and computing totals...', 'pending'); + const receipt = await finishGame(contract, account, gameId); + addLog('Scores revealed successfully', 'success', receipt.receipt.txHash?.toString()); + setStatus('Scores revealed! Waiting for opponent to reveal, then finalize.'); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Error revealing scores: ${msg}`, 'error'); + } finally { + setLoading(false); + } + } + + async function handleFinalizeGame() { + setLoading(true); + setStatus('Determining winner...'); + addLog('Finalizing game and determining winner...', 'pending'); + try { + const receipt = await finalizeGame(contract, account, gameId); + addLog('Game finalized! Winner determined.', 'success', receipt.receipt.txHash?.toString()); + setStatus('Game finalized! Winner determined.'); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Error finalizing game: ${msg}`, 'error'); + } finally { + setLoading(false); + } + } + // docs:end:finish-and-finalize + + const allRoundsPlayed = currentRound > TOTAL_ROUNDS; + + return ( +
+

{allRoundsPlayed ? 'All Rounds Played' : `Round ${currentRound} of ${TOTAL_ROUNDS}`}

+ {!allRoundsPlayed && ( +

Allocate up to {MAX_POINTS_PER_ROUND} points across 5 tracks. Your allocation is private.

+ )} + {status &&

{status}

} + + {!allRoundsPlayed && ( + <> +
+ {TRACK_NAMES.map((name, i) => ( +
+ + updateAllocation(i, Number(e.target.value))} + disabled={loading} + /> + {allocations[i]} pts +
+ ))} +
+ +

= 10 ? 'error' : ''}`}> + Total: {total} / {MAX_POINTS_PER_ROUND} max +

+ + + + )} + + {allRoundsPlayed && ( +
+ + +
+ )} +
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/src/components/GameLobby.tsx b/docs/lines/v6/examples/webapp-tutorial/src/components/GameLobby.tsx new file mode 100644 index 00000000000..955ce440248 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/components/GameLobby.tsx @@ -0,0 +1,154 @@ +// docs:start:game-lobby-imports +import React, { useState } from 'react'; +import { AztecAddress } from '@aztec-labs/aztec.js/addresses'; +import type { Wallet } from '@aztec-labs/aztec.js/wallet'; +import type { PodRacingContract } from '../artifacts/PodRacing'; +import { deployContract, createGame, joinGame, attachToContract } from '../contract'; +import { useTransactionLog } from './TransactionLog'; + +interface GameLobbyProps { + wallet: Wallet; + account: AztecAddress; + onGameJoined: (contract: PodRacingContract, gameId: bigint) => void; +} +// docs:end:game-lobby-imports + +export function GameLobby({ wallet, account, onGameJoined }: GameLobbyProps) { + const [status, setStatus] = useState(''); + const [isCreating, setIsCreating] = useState(false); + const [isJoining, setIsJoining] = useState(false); + const [gameId, setGameId] = useState('1'); + const [joinGameIdInput, setJoinGameIdInput] = useState(''); + const [joinContractAddress, setJoinContractAddress] = useState(''); + const { addLog } = useTransactionLog(); + + // docs:start:handle-create + async function handleCreateGame() { + setIsCreating(true); + setStatus('Deploying Pod Racing contract...'); + addLog('Starting contract deployment...', 'pending'); + try { + let gId: bigint; + try { + gId = BigInt(gameId); + if (gId <= 0n) throw new Error('must be positive'); + } catch { + setStatus('Invalid game ID — enter a positive integer'); + setIsCreating(false); + return; + } + + addLog('Compiling and sending deployment transaction...', 'pending'); + const contract = await deployContract(wallet, account); + addLog(`Contract deployed at ${contract.address.toString()}`, 'success'); + + setStatus('Creating game...'); + addLog('Creating game...', 'pending'); + const receipt = await createGame(contract, account, gId); + addLog(`Game ${gId} created successfully`, 'success', receipt.receipt.txHash?.toString()); + + setStatus(`Game created! Share contract address: ${contract.address}`); + onGameJoined(contract, gId); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Error: ${msg}`, 'error'); + } finally { + setIsCreating(false); + } + } + // docs:end:handle-create + + // docs:start:handle-join + async function handleJoinGame() { + if (!joinContractAddress || !joinGameIdInput) { + setStatus('Enter contract address and game ID'); + return; + } + setIsJoining(true); + setStatus('Joining game...'); + addLog('Attaching to existing contract...', 'pending'); + try { + let gId: bigint; + try { + gId = BigInt(joinGameIdInput); + if (gId <= 0n) throw new Error('must be positive'); + } catch { + setStatus('Invalid game ID — enter a positive integer'); + setIsJoining(false); + return; + } + + const contractAddr = AztecAddress.fromStringUnsafe(joinContractAddress); + const contract = await attachToContract( + wallet, + contractAddr + ); + addLog(`Attached to contract ${contractAddr.toString()}`, 'info'); + addLog(`Joining game ${gId}...`, 'pending'); + const receipt = await joinGame(contract, account, gId); + addLog(`Joined game ${gId} successfully`, 'success', receipt.receipt.txHash?.toString()); + + setStatus('Joined game!'); + onGameJoined(contract, gId); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Error joining game: ${msg}`, 'error'); + } finally { + setIsJoining(false); + } + } + // docs:end:handle-join + + return ( +
+

Game Lobby

+ {status &&

{status}

} + +
+

Create New Game

+ + +
+ +
+

Join Existing Game

+ + + +
+
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/src/components/GameStatus.tsx b/docs/lines/v6/examples/webapp-tutorial/src/components/GameStatus.tsx new file mode 100644 index 00000000000..1dcdb9bf249 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/components/GameStatus.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { AztecAddress } from '@aztec-labs/aztec.js/addresses'; + +interface GameStatusProps { + account: AztecAddress; + gameId: bigint; + currentRound: number; +} + +// docs:start:game-status-component +/** + * Displays the current game status. + * + * In the Pod Racing contract, round progress is tracked publicly + * (which round each player is on), but point allocations are private. + * The currentRound is tracked locally in React state and incremented + * after each successful play_round transaction. + */ +export function GameStatus({ account, gameId, currentRound }: GameStatusProps) { + const addr = account.toString(); + const display = `${addr.slice(0, 10)}...${addr.slice(-6)}`; + + return ( +
+

Game Status

+

Game ID: {gameId.toString()}

+

Playing as: {display}

+

Current Round: {currentRound} / 3

+

+ Your point allocations are stored as private notes. + Opponents cannot see your strategy until you reveal scores. +

+
+ ); +} +// docs:end:game-status-component diff --git a/docs/lines/v6/examples/webapp-tutorial/src/components/NetworkPicker.tsx b/docs/lines/v6/examples/webapp-tutorial/src/components/NetworkPicker.tsx new file mode 100644 index 00000000000..90e79be3d2e --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/components/NetworkPicker.tsx @@ -0,0 +1,29 @@ +// docs:start:network-picker +import React from 'react'; +import type { NetworkType } from '../config'; + +interface NetworkPickerProps { + network: NetworkType; + onNetworkChange: (network: NetworkType) => void; + disabled?: boolean; +} + +/** + * Toggle between local network (uses EmbeddedWallet) and remote (uses wallet extension). + */ +export function NetworkPicker({ network, onNetworkChange, disabled }: NetworkPickerProps) { + return ( +
+ + +
+ ); +} +// docs:end:network-picker diff --git a/docs/lines/v6/examples/webapp-tutorial/src/components/TransactionLog.tsx b/docs/lines/v6/examples/webapp-tutorial/src/components/TransactionLog.tsx new file mode 100644 index 00000000000..aa454018466 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/components/TransactionLog.tsx @@ -0,0 +1,110 @@ +import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react'; + +export type LogLevel = 'info' | 'success' | 'error' | 'pending'; + +export interface LogEntry { + id: number; + timestamp: Date; + message: string; + level: LogLevel; + txHash?: string; +} + +interface LogContextType { + logs: LogEntry[]; + addLog: (message: string, level?: LogLevel, txHash?: string) => void; + clearLogs: () => void; +} + +const LogContext = createContext(null); + +export function useTransactionLog() { + const context = useContext(LogContext); + if (!context) { + throw new Error('useTransactionLog must be used within a LogProvider'); + } + return context; +} + +export function LogProvider({ children }: { children: React.ReactNode }) { + const [logs, setLogs] = useState([]); + const idCounter = useRef(0); + + const addLog = useCallback((message: string, level: LogLevel = 'info', txHash?: string) => { + const entry: LogEntry = { + id: idCounter.current++, + timestamp: new Date(), + message, + level, + txHash, + }; + setLogs(prev => [...prev, entry]); + console.log(`[${level.toUpperCase()}] ${message}`, txHash ? `(${txHash})` : ''); + }, []); + + const clearLogs = useCallback(() => { + setLogs([]); + }, []); + + return ( + + {children} + + ); +} + +function formatTime(date: Date): string { + return date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); +} + +function truncateHash(hash: string): string { + if (hash.length <= 16) return hash; + return `${hash.slice(0, 10)}...${hash.slice(-6)}`; +} + +export function TransactionLog() { + const { logs, clearLogs } = useTransactionLog(); + const logEndRef = useRef(null); + + useEffect(() => { + logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [logs]); + + if (logs.length === 0) { + return null; + } + + return ( +
+
+

Transaction Log

+ +
+
+ {logs.map(entry => ( +
+ {formatTime(entry.timestamp)} + + {entry.level === 'success' && '✓'} + {entry.level === 'error' && '✗'} + {entry.level === 'pending' && '◌'} + {entry.level === 'info' && '•'} + + {entry.message} + {entry.txHash && ( + + {truncateHash(entry.txHash)} + + )} +
+ ))} +
+
+
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/src/components/TwoPlayerLocal.tsx b/docs/lines/v6/examples/webapp-tutorial/src/components/TwoPlayerLocal.tsx new file mode 100644 index 00000000000..27071b8fe98 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/components/TwoPlayerLocal.tsx @@ -0,0 +1,463 @@ +import React, { useState, useEffect } from 'react'; +import { AztecAddress } from '@aztec-labs/aztec.js/addresses'; +import type { Wallet } from '@aztec-labs/aztec.js/wallet'; +import type { PodRacingContract } from '../artifacts/PodRacing'; +import { EmbeddedWallet } from '../embedded-wallet'; +import { getNodeUrl } from '../config'; +import { + deployContract, + createGame, + joinGame, + playRound, + finishGame, + finalizeGame, + attachToContract, +} from '../contract'; +import { TRACK_NAMES, MAX_POINTS_PER_ROUND, TOTAL_ROUNDS } from '../game-constants'; +import { useTransactionLog } from './TransactionLog'; + +type GamePhase = 'setup' | 'playing' | 'reveal' | 'finished'; + +interface PlayerState { + wallet: EmbeddedWallet | null; + account: AztecAddress | null; + contract: PodRacingContract | null; + connected: boolean; + currentRound: number; + allocations: [number, number, number, number, number]; + roundsSubmitted: number[]; + hasRevealed: boolean; + loading: boolean; + status: string; +} + +const initialPlayerState: PlayerState = { + wallet: null, + account: null, + contract: null, + connected: false, + currentRound: 1, + allocations: [2, 2, 2, 2, 1], + roundsSubmitted: [], + hasRevealed: false, + loading: false, + status: '', +}; + +export function TwoPlayerLocal() { + const [player1, setPlayer1] = useState({ ...initialPlayerState }); + const [player2, setPlayer2] = useState({ ...initialPlayerState }); + const [contract, setContract] = useState(null); + const [gameId, setGameId] = useState(BigInt(1)); + const [phase, setPhase] = useState('setup'); + const [winner, setWinner] = useState(null); + const { addLog } = useTransactionLog(); + + // Connect Player 1 + async function connectPlayer1() { + setPlayer1(p => ({ ...p, loading: true, status: 'Initializing PXE...' })); + addLog('[P1] Initializing PXE...', 'pending'); + try { + const nodeUrl = getNodeUrl('local'); + const wallet = await EmbeddedWallet.initialize(nodeUrl); + addLog('[P1] PXE initialized', 'success'); + + setPlayer1(p => ({ ...p, status: 'Connecting test account #1...' })); + addLog('[P1] Connecting test account #1...', 'pending'); + await wallet.connectTestAccount(0); + const account = wallet.getConnectedAccount()!; + addLog(`[P1] Connected: ${account.toString().slice(0, 10)}...`, 'success'); + + setPlayer1(p => ({ + ...p, + wallet, + account, + connected: true, + loading: false, + status: 'Connected!', + })); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setPlayer1(p => ({ ...p, loading: false, status: `Error: ${msg}` })); + addLog(`[P1] Error: ${msg}`, 'error'); + } + } + + // Connect Player 2 + async function connectPlayer2() { + setPlayer2(p => ({ ...p, loading: true, status: 'Initializing PXE...' })); + addLog('[P2] Initializing PXE...', 'pending'); + try { + const nodeUrl = getNodeUrl('local'); + const wallet = await EmbeddedWallet.initialize(nodeUrl); + addLog('[P2] PXE initialized', 'success'); + + setPlayer2(p => ({ ...p, status: 'Connecting test account #2...' })); + addLog('[P2] Connecting test account #2...', 'pending'); + await wallet.connectTestAccount(1); + const account = wallet.getConnectedAccount()!; + addLog(`[P2] Connected: ${account.toString().slice(0, 10)}...`, 'success'); + + setPlayer2(p => ({ + ...p, + wallet, + account, + connected: true, + loading: false, + status: 'Connected!', + })); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setPlayer2(p => ({ ...p, loading: false, status: `Error: ${msg}` })); + addLog(`[P2] Error: ${msg}`, 'error'); + } + } + + // Player 1 creates the game + async function handleCreateGame() { + if (!player1.wallet || !player1.account) return; + + setPlayer1(p => ({ ...p, loading: true, status: 'Deploying contract...' })); + addLog('[P1] Deploying Pod Racing contract...', 'pending'); + + try { + const deployed = await deployContract(player1.wallet as Wallet, player1.account); + addLog(`[P1] Contract deployed: ${deployed.address.toString().slice(0, 10)}...`, 'success'); + setContract(deployed); + + setPlayer1(p => ({ ...p, contract: deployed, status: 'Creating game...' })); + addLog('[P1] Creating game...', 'pending'); + await createGame(deployed, player1.account!, gameId); + addLog(`[P1] Game ${gameId} created`, 'success'); + + setPlayer1(p => ({ ...p, loading: false, status: 'Game created! Waiting for Player 2...' })); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setPlayer1(p => ({ ...p, loading: false, status: `Error: ${msg}` })); + addLog(`[P1] Error: ${msg}`, 'error'); + } + } + + // Player 2 joins the game + async function handleJoinGame() { + if (!player2.wallet || !player2.account || !contract) return; + + setPlayer2(p => ({ ...p, loading: true, status: 'Registering contract...' })); + addLog('[P2] Registering contract...', 'pending'); + + try { + // Player 2 needs to attach to the same contract + const p2Contract = await attachToContract( + player2.wallet as Wallet, + contract.address + ); + addLog('[P2] Contract registered', 'success'); + + setPlayer2(p => ({ ...p, contract: p2Contract, status: 'Joining game...' })); + addLog('[P2] Joining game...', 'pending'); + await joinGame(p2Contract, player2.account!, gameId); + addLog(`[P2] Joined game ${gameId}`, 'success'); + + setPlayer2(p => ({ ...p, loading: false, status: 'Joined! Ready to play.' })); + setPhase('playing'); + addLog('Both players ready - game started!', 'success'); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setPlayer2(p => ({ ...p, loading: false, status: `Error: ${msg}` })); + addLog(`[P2] Error: ${msg}`, 'error'); + } + } + + // Update allocations + function updateAllocation(player: 1 | 2, trackIndex: number, value: number) { + const setter = player === 1 ? setPlayer1 : setPlayer2; + setter(p => { + const newAllocations = [...p.allocations] as [number, number, number, number, number]; + newAllocations[trackIndex] = value; + return { ...p, allocations: newAllocations }; + }); + } + + // Submit round + async function submitRound(player: 1 | 2) { + const state = player === 1 ? player1 : player2; + const setter = player === 1 ? setPlayer1 : setPlayer2; + const tag = player === 1 ? '[P1]' : '[P2]'; + + if (!state.wallet || !state.account || !state.contract) return; + + const total = state.allocations.reduce((sum, v) => sum + v, 0); + if (total >= 10) { + setter(p => ({ ...p, status: `Points must be < 10 (currently ${total})` })); + return; + } + + setter(p => ({ ...p, loading: true, status: `Submitting round ${state.currentRound}...` })); + addLog(`${tag} Submitting round ${state.currentRound} [${state.allocations.join(', ')}]...`, 'pending'); + + try { + await playRound( + state.contract, + state.account!, + gameId, + state.currentRound, + state.allocations + ); + addLog(`${tag} Round ${state.currentRound} submitted`, 'success'); + + const newRound = state.currentRound + 1; + setter(p => ({ + ...p, + loading: false, + status: newRound > TOTAL_ROUNDS ? 'All rounds complete!' : `Round ${newRound}`, + currentRound: newRound, + roundsSubmitted: [...p.roundsSubmitted, state.currentRound], + allocations: [2, 2, 2, 2, 1], // Reset for next round + })); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setter(p => ({ ...p, loading: false, status: `Error: ${msg}` })); + addLog(`${tag} Error: ${msg}`, 'error'); + } + } + + // Reveal scores + async function revealScores(player: 1 | 2) { + const state = player === 1 ? player1 : player2; + const setter = player === 1 ? setPlayer1 : setPlayer2; + const tag = player === 1 ? '[P1]' : '[P2]'; + + if (!state.wallet || !state.account || !state.contract) return; + + setter(p => ({ ...p, loading: true, status: 'Revealing scores...' })); + addLog(`${tag} Revealing scores...`, 'pending'); + + try { + await finishGame(state.contract, state.account!, gameId); + addLog(`${tag} Scores revealed`, 'success'); + + setter(p => ({ + ...p, + loading: false, + status: 'Scores revealed!', + hasRevealed: true, + })); + + // Check if both revealed + const otherState = player === 1 ? player2 : player1; + if (otherState.hasRevealed) { + setPhase('finished'); + addLog('Both players revealed - ready to finalize!', 'info'); + } + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setter(p => ({ ...p, loading: false, status: `Error: ${msg}` })); + addLog(`${tag} Error: ${msg}`, 'error'); + } + } + + // Finalize game + async function handleFinalize() { + if (!player1.wallet || !player1.account || !player1.contract) return; + + setPlayer1(p => ({ ...p, loading: true, status: 'Finalizing game...' })); + addLog('[P1] Finalizing game and determining winner...', 'pending'); + + try { + await finalizeGame(player1.contract, player1.account!, gameId); + addLog('Game finalized!', 'success'); + setWinner('Check contract events for winner!'); + setPlayer1(p => ({ ...p, loading: false, status: 'Game complete!' })); + setPlayer2(p => ({ ...p, status: 'Game complete!' })); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setPlayer1(p => ({ ...p, loading: false, status: `Error: ${msg}` })); + addLog(`Error: ${msg}`, 'error'); + } + } + + // Check for phase transitions when player state changes. + // Uses functional setPhase to avoid a stale-closure on `phase`. + useEffect(() => { + const bothDone = player1.currentRound > TOTAL_ROUNDS && player2.currentRound > TOTAL_ROUNDS; + const bothRevealed = player1.hasRevealed && player2.hasRevealed; + + setPhase(prev => { + if (prev === 'playing' && bothDone) return 'reveal'; + if (prev === 'reveal' && bothRevealed) return 'finished'; + return prev; + }); + }, [player1.currentRound, player2.currentRound, player1.hasRevealed, player2.hasRevealed]); + + return ( +
+
+

Two-Player Local Mode

+ {contract && ( +
+ Contract: {contract.address.toString().slice(0, 12)}... + Game ID: {gameId.toString()} + {phase.toUpperCase()} +
+ )} +
+ +
+ {/* Player 1 Panel */} +
+
+

Player 1

+ {player1.account && ( + {player1.account.toString().slice(0, 10)}... + )} +
+ + {player1.status &&

{player1.status}

} + + {!player1.connected && ( + + )} + + {player1.connected && phase === 'setup' && !contract && ( + + )} + + {phase === 'playing' && player1.currentRound <= TOTAL_ROUNDS && ( + updateAllocation(1, i, v)} + onSubmit={() => submitRound(1)} + /> + )} + + {phase === 'playing' && player1.currentRound > TOTAL_ROUNDS && ( +
Waiting for Player 2 to finish...
+ )} + + {phase === 'reveal' && !player1.hasRevealed && ( + + )} + + {phase === 'reveal' && player1.hasRevealed && ( +
Scores Revealed
+ )} + + {phase === 'finished' && !winner && ( + + )} + + {winner &&
{winner}
} +
+ + {/* Player 2 Panel */} +
+
+

Player 2

+ {player2.account && ( + {player2.account.toString().slice(0, 10)}... + )} +
+ + {player2.status &&

{player2.status}

} + + {!player2.connected && ( + + )} + + {player2.connected && phase === 'setup' && contract && ( + + )} + + {phase === 'playing' && player2.currentRound <= TOTAL_ROUNDS && ( + updateAllocation(2, i, v)} + onSubmit={() => submitRound(2)} + /> + )} + + {phase === 'playing' && player2.currentRound > TOTAL_ROUNDS && ( +
Waiting for Player 1 to finish...
+ )} + + {phase === 'reveal' && !player2.hasRevealed && ( + + )} + + {phase === 'reveal' && player2.hasRevealed && ( +
Scores Revealed
+ )} + + {phase === 'finished' && ( +
+ {winner ? winner : 'Waiting for finalization...'} +
+ )} +
+
+
+ ); +} + +// Sub-component for the game board +function PlayerGameBoard({ + allocations, + currentRound, + loading, + onUpdateAllocation, + onSubmit, +}: { + allocations: [number, number, number, number, number]; + currentRound: number; + loading: boolean; + onUpdateAllocation: (index: number, value: number) => void; + onSubmit: () => void; +}) { + const total = allocations.reduce((sum, v) => sum + v, 0); + + return ( +
+
Round {currentRound} of {TOTAL_ROUNDS}
+
+ {TRACK_NAMES.map((name, i) => ( +
+ {name} + onUpdateAllocation(i, Number(e.target.value))} + disabled={loading} + /> + {allocations[i]} +
+ ))} +
+
= 10 ? 'over-limit' : ''}`}> + Total: {total} / {MAX_POINTS_PER_ROUND} +
+ +
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/src/components/TxStatus.tsx b/docs/lines/v6/examples/webapp-tutorial/src/components/TxStatus.tsx new file mode 100644 index 00000000000..060a436c52f --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/components/TxStatus.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +type TxState = 'idle' | 'sending' | 'proving' | 'confirmed' | 'error'; + +interface TxStatusProps { + state: TxState; + txHash?: string; + error?: string; +} + +// docs:start:tx-status-component +/** + * Displays the current transaction lifecycle stage. + * Transactions flow: send() -> proving -> confirmed (or error). + */ +export function TxStatus({ state, txHash, error }: TxStatusProps) { + if (state === 'idle') return null; + + const messages: Record = { + idle: '', + sending: 'Sending transaction...', + proving: 'Proving transaction (generating ZK proof)...', + confirmed: 'Transaction confirmed!', + error: `Transaction failed: ${error}`, + }; + + return ( +
+

{messages[state]}

+ {txHash &&

Tx: {txHash.slice(0, 10)}...

} +
+ ); +} +// docs:end:tx-status-component diff --git a/docs/lines/v6/examples/webapp-tutorial/src/components/WalletConnect.tsx b/docs/lines/v6/examples/webapp-tutorial/src/components/WalletConnect.tsx new file mode 100644 index 00000000000..0be0d0fa0ad --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/components/WalletConnect.tsx @@ -0,0 +1,198 @@ +// docs:start:wallet-connect-imports +import React, { useState, useEffect } from 'react'; +import type { Wallet, GrantedAccountsCapability } from '@aztec-labs/aztec.js/wallet'; +import type { WalletProvider } from '@aztec-labs/wallet-sdk/manager'; +import type { NetworkType } from '../config'; +import { EmbeddedWallet } from '../embedded-wallet'; +import { discoverWallets, connectToProvider, getAppCapabilities } from '../wallet-connection'; +import { getNodeUrl } from '../config'; +import { useTransactionLog } from './TransactionLog'; + +interface WalletConnectProps { + network: NetworkType; + onWalletConnected: (wallet: Wallet | EmbeddedWallet) => void; +} +// docs:end:wallet-connect-imports + +// docs:start:wallet-connect-component +export function WalletConnect({ network, onWalletConnected }: WalletConnectProps) { + const [status, setStatus] = useState(''); + const [providers, setProviders] = useState([]); + const [verificationEmojis, setVerificationEmojis] = useState(null); + const [testAccountIndex, setTestAccountIndex] = useState(0); + const [loading, setLoading] = useState(false); + const [connected, setConnected] = useState(false); + const [discoveryDone, setDiscoveryDone] = useState(false); + const { addLog } = useTransactionLog(); + + /** Connect using the embedded wallet with a pre-deployed test account */ + async function connectLocal() { + setLoading(true); + setStatus('Initializing PXE (this may take a moment)...'); + addLog('Initializing local PXE client...', 'pending'); + try { + const nodeUrl = getNodeUrl('local'); + addLog(`Connecting to node at ${nodeUrl}`, 'info'); + const wallet = await EmbeddedWallet.initialize(nodeUrl); + addLog('PXE initialized successfully', 'success'); + + setStatus('Connecting test account...'); + addLog(`Connecting test account #${testAccountIndex + 1}...`, 'pending'); + await wallet.connectTestAccount(testAccountIndex); + addLog(`Test account #${testAccountIndex + 1} connected`, 'success'); + + setStatus('Connected!'); + onWalletConnected(wallet); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Connection error: ${msg}`, 'error'); + } finally { + setLoading(false); + } + } + + // docs:start:remote-connect + /** Discover and connect to a browser extension wallet */ + useEffect(() => { + if (network !== 'remote') return; + + setStatus('Discovering wallet extensions...'); + setDiscoveryDone(false); + const { cancel, done } = discoverWallets(31337, 'pod-racing', (found) => { + setProviders(found); + setStatus(`Found ${found.length} wallet(s)`); + }); + + let isMounted = true; + done.then(() => { + if (isMounted) setDiscoveryDone(true); + }).catch((err) => { + if (isMounted) setStatus(`Discovery error: ${err.message}`); + }); + + return () => { + isMounted = false; + cancel(); + }; + }, [network]); + + async function connectExtension(provider: WalletProvider) { + setLoading(true); + setStatus('Establishing secure channel...'); + try { + const { emojis, confirm } = await connectToProvider( + provider, + 'pod-racing' + ); + + // Show emojis for reference — the wallet extension is the authority + // that verifies the emojis match. confirm() is a local operation + // that creates the ExtensionWallet proxy (no message sent to extension). + setVerificationEmojis(emojis); + setStatus('Verify these emojis match in the wallet extension, then approve there.'); + + const wallet = await confirm(); + + // Request capabilities — the dApp declares all permissions it needs upfront. + // The extension shows an approval dialog; this call blocks until the user approves. + setStatus('Requesting permissions from wallet extension...'); + const manifest = getAppCapabilities(); + + const capabilities = await wallet.requestCapabilities(manifest); + setVerificationEmojis(null); + console.log('[WalletConnect] Granted capabilities:', capabilities); + + // Check if accounts were granted + const accountsCap = capabilities.granted.find( + (c): c is GrantedAccountsCapability => c.type === 'accounts' + ); + + if (!accountsCap?.accounts?.length) { + setStatus('No accounts granted. Please approve the capabilities request in the wallet extension.'); + setLoading(false); + return; + } + + setConnected(true); + setStatus('Connected!'); + addLog('Connected to extension wallet', 'success'); + onWalletConnected(wallet); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Connection error: ${msg}`, 'error'); + } finally { + setLoading(false); + } + } + + // docs:end:remote-connect + + return ( +
+

Connect Wallet

+ {status &&

{status}

} + + {network === 'local' && ( +
+ + +
+ )} + + {network === 'remote' && !verificationEmojis && !connected && ( +
+ {providers.length === 0 && ( + discoveryDone + ?

No wallet extensions found. Install an Aztec wallet extension.

+ :

Looking for wallet extensions...

+ )} + {providers.map((provider, i) => ( + + ))} +
+ )} + + {verificationEmojis && ( +
+

Verify Connection

+

Check that these emojis match what your wallet extension shows, then approve there:

+
{verificationEmojis}
+
+ )} + +
+ ); +} +// docs:end:wallet-connect-component diff --git a/docs/lines/v6/examples/webapp-tutorial/src/config.ts b/docs/lines/v6/examples/webapp-tutorial/src/config.ts new file mode 100644 index 00000000000..5cc60942754 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/config.ts @@ -0,0 +1,33 @@ +// docs:start:config +import { createAztecNodeClient } from '@aztec-labs/aztec.js/node'; +import { getPXEConfig } from '@aztec-labs/pxe/config'; +import { createPXE } from '@aztec-labs/pxe/client/lazy'; + + +export type NetworkType = 'local' | 'remote'; + +export function getNodeUrl(network: NetworkType): string { + if (network === 'local') { + return process.env.AZTEC_NODE_URL || 'http://localhost:8080'; + } + // For remote networks, the wallet extension manages the node connection + return process.env.AZTEC_NODE_URL || 'http://localhost:8080'; +} + +/** + * Creates an in-browser PXE instance connected to an Aztec node. + * PXE (Private eXecution Environment) runs locally and handles + * private state, note discovery, and transaction creation. + */ +export async function createLocalPXE(nodeUrl: string) { + const aztecNode = createAztecNodeClient(nodeUrl); + const config = getPXEConfig(); + config.rollupAddress = (await aztecNode.getL1ContractAddresses()).rollupAddress; + const isLocal = nodeUrl.includes('localhost') || nodeUrl.includes('127.0.0.1'); + config.proverEnabled = !isLocal; + const pxe = await createPXE(aztecNode, config, {}); + console.log('PXE connected to node at:', nodeUrl); + + return { pxe, aztecNode }; +} +// docs:end:config diff --git a/docs/lines/v6/examples/webapp-tutorial/src/contract.ts b/docs/lines/v6/examples/webapp-tutorial/src/contract.ts new file mode 100644 index 00000000000..de4b339d9f2 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/contract.ts @@ -0,0 +1,120 @@ +import { AztecAddress } from '@aztec-labs/aztec.js/addresses'; +import type { Wallet } from '@aztec-labs/aztec.js/wallet'; +// @ts-ignore — generated artifact, may not exist until compiled +import { PodRacingContract, PodRacingContractArtifact } from './artifacts/PodRacing'; +import { createSponsoredFeePayment } from './fees'; +import { EmbeddedWallet } from './embedded-wallet'; + +/** + * Deploys a new Pod Racing contract. + * The deployer becomes the game admin. + */ +export async function deployContract(wallet: Wallet, deployer: AztecAddress): Promise { + const paymentMethod = await createSponsoredFeePayment(); + const { contract } = await PodRacingContract.deploy(wallet, deployer) + .send({ from: deployer, fee: { paymentMethod } }); + + console.log('Pod Racing contract deployed at:', contract.address.toString()); + return contract; +} + +/** + * Attaches to an existing deployed Pod Racing contract. + * Registers the contract with PXE so private functions can execute locally. + */ +export async function attachToContract( + wallet: Wallet, + contractAddress: AztecAddress +) { + if (wallet instanceof EmbeddedWallet) { + await wallet.registerContractFromNode(contractAddress, PodRacingContractArtifact); + } + return PodRacingContract.at(contractAddress, wallet); +} + +/** + * Creates a new game with the given game_id. + */ +export async function createGame( + contract: PodRacingContract, + from: AztecAddress, + gameId: bigint +) { + const paymentMethod = await createSponsoredFeePayment(); + const receipt = await contract.methods + .create_game(gameId) + .send({ from, fee: { paymentMethod } }); + + console.log('Game created, tx hash:', receipt.receipt.txHash.toString()); + return receipt; +} + +/** + * Joins an existing game as player2. + */ +export async function joinGame( + contract: PodRacingContract, + from: AztecAddress, + gameId: bigint +) { + const paymentMethod = await createSponsoredFeePayment(); + const receipt = await contract.methods + .join_game(gameId) + .send({ from, fee: { paymentMethod } }); + + console.log('Joined game, tx hash:', receipt.receipt.txHash.toString()); + return receipt; +} + +/** + * Allocates points to 5 tracks for a round (private transaction). + */ +export async function playRound( + contract: PodRacingContract, + from: AztecAddress, + gameId: bigint, + round: number, + tracks: [number, number, number, number, number] +) { + const paymentMethod = await createSponsoredFeePayment(); + const receipt = await contract.methods + .play_round(gameId, round, tracks[0], tracks[1], tracks[2], tracks[3], tracks[4]) + .send({ from, fee: { paymentMethod } }); + + console.log('Round played, tx hash:', receipt.receipt.txHash.toString()); + return receipt; +} + +/** + * Reveals your total scores per track after all rounds are played. + */ +export async function finishGame( + contract: PodRacingContract, + from: AztecAddress, + gameId: bigint +) { + const paymentMethod = await createSponsoredFeePayment(); + const receipt = await contract.methods + .finish_game(gameId) + .send({ from, fee: { paymentMethod } }); + + console.log('Game finished (scores revealed), tx hash:', receipt.receipt.txHash.toString()); + return receipt; +} + +/** + * Determines the winner after both players have revealed. + */ +export async function finalizeGame( + contract: PodRacingContract, + from: AztecAddress, + gameId: bigint +) { + const paymentMethod = await createSponsoredFeePayment(); + const receipt = await contract.methods + .finalize_game(gameId) + .send({ from, fee: { paymentMethod } }); + + console.log('Game finalized, tx hash:', receipt.receipt.txHash.toString()); + return receipt; +} diff --git a/docs/lines/v6/examples/webapp-tutorial/src/embedded-wallet.ts b/docs/lines/v6/examples/webapp-tutorial/src/embedded-wallet.ts new file mode 100644 index 00000000000..6b72bacf7c0 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/embedded-wallet.ts @@ -0,0 +1,130 @@ +// docs:start:embedded-wallet-imports +import { type NoFrom, NO_FROM } from '@aztec-labs/aztec.js/account'; +import { AztecAddress } from '@aztec-labs/aztec.js/addresses'; +import { getContractInstanceFromInstantiationParams } from '@aztec-labs/aztec.js/contracts'; +import { SponsoredFeePaymentMethod } from '@aztec-labs/aztec.js/fee'; +import { Fr } from '@aztec-labs/aztec.js/fields'; +import { SPONSORED_FPC_SALT } from '@aztec-labs/constants'; +import { AccountFeePaymentMethodOptions } from '@aztec-labs/entrypoints/account'; +import { getInitialTestAccountsData } from '@aztec-labs/accounts/testing/lazy'; +import { openTmpStore } from '@aztec-labs/kv-store/sqlite-opfs'; +import type { ContractArtifact } from '@aztec-labs/stdlib/abi'; +import { type CompleteFeeOptionsConfig, type FeeOptions } from '@aztec-labs/wallet-sdk/base-wallet'; +import { EmbeddedWallet as BaseEmbeddedWallet } from '@aztec-labs/wallets/embedded'; +// docs:end:embedded-wallet-imports + +// docs:start:embedded-wallet-class +/** + * A tutorial wallet for local development. + * Extends the official EmbeddedWallet to add SponsoredFPC fee payment + * so users don't need to hold fee tokens. + * + * Inherits from the SDK's EmbeddedWallet which provides: + * - Account creation and persistence via WalletDB + * - Pre-simulation with gas estimation in sendTx + * - Automatic authwitness generation + * - Stub-account simulation (no expensive kernel proving) + */ +export class EmbeddedWallet extends BaseEmbeddedWallet { + connectedAccount: AztecAddress | null = null; + + // docs:start:fee-options + /** + * Uses SponsoredFPC for fee payment by default, so users + * don't need to hold fee tokens. + */ + override async completeFeeOptions(config: CompleteFeeOptionsConfig): Promise { + const base = await super.completeFeeOptions(config); + + if (config.feePayer) { + return base; + } + + const fpc = await EmbeddedWallet.#getSponsoredFPCContract(); + + return { + ...base, + walletFeePaymentMethod: new SponsoredFeePaymentMethod(fpc.instance.address), + accountFeePaymentMethodOptions: + config.from !== NO_FROM ? AccountFeePaymentMethodOptions.EXTERNAL : base.accountFeePaymentMethodOptions, + }; + } + // docs:end:fee-options + + // docs:start:initialize + /** + * Creates a new EmbeddedWallet connected to the given Aztec node URL. + * Sets up an in-browser PXE and registers the SponsoredFPC contract. + */ + static async initialize(nodeUrl: string): Promise { + const isLocal = + nodeUrl.includes('localhost') || nodeUrl.includes('127.0.0.1'); + const wallet = await EmbeddedWallet.create(nodeUrl, { + ephemeral: true, + pxeConfig: { proverEnabled: !isLocal }, + // In the browser, createPXE opens one persistent store per chain and rollup and ignores `ephemeral`, so a + // second wallet on the same page (the two-player view) would collide with the first. Give each its own. + pxeOptions: { store: await openTmpStore(true) }, + }); + + // Register SponsoredFPC so we can pay fees + const fpc = await EmbeddedWallet.#getSponsoredFPCContract(); + await wallet.registerContract(fpc.instance, fpc.artifact); + + return wallet; + } + // docs:end:initialize + + static async #getSponsoredFPCContract() { + const { SponsoredFPCContractArtifact } = await import( + '@aztec-labs/noir-contracts.js/SponsoredFPC' + ); + const instance = await getContractInstanceFromInstantiationParams( + SponsoredFPCContractArtifact, + { salt: new Fr(SPONSORED_FPC_SALT) }, + ); + return { instance, artifact: SponsoredFPCContractArtifact }; + } + + getConnectedAccount() { + return this.connectedAccount; + } + + // docs:start:connect-test-account + /** + * Connects one of the pre-deployed test accounts available on the local network. + * The local network funds its test accounts at genesis as initializerless Schnorr accounts, so they need + * no deployment tx. createSchnorrInitializerlessAccount registers the account and persists it in WalletDB. + */ + async connectTestAccount(index: number) { + const testAccounts = await getInitialTestAccountsData(); + const accountData = testAccounts[index]; + + const accountManager = await this.createSchnorrInitializerlessAccount( + accountData.secret, + accountData.salt, + accountData.signingKey, + ); + + this.connectedAccount = accountManager.address; + return this.connectedAccount; + } + // docs:end:connect-test-account + + /** + * Fetches a contract instance from the Aztec node (onchain) and registers it + * with this wallet's PXE. Required before calling private functions on contracts + * deployed by another wallet/PXE. + */ + async registerContractFromNode( + address: AztecAddress, + artifact: ContractArtifact, + ) { + const instance = await this.aztecNode.getContract(address); + if (!instance) { + throw new Error(`Contract not found onchain at ${address}`); + } + await this.registerContract(instance, artifact); + } + // docs:end:embedded-wallet-class +} diff --git a/docs/lines/v6/examples/webapp-tutorial/src/fees.ts b/docs/lines/v6/examples/webapp-tutorial/src/fees.ts new file mode 100644 index 00000000000..3f2fcd6473f --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/fees.ts @@ -0,0 +1,48 @@ +import { AztecAddress } from '@aztec-labs/aztec.js/addresses'; +import { getContractInstanceFromInstantiationParams } from '@aztec-labs/aztec.js/contracts'; +import { SponsoredFeePaymentMethod } from '@aztec-labs/aztec.js/fee'; +import { Fr } from '@aztec-labs/aztec.js/fields'; +import { SPONSORED_FPC_SALT } from '@aztec-labs/constants'; +import type { PXE } from '@aztec-labs/pxe/server'; + +// docs:start:get-sponsored-fpc +/** + * Returns the SponsoredFPC contract details. + * The SponsoredFPC (Fee Payment Contract) pays transaction fees on behalf of users. + * This is deployed at a well-known address derived from a fixed salt. + */ +export async function getSponsoredFPCContract() { + const { SponsoredFPCContractArtifact } = await import( + '@aztec-labs/noir-contracts.js/SponsoredFPC' + ); + const instance = await getContractInstanceFromInstantiationParams( + SponsoredFPCContractArtifact, + { salt: new Fr(SPONSORED_FPC_SALT) } + ); + return { instance, artifact: SponsoredFPCContractArtifact }; +} +// docs:end:get-sponsored-fpc + +// docs:start:register-fpc +/** + * Registers the SponsoredFPC contract with PXE so it can be used for fee payment. + * This must be called before sending any transactions. + */ +export async function registerSponsoredFPC(pxe: PXE) { + const contract = await getSponsoredFPCContract(); + await pxe.registerContractClass(contract.artifact); + await pxe.registerContract(contract.instance); + return contract.instance.address; +} +// docs:end:register-fpc + +// docs:start:create-fee-payment +/** + * Creates a SponsoredFeePaymentMethod that can be passed as the + * `paymentMethod` option when sending transactions. + */ +export async function createSponsoredFeePayment() { + const contract = await getSponsoredFPCContract(); + return new SponsoredFeePaymentMethod(contract.instance.address); +} +// docs:end:create-fee-payment diff --git a/docs/lines/v6/examples/webapp-tutorial/src/game-constants.ts b/docs/lines/v6/examples/webapp-tutorial/src/game-constants.ts new file mode 100644 index 00000000000..4e1ab1d2854 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/game-constants.ts @@ -0,0 +1,5 @@ +/** Shared game constants used by all game UI components. */ + +export const TRACK_NAMES = ['Straight', 'Canyon', 'Asteroid Field', 'Nebula', 'Wormhole'] as const; +export const MAX_POINTS_PER_ROUND = 9; +export const TOTAL_ROUNDS = 3; diff --git a/docs/lines/v6/examples/webapp-tutorial/src/main.tsx b/docs/lines/v6/examples/webapp-tutorial/src/main.tsx new file mode 100644 index 00000000000..61759e2d85a --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/main.tsx @@ -0,0 +1,12 @@ +// docs:start:main +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import './App.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); +// docs:end:main diff --git a/docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts b/docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts new file mode 100644 index 00000000000..9e8846a86bc --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts @@ -0,0 +1,112 @@ +import { Fr } from '@aztec-labs/aztec.js/fields'; +import type { Wallet, AppCapabilities } from '@aztec-labs/aztec.js/wallet'; +import { WalletManager, type WalletProvider } from '@aztec-labs/wallet-sdk/manager'; +import { hashToEmoji } from '@aztec-labs/wallet-sdk/crypto'; + +// docs:start:wallet-sdk-types +export interface WalletDiscoveryState { + providers: WalletProvider[]; + selectedProvider: WalletProvider | null; + verificationEmojis: string | null; + wallet: Wallet | null; + status: 'idle' | 'discovering' | 'verifying' | 'connected' | 'error'; + error: string | null; +} + +export const initialWalletState: WalletDiscoveryState = { + providers: [], + selectedProvider: null, + verificationEmojis: null, + wallet: null, + status: 'idle', + error: null, +}; +// docs:end:wallet-sdk-types + +// docs:start:discover-wallets +/** + * Starts discovering available wallet extensions. + * Wallet extensions broadcast their availability via window.postMessage. + * Returns a cancel function and calls onUpdate with each discovered wallet. + */ +export function discoverWallets( + chainId: number, + appId: string, + onUpdate: (providers: WalletProvider[]) => void +): { cancel: () => void; done: Promise } { + const manager = WalletManager.configure({ + extensions: { enabled: true }, + }); + + const providers: WalletProvider[] = []; + + const discovery = manager.getAvailableWallets({ + chainInfo: { + chainId: new Fr(chainId), + version: new Fr(1), + }, + appId, + onWalletDiscovered: (provider) => { + // Deduplicate by wallet ID (StrictMode or remounts can cause duplicate discoveries) + if (providers.some(p => p.id === provider.id)) { + return; + } + providers.push(provider); + onUpdate([...providers]); + }, + }); + + return { + cancel: () => discovery.cancel(), + done: discovery.done, + }; +} +// docs:end:discover-wallets + +// docs:start:connect-wallet +/** + * Connects to a discovered wallet provider. + * This establishes a secure encrypted channel using ECDH key exchange. + * The returned emojis should be shown to the user for verification. + */ +export async function connectToProvider( + provider: WalletProvider, + appId: string +): Promise<{ + emojis: string; + confirm: () => Promise; + cancel: () => void; +}> { + console.log('[wallet-connection] Calling establishSecureChannel for provider:', provider.name); + const pending = await provider.establishSecureChannel(appId); + console.log('[wallet-connection] Secure channel established, verificationHash:', pending.verificationHash); + const emojis = hashToEmoji(pending.verificationHash); + console.log('[wallet-connection] Emojis:', emojis); + + return { + emojis, + confirm: () => pending.confirm(), + cancel: () => pending.cancel(), + }; +} +// docs:end:connect-wallet + +// docs:start:app-capabilities +export function getAppCapabilities(): AppCapabilities { + return { + version: '1.0', + metadata: { + name: 'Pod Racing', + version: '1.0.0', + description: 'Pod Racing game on Aztec', + url: window.location.origin, + }, + capabilities: [ + { type: 'accounts', canGet: true }, + { type: 'contracts', contracts: '*', canRegister: true, canGetMetadata: true }, + { type: 'simulation', transactions: { scope: '*' }, utilities: { scope: '*' } }, + { type: 'transaction', scope: '*' }, + ], + }; +} +// docs:end:app-capabilities diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/README.md b/docs/lines/v6/examples/webapp-tutorial/test-extension/README.md new file mode 100644 index 00000000000..9667ef637e0 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/README.md @@ -0,0 +1,192 @@ +# Aztec Tutorial Wallet Extension + +A functional Chrome extension wallet for Aztec that demonstrates how to build a real wallet with: + +- **Encrypted key storage** using PBKDF2 + AES-GCM +- **SponsoredFPC fee payment** so users don't need fee tokens +- **Transaction approval UI** for secure transaction signing +- **Connection approval** with the wallet SDK protocol + +This wallet connects to a local Aztec network and is designed for learning how to build wallet extensions for Aztec. + +## Architecture + +```text +┌─────────────────────────────────────────────────────────────┐ +│ Content Script (content-script.ts) │ +│ - Relays messages between page and background │ +└──────────────────────┬──────────────────────────────────────┘ + │ chrome.runtime messages +┌──────────────────────▼──────────────────────────────────────┐ +│ Service Worker (background.ts) │ +│ - Handles wallet SDK protocol (discovery, key exchange) │ +│ - Routes wallet method calls to offscreen document │ +│ - Manages popup for user approvals │ +└──────────────────────┬──────────────────────────────────────┘ + │ chrome.runtime messages +┌──────────────────────▼──────────────────────────────────────┐ +│ Offscreen Document (offscreen.ts) │ +│ - Runs PXE instance (long-lived, supports WASM) │ +│ - Extends BaseWallet with SponsoredFPC fee payment │ +│ - Manages accounts (create, store, sign) │ +└──────────────────────────────────────────────────────────────┘ +``` + +### Why Offscreen Document? + +Service workers have a 5-minute inactivity timeout and don't fully support long-running WASM operations. The PXE needs persistent state and proof generation, so the extension uses a Manifest V3 offscreen document that can run longer and supports IndexedDB/WASM. + +## Building + +From the `webapp-tutorial` directory: + +```bash +# Full build with Vite (includes PXE and all dependencies) +yarn build:wallet-extension + +# Or using npx directly: +npx vite build --config vite.extension.config.ts +``` + +This compiles: +- `src/background.ts` → `dist/background.js` +- `src/content-script.ts` → `dist/content-script.js` +- `src/offscreen/offscreen.ts` → `dist/offscreen.js` (includes PXE and kernel circuits) +- `src/popup/popup.tsx` → `dist/popup.js` + +**Note:** `node esbuild.extension.mjs` is the primary build command. It runs the full Vite build (background, offscreen, popup) plus esbuild for the content script, and copies static files to the correct locations. + +## Installing in Chrome + +1. Open Chrome and navigate to `chrome://extensions/` +2. Enable "Developer mode" (toggle in top-right corner) +3. Click "Load unpacked" +4. Select the `test-extension` folder +5. The extension "Aztec Tutorial Wallet" should appear + +## Using the Wallet + +### Create an Account + +1. Click the extension icon in Chrome toolbar +2. Click "Create New Account" +3. Enter a password to encrypt your keys +4. The account address will appear (not yet deployed) + +### Deploy an Account + +1. Enter your password in the password field +2. Click "Deploy" next to an undeployed account +3. Wait for the transaction to complete (uses SponsoredFPC for fees) + +### Connect to a dApp + +1. Visit a dApp that uses `@aztec-labs/wallet-sdk` +2. When the dApp calls `aztec.connect()`, a connection request appears +3. Click the extension icon to see the pending approval +4. Click "Connect" to approve + +### Approve Transactions + +1. When a dApp calls `sendTx()`, the extension shows an approval popup +2. Review the transaction details +3. Click "Approve" to sign and submit + +Note: `simulateTx()` does **not** require approval — it auto-executes because simulations are read-only and don't change state. Only `sendTx()` (which submits a state-changing transaction) triggers the approval popup. + +## File Structure + +```text +test-extension/ +├── manifest.json # Chrome extension manifest +├── popup/ +│ ├── popup.html # Popup UI HTML +│ └── popup.css # Popup styles +├── src/ +│ ├── background.ts # Service worker - protocol + routing +│ ├── content-script.ts # Page <-> background relay +│ ├── config.ts # Constants and message types +│ ├── account-utils.ts # Shared account instantiation logic +│ ├── aztec-imports.ts # Lazy import caching for Aztec modules +│ ├── utils.ts # Chrome runtime helpers and utilities +│ ├── offscreen/ +│ │ ├── offscreen.html # Offscreen document HTML +│ │ └── offscreen.ts # PXE host + wallet implementation +│ ├── popup/ +│ │ └── popup.tsx # React popup component +│ └── wallet/ +│ ├── wallet-impl.ts # ExtensionWalletManager - secret generation and encrypted storage +│ └── storage.ts # Encrypted key storage +└── dist/ # Compiled JavaScript (gitignored) +``` + +## Key Implementation Details + +### SponsoredFPC Fee Payment + +The wallet overrides `completeFeeOptions()` to use SponsoredFPC by default: + +```typescript +override async completeFeeOptions(config) { + if (!config.feePayer) { + const sponsoredFPC = await getSponsoredFPCContract(); + return { + walletFeePaymentMethod: new SponsoredFeePaymentMethod(sponsoredFPC.address), + accountFeePaymentMethodOptions: AccountFeePaymentMethodOptions.EXTERNAL, + // ... + }; + } + return super.completeFeeOptions(config); +} +``` + +### Encrypted Key Storage + +Account secrets are encrypted with the user's password: + +1. Generate random salt and IV +2. Derive AES-256-GCM key from password using PBKDF2 (600,000 iterations) +3. Encrypt secret with AES-GCM +4. Store encrypted data in `chrome.storage.local` + +### Message Routing + +1. **Content script** receives messages from page, forwards to background +2. **Background** handles wallet SDK protocol, forwards wallet calls to offscreen +3. **Offscreen** executes wallet methods using PXE, returns results +4. **Popup** sends approval/rejection messages to background + +## Debugging + +### Background Script (Service Worker) + +1. Go to `chrome://extensions/` +2. Find "Aztec Tutorial Wallet" +3. Click "Service Worker" under "Inspect views" +4. Check Console for `[background]` logs + +### Offscreen Document + +1. Open `chrome://extensions/` +2. Find "Aztec Tutorial Wallet" +3. Look for "Offscreen document" in "Inspect views" +4. Check Console for `[offscreen]` logs + +### Content Script + +1. Open DevTools on any page where the extension is active +2. Check Console for `[content-script]` logs (may be filtered by extension) + +## Security Notes + +This is a **tutorial wallet** for learning purposes. For production: + +- Use hardware security modules or secure enclaves for key storage +- Implement proper session management and auto-lock +- Add transaction simulation previews +- Support multiple networks with network switching +- Implement proper error recovery + +## Related Documentation + +See the wallet extension tutorial and webapp tutorial in the Aztec documentation site for full explanations of this implementation and how to build dApps that connect to wallets. diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/manifest.json b/docs/lines/v6/examples/webapp-tutorial/test-extension/manifest.json new file mode 100644 index 00000000000..60240864f49 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/manifest.json @@ -0,0 +1,35 @@ +{ + "manifest_version": 3, + "name": "Aztec Tutorial Wallet", + "version": "1.0.0", + "description": "A functional wallet extension for Aztec with SponsoredFPC fee payment", + "permissions": ["storage", "offscreen", "alarms", "notifications", "windows"], + "host_permissions": [ + "https://*.aztec-labs.com/*", + "https://*.aztec.network/*" + ], + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; object-src 'self'" + }, + "background": { + "service_worker": "dist/background.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["dist/content-script.js"], + "run_at": "document_start" + } + ], + "action": { + "default_popup": "popup/popup.html", + "default_title": "Aztec Tutorial Wallet" + }, + "web_accessible_resources": [ + { + "resources": ["wasm/*"], + "matches": [""] + } + ] +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/popup/popup.css b/docs/lines/v6/examples/webapp-tutorial/test-extension/popup/popup.css new file mode 100644 index 00000000000..f2398656315 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/popup/popup.css @@ -0,0 +1,723 @@ +/* docs:start:popup-styles */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + width: 360px; + min-height: 400px; + background: #1a0a0a; + color: #F2EEE1; +} + +#root { + padding: 12px; +} + +/* Header */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #3d1f1f; +} + +.header h1 { + font-size: 18px; + font-weight: 600; + color: #D4FF28; +} + +.network-badge { + font-size: 11px; + padding: 4px 8px; + background: #2e1515; + border-radius: 4px; + color: #9a8e7e; +} + +.notification-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + padding: 0 6px; + background: #D4FF28; + color: #1a0a0a; + border: none; + border-radius: 11px; + font-size: 11px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} + +.notification-badge:hover:not(:disabled) { + background: #BDE625; +} + +.notification-badge-empty { + background: #2e1515; + color: #9a8e7e; + cursor: default; + opacity: 0.6; +} + +.notification-badge-empty:hover { + background: #2e1515; +} + +/* Sub-header (back button + title for sub-pages) */ +.sub-header { + display: flex; + align-items: center; + margin-bottom: 10px; + gap: 8px; +} + +.back-button { + background: none; + border: none; + color: #D4FF28; + font-size: 18px; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: background 0.15s; +} + +.back-button:hover { + background: #2e1515; +} + +.sub-header-title { + font-size: 15px; + font-weight: 600; +} + +/* Sections */ +.section { + margin-bottom: 14px; +} + +.section-title { + font-size: 12px; + font-weight: 600; + color: #9a8e7e; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 8px; +} + +/* Cards */ +.card { + background: #2e1515; + border-radius: 8px; + padding: 12px; + margin-bottom: 8px; +} + +.card:last-child { + margin-bottom: 0; +} + +/* Account selector pill (main page) */ +.account-selector { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + margin: 0 auto 10px; + padding: 8px 20px; + background: #2e1515; + border: 2px solid #3d1f1f; + border-radius: 20px; + color: #F2EEE1; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: border-color 0.15s; +} + +.account-selector:hover:not(:disabled) { + border-color: #D4FF28; +} + +.account-selector:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.account-selector-name { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.account-selector-arrow { + font-size: 10px; + color: #9a8e7e; +} + +/* Account detail card (main page) */ +.account-detail-card { + background: #2e1515; + border-radius: 12px; + padding: 14px; + text-align: center; +} + +.account-detail-status { + margin-bottom: 12px; +} + +.account-detail-address { + font-family: monospace; + font-size: 11px; + color: #9a8e7e; + word-break: break-all; + line-height: 1.6; + margin-bottom: 12px; + padding: 10px; + background: #1a0a0a; + border-radius: 6px; + cursor: pointer; + transition: color 0.15s; +} + +.account-detail-address:hover { + color: #b3a998; +} + +/* Account switcher items */ +.switcher-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + background: #2e1515; + border: 2px solid transparent; + border-radius: 8px; + margin-bottom: 8px; + cursor: pointer; + transition: border-color 0.15s; +} + +.switcher-item:hover { + border-color: #4d2a2a; +} + +.switcher-item-active { + border-color: #D4FF28; +} + +.switcher-item-info { + flex: 1; + min-width: 0; +} + +.switcher-item-name { + font-size: 14px; + font-weight: 500; + margin-bottom: 2px; +} + +.switcher-item-address { + font-size: 11px; + color: #9a8e7e; + font-family: monospace; +} + +.switcher-item-right { + display: flex; + align-items: center; + gap: 8px; + margin-left: 8px; +} + +.switcher-checkmark { + color: #D4FF28; + font-size: 16px; + font-weight: bold; +} + +/* Account status badges */ +.account-status { + font-size: 10px; + padding: 2px 6px; + border-radius: 3px; +} + +.status-deployed { + background: #1a3d1a; + color: #4ade80; +} + +.status-pending { + background: #4d3d1a; + color: #fbbf24; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + border: none; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: #D4FF28; + color: #1a0a0a; +} + +.btn-primary:hover:not(:disabled) { + background: #BDE625; +} + +.btn-secondary { + background: #3d1f1f; + color: #d9d3c7; +} + +.btn-secondary:hover:not(:disabled) { + background: #4d2a2a; +} + +.btn-danger { + background: #6b1a1a; + color: #f87171; +} + +.btn-danger:hover:not(:disabled) { + background: #7b2a2a; +} + +.btn-small { + padding: 4px 10px; + font-size: 11px; +} + +.btn-block { + width: 100%; +} + +/* Button groups */ +.btn-group { + display: flex; + gap: 8px; +} + +/* Approval cards */ +.approval-card { + background: #2e1515; + border: 1px solid #4d2a2a; + border-radius: 8px; + padding: 12px; + margin-bottom: 8px; +} + +.approval-header { + display: flex; + align-items: center; + margin-bottom: 8px; +} + +.approval-icon { + width: 32px; + height: 32px; + background: #3d1f1f; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; + font-size: 16px; +} + +.approval-origin { + font-size: 14px; + font-weight: 500; +} + +.approval-type { + font-size: 12px; + color: #9a8e7e; +} + +.approval-details { + background: #1a0a0a; + border-radius: 6px; + padding: 12px; + margin-bottom: 8px; + font-size: 12px; +} + +.detail-row { + display: flex; + justify-content: space-between; + margin-bottom: 6px; +} + +.detail-row:last-child { + margin-bottom: 0; +} + +.detail-label { + color: #9a8e7e; +} + +.detail-value { + font-family: monospace; + color: #d9d3c7; + overflow: hidden; + text-overflow: ellipsis; + max-width: 180px; +} + +/* Forms */ +.form-group { + margin-bottom: 12px; +} + +.form-label { + display: block; + font-size: 12px; + color: #9a8e7e; + margin-bottom: 6px; +} + +.form-input { + width: 100%; + padding: 10px 12px; + background: #1a0a0a; + border: 1px solid #3d1f1f; + border-radius: 6px; + color: #F2EEE1; + font-size: 14px; +} + +.form-input:focus { + outline: none; + border-color: #D4FF28; +} + +.form-input::placeholder { + color: #6b5f50; +} + +/* Empty state */ +.empty-state { + text-align: center; + padding: 24px; + color: #9a8e7e; +} + +.empty-icon { + font-size: 32px; + margin-bottom: 8px; +} + +.empty-text { + font-size: 13px; +} + +/* Loading */ +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + color: #9a8e7e; +} + +.spinner { + width: 20px; + height: 20px; + border: 2px solid #3d1f1f; + border-top-color: #D4FF28; + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-right: 8px; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Messages */ +.message { + padding: 10px 12px; + border-radius: 6px; + font-size: 12px; + margin-bottom: 12px; +} + +.message-error { + background: #4d1a1a; + color: #f87171; +} + +.message-success { + background: #1a3d1a; + color: #4ade80; +} + +/* Task banner */ +.task-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + background: #2e1515; + border: 1px solid #D4FF28; + border-radius: 6px; + margin-bottom: 12px; + font-size: 13px; + color: #D4FF28; +} + +/* Task progress stage (PXE execution stage) */ +.task-progress-stage { + font-size: 11px; + color: #9a8e7e; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Connection status in header */ +.connection-status-wrapper { + position: relative; +} + +.connection-status { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 8px; + background: #2e1515; + border: none; + border-radius: 4px; + color: #9a8e7e; + font-size: 11px; + white-space: nowrap; +} + +.connection-status-connected { + color: #4ade80; +} + +.connection-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #5a4a3a; + flex-shrink: 0; +} + +.connection-dot-active { + background: #4ade80; +} + +.connection-label { + max-width: 80px; + overflow: hidden; + text-overflow: ellipsis; +} + +.connection-count { + font-size: 10px; + opacity: 0.7; +} + +.connection-dropdown { + position: absolute; + top: 100%; + right: 0; + margin-top: 4px; + min-width: 200px; + background: #2e1515; + border: 1px solid #4d2a2a; + border-radius: 6px; + padding: 6px; + z-index: 100; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +.connection-dropdown-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 6px 8px; + border-radius: 4px; +} + +.connection-dropdown-item:hover { + background: #3d1f1f; +} + +.connection-dropdown-origin { + font-size: 12px; + color: #d9d3c7; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + +/* Emoji verification */ +.emoji-verification-card { + background: #1a0a0a; + border-radius: 6px; + padding: 16px; + margin-bottom: 12px; + text-align: center; +} + +.emoji-verification-label { + font-size: 12px; + color: #9a8e7e; + margin-bottom: 12px; +} + +.emoji-grid { + font-size: 28px; + letter-spacing: 4px; + line-height: 1.6; +} + +/* Transaction details */ +.tx-calls { + margin-top: 8px; +} + +.tx-call { + background: #1a0a0a; + border-radius: 4px; + padding: 8px; + margin-bottom: 6px; + font-size: 11px; +} + +.tx-call-header { + font-weight: 500; + margin-bottom: 4px; + color: #D4FF28; +} + +.tx-call-arg { + font-family: monospace; + color: #9a8e7e; + word-break: break-all; +} +/* Expandable capability details */ +.expandable-toggle { + display: inline-flex; + align-items: center; + gap: 4px; + background: none; + border: none; + color: #D4FF28; + font-size: 11px; + cursor: pointer; + padding: 2px 0; + font-family: monospace; +} + +.expandable-toggle:hover { + text-decoration: underline; +} + +.expandable-items { + margin-top: 4px; + padding-left: 8px; + border-left: 2px solid #3d1f1f; +} + +.expandable-item { + font-family: monospace; + font-size: 10px; + color: #9a8e7e; + word-break: break-all; + padding: 2px 0; +} + +/* Settings gear button */ +.settings-gear { + background: transparent; + border: none; + color: #9a8e7e; + font-size: 16px; + cursor: pointer; + padding: 4px 6px; + border-radius: 4px; + transition: all 0.15s; +} + +.settings-gear:hover { + color: #D4FF28; + background: #2e1515; +} + +/* Settings sections */ +.settings-section { + background: #2e1515; + border-radius: 8px; + padding: 16px; + margin-bottom: 12px; +} + +.settings-section-title { + font-size: 14px; + font-weight: 600; + margin-bottom: 6px; +} + +.settings-description { + font-size: 12px; + color: #9a8e7e; +} + +.settings-version { + display: flex; + justify-content: space-between; + font-size: 12px; + padding: 8px 0; +} + +/* Import preview card */ +.import-preview { + background: #1a0a0a; + border-radius: 6px; + padding: 12px; +} + +.import-preview-header { + font-weight: 600; + color: #d9d3c7; + margin-bottom: 4px; +} +/* docs:end:popup-styles */ diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/popup/popup.html b/docs/lines/v6/examples/webapp-tutorial/test-extension/popup/popup.html new file mode 100644 index 00000000000..776e717cf9d --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/popup/popup.html @@ -0,0 +1,13 @@ + + + + + + Aztec Tutorial Wallet + + + +
+ + + diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/account-utils.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/account-utils.ts new file mode 100644 index 00000000000..489dbe482a7 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/account-utils.ts @@ -0,0 +1,61 @@ +/** + * Shared account contract instantiation logic. + * + * Centralizes the key derivation + contract setup sequence used by + * account creation, deployment, and registration. + */ + +import { getAztecCore } from "./aztec-imports"; + +/** + * Derives keys and instantiates a Schnorr account contract from a secret and salt. + * + * Returns everything needed for PXE registration and deployment: + * - secretFr / saltFr — field element versions of the inputs + * - publicKeys — derived public keys + * - signingKey — Schnorr signing key + * - accountContract — the SchnorrAccountContract instance + * - artifact — the contract artifact + * - instance — the contract instance with computed address + */ +export async function instantiateAccount(secret: string, salt: string) { + const { + Fr, + GrumpkinScalar, + deriveKeys, + deriveSecretKeyFromSigningKey, + SchnorrAccountContract, + getContractInstanceFromInstantiationParams, + } = await getAztecCore(); + + const signingKey = GrumpkinScalar.fromString(secret); + const saltFr = Fr.fromString(salt); + + const secretFr = await deriveSecretKeyFromSigningKey(signingKey); + const { publicKeys } = await deriveKeys(secretFr); + const accountContract = new SchnorrAccountContract(signingKey); + + const initInfo = await accountContract.getInitializationFunctionAndArgs(); + const { constructorName, constructorArgs } = initInfo ?? { + constructorName: undefined, + constructorArgs: undefined, + }; + const artifact = await accountContract.getContractArtifact(); + + const instance = await getContractInstanceFromInstantiationParams(artifact, { + constructorArtifact: constructorName, + constructorArgs, + salt: saltFr, + publicKeys, + }); + + return { + secretFr, + saltFr, + publicKeys, + signingKey, + accountContract, + artifact, + instance, + }; +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/aztec-imports.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/aztec-imports.ts new file mode 100644 index 00000000000..237b319e917 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/aztec-imports.ts @@ -0,0 +1,142 @@ +/** + * Centralized lazy import cache for Aztec SDK modules. + * + * The offscreen document uses dynamic imports to keep startup fast — we don't + * want to load Barretenberg, Schnorr account contracts, etc. until the user + * actually triggers an operation. But the same imports were scattered across + * 5+ locations with no shared cache. + * + * This module loads all needed Aztec imports once, caches them, and provides + * a typed interface for consumers. The module system caches the underlying + * import() calls, but this provides a single entry point and avoids repeating + * the destructuring boilerplate. + */ + +/** Core imports needed for account operations (key derivation, contract setup) */ +export interface AztecCoreImports { + Fr: typeof import("@aztec-labs/aztec.js/fields").Fr; + GrumpkinScalar: typeof import("@aztec-labs/aztec.js/fields").GrumpkinScalar; + AztecAddress: typeof import("@aztec-labs/aztec.js/addresses").AztecAddress; + NO_FROM: typeof import("@aztec-labs/aztec.js/account").NO_FROM; + deriveKeys: typeof import("@aztec-labs/stdlib/keys").deriveKeys; + deriveSecretKeyFromSigningKey: typeof import("@aztec-labs/accounts/utils").deriveSecretKeyFromSigningKey; + SchnorrAccountContract: typeof import("@aztec-labs/accounts/schnorr/lazy").SchnorrAccountContract; + getContractInstanceFromInstantiationParams: typeof import("@aztec-labs/aztec.js/contracts").getContractInstanceFromInstantiationParams; + AccountManager: typeof import("@aztec-labs/aztec.js/wallet").AccountManager; +} + +/** Additional imports for the wallet runtime (BaseWallet, serialization) */ +export interface AztecWalletImports extends AztecCoreImports { + BaseWallet: typeof import("@aztec-labs/wallet-sdk/base-wallet").BaseWallet; + WalletSchema: typeof import("@aztec-labs/aztec.js/wallet").WalletSchema; + jsonStringify: typeof import("@aztec-labs/foundation/json-rpc").jsonStringify; + schemaHasMethod: typeof import("@aztec-labs/foundation/schemas").schemaHasMethod; + getSchemaParameters: typeof import("@aztec-labs/foundation/schemas").getSchemaParameters; + parseWithOptionals: typeof import("@aztec-labs/foundation/schemas").parseWithOptionals; +} + +/** Deploy-specific imports (fee payment, SponsoredFPC) */ +export interface AztecDeployImports extends AztecCoreImports { + SponsoredFeePaymentMethod: typeof import("@aztec-labs/aztec.js/fee").SponsoredFeePaymentMethod; + SponsoredFPCContract: typeof import("@aztec-labs/noir-contracts.js/SponsoredFPC").SponsoredFPCContract; + SPONSORED_FPC_SALT: typeof import("@aztec-labs/constants").SPONSORED_FPC_SALT; +} + +let coreCache: AztecCoreImports | null = null; +let walletCache: AztecWalletImports | null = null; +let deployCache: AztecDeployImports | null = null; + +/** + * Loads the core Aztec imports needed for account operations. + * Cached after first call. + */ +export async function getAztecCore(): Promise { + if (coreCache) return coreCache; + + const [ + fields, + addresses, + account, + keys, + accountUtils, + schnorr, + contracts, + wallet, + ] = await Promise.all([ + import("@aztec-labs/aztec.js/fields"), + import("@aztec-labs/aztec.js/addresses"), + import("@aztec-labs/aztec.js/account"), + import("@aztec-labs/stdlib/keys"), + import("@aztec-labs/accounts/utils"), + import("@aztec-labs/accounts/schnorr/lazy"), + import("@aztec-labs/aztec.js/contracts"), + import("@aztec-labs/aztec.js/wallet"), + ]); + + coreCache = { + Fr: fields.Fr, + GrumpkinScalar: fields.GrumpkinScalar, + AztecAddress: addresses.AztecAddress, + NO_FROM: account.NO_FROM, + deriveKeys: keys.deriveKeys, + deriveSecretKeyFromSigningKey: accountUtils.deriveSecretKeyFromSigningKey, + SchnorrAccountContract: schnorr.SchnorrAccountContract, + getContractInstanceFromInstantiationParams: + contracts.getContractInstanceFromInstantiationParams, + AccountManager: wallet.AccountManager, + }; + + return coreCache; +} + +/** + * Loads the wallet runtime imports (core + BaseWallet, serialization). + * Cached after first call. + */ +export async function getAztecWallet(): Promise { + if (walletCache) return walletCache; + + const [core, bw, walletMod, jsonRpc, schemas] = await Promise.all([ + getAztecCore(), + import("@aztec-labs/wallet-sdk/base-wallet"), + import("@aztec-labs/aztec.js/wallet"), + import("@aztec-labs/foundation/json-rpc"), + import("@aztec-labs/foundation/schemas"), + ]); + + walletCache = { + ...core, + BaseWallet: bw.BaseWallet, + WalletSchema: walletMod.WalletSchema, + jsonStringify: jsonRpc.jsonStringify, + schemaHasMethod: schemas.schemaHasMethod, + getSchemaParameters: schemas.getSchemaParameters, + parseWithOptionals: schemas.parseWithOptionals, + }; + + return walletCache; +} + +/** + * Loads the deploy-specific imports (core + fee payment, SponsoredFPC). + * Cached after first call. + */ +export async function getAztecDeploy(): Promise { + if (deployCache) return deployCache; + + const [core, fee, sponsoredFpc, constants] = await Promise.all([ + getAztecCore(), + import("@aztec-labs/aztec.js/fee"), + import("@aztec-labs/noir-contracts.js/SponsoredFPC"), + import("@aztec-labs/constants"), + ]); + + deployCache = { + ...core, + SponsoredFeePaymentMethod: fee.SponsoredFeePaymentMethod, + SponsoredFPCContract: sponsoredFpc.SponsoredFPCContract, + SPONSORED_FPC_SALT: constants.SPONSORED_FPC_SALT, + }; + + return deployCache; +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts new file mode 100644 index 00000000000..1a95e66b09b --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts @@ -0,0 +1,1442 @@ +/** + * Background service worker for the Aztec Tutorial Wallet. + * + * Handles: + * - Wallet SDK protocol (discovery, key exchange, encrypted wallet method calls) + * - Offscreen document lifecycle management (with retry on teardown) (#15) + * - Message routing between content scripts and offscreen document + * - Background task tracking with push notifications to popup via ports + * - State persistence via chrome.storage.session (survives SW restart) (#8) + * - Auto-lock via chrome.alarms (#28) + */ + +import { + BackgroundConnectionHandler, + type BackgroundTransport, + type BackgroundConnectionCallbacks, + type ActiveSession, +} from "@aztec-labs/wallet-sdk/extension/handlers"; + +import { + WALLET_CONFIG, + MessageTarget, + MessageTypes, + AUTO_LOCK_MINUTES, + log, +} from "./config"; +import { getErrorMessage } from "./utils"; +import { STORAGE_KEYS } from "./wallet/storage"; +import type { + PendingTransaction, + PendingSessionVerification, + PendingCapabilities, + BackgroundTask, +} from "./shared-types"; + +// docs:start:offscreen-management +let offscreenCreating: Promise | null = null; + +/** + * Ensures the offscreen document exists. Creates it if needed. + * The offscreen document hosts the PXE and wallet implementation. + */ +async function ensureOffscreenDocument(): Promise { + const existingContexts = await chrome.runtime.getContexts({ + contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT], + }); + + if (existingContexts.length > 0) { + return; + } + + if (offscreenCreating) { + await offscreenCreating; + return; + } + + const offscreenUrl = chrome.runtime.getURL("dist/offscreen.html"); + log.debug("[background] Creating offscreen document:", offscreenUrl); + + offscreenCreating = chrome.offscreen.createDocument({ + url: offscreenUrl, + reasons: [chrome.offscreen.Reason.WORKERS], + justification: "Aztec PXE requires long-running WASM operations", + }); + + await offscreenCreating; + offscreenCreating = null; + log.debug("[background] Offscreen document created"); +} +// docs:end:offscreen-management + +// docs:start:send-to-offscreen +/** + * Persistent port to the offscreen document. + * Unlike chrome.runtime.sendMessage() (broadcast), a port gives us: + * - Point-to-point channel (no broadcast to all extension pages) + * - Automatic disconnect detection (offscreen teardown) + * - No `return true`/`false` landmine for async responses + */ +let offscreenPort: chrome.runtime.Port | null = null; +const pendingOffscreenCalls = new Map< + string, + { + resolve: (value: any) => void; + reject: (error: Error) => void; + timer: ReturnType; + } +>(); +let offscreenMessageId = 0; + +const OFFSCREEN_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes + +function connectOffscreenPort() { + const port = chrome.runtime.connect({ name: "offscreen" }); + offscreenPort = port; + + port.onMessage.addListener((message: any) => { + // Progress updates — relay to popup + if (message.type === "task-progress") { + const runningTask = backgroundTasks.find((t) => t.status === "running"); + if (runningTask) { + runningTask.progress = message.stage; + notifyPopup({ type: "task-update", task: { ...runningTask } }); + } + return; + } + + // Request/response correlation + const pending = pendingOffscreenCalls.get(message.messageId); + if (!pending) return; + pendingOffscreenCalls.delete(message.messageId); + clearTimeout(pending.timer); + + if (message.success) { + pending.resolve(message.result); + } else { + pending.reject(new Error(message.error || "Unknown error")); + } + }); + + port.onDisconnect.addListener(() => { + log.debug("[background] Offscreen port disconnected"); + offscreenPort = null; + // Reject all pending calls — sendToOffscreen will retry + for (const [id, pending] of pendingOffscreenCalls) { + clearTimeout(pending.timer); + pending.reject(new Error("Offscreen port disconnected")); + pendingOffscreenCalls.delete(id); + } + }); +} + +/** + * Sends a message to the offscreen document and waits for response. + * Uses a persistent port with request/response correlation via messageId. + * Retries once if the offscreen document was torn down. (#15) + */ +async function sendToOffscreen(message: any, _retried = false): Promise { + await ensureOffscreenDocument(); + + if (!offscreenPort) { + connectOffscreenPort(); + } + + const messageId = `off-${++offscreenMessageId}`; + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + pendingOffscreenCalls.delete(messageId); + reject(new Error(`Offscreen call timed out: ${message.type}`)); + }, OFFSCREEN_TIMEOUT_MS); + + pendingOffscreenCalls.set(messageId, { resolve, reject, timer }); + + try { + if (!offscreenPort) { + throw new Error("Offscreen port not connected"); + } + offscreenPort.postMessage({ ...message, messageId }); + } catch (err: unknown) { + pendingOffscreenCalls.delete(messageId); + clearTimeout(timer); + + // Port may have disconnected — retry once + if (!_retried) { + log.warn("[background] Offscreen port send failed, retrying..."); + offscreenPort = null; + offscreenCreating = null; + sendToOffscreen(message, true).then(resolve, reject); + } else { + reject(err instanceof Error ? err : new Error(String(err))); + } + } + }); +} +// docs:end:send-to-offscreen + +// Store pending transactions, session verifications, and capability requests. +// Discovery and session tracking uses only the SDK handler. +let pendingTransactions: PendingTransaction[] = []; +let pendingSessionVerifications: PendingSessionVerification[] = []; +let pendingCapabilities: PendingCapabilities[] = []; +let walletUnlocked = false; + +/** Sessions that have had capabilities approved. Methods are blocked until this is set. */ +const capabilitiesApprovedSessions = new Set(); + +/** + * Messages queued while awaiting emoji verification. + * The extension must confirm the session before any wallet method calls are processed. + * This prevents the dApp from bypassing verification by sending messages immediately. + */ +const queuedMessages: Map = + new Map(); + +/** + * Trusted origins persistence for auto-reconnect. (#30) + * + * When a user confirms emoji verification for a dApp, we remember the + * origin+appId in chrome.storage.local. On subsequent page refreshes, + * discovery and emoji verification are auto-approved (a fresh ECDH key + * exchange still happens every time for security). Disconnecting a site + * removes it from the trusted list. + */ +const TRUSTED_ORIGINS_KEY = "aztec_trusted_origins"; + +interface TrustedOrigin { + origin: string; + appId: string; + trustedAt: number; + grantedCapabilities?: Array<{ type: string; [key: string]: any }>; +} + +async function getTrustedOrigins(): Promise { + const data = await chrome.storage.local.get(TRUSTED_ORIGINS_KEY); + return data[TRUSTED_ORIGINS_KEY] || []; +} + +async function addTrustedOrigin(origin: string, appId: string): Promise { + const trusted = await getTrustedOrigins(); + if (!trusted.some((t) => t.origin === origin && t.appId === appId)) { + trusted.push({ origin, appId, trustedAt: Date.now() }); + await chrome.storage.local.set({ [TRUSTED_ORIGINS_KEY]: trusted }); + } +} + +async function removeTrustedOrigin( + origin: string, + appId: string, +): Promise { + const trusted = await getTrustedOrigins(); + const filtered = trusted.filter( + (t) => !(t.origin === origin && t.appId === appId), + ); + await chrome.storage.local.set({ [TRUSTED_ORIGINS_KEY]: filtered }); +} + +async function isTrustedOrigin( + origin: string, + appId: string, +): Promise { + const trusted = await getTrustedOrigins(); + return trusted.some((t) => t.origin === origin && t.appId === appId); +} + +async function getStoredCapabilities( + origin: string, + appId: string, +): Promise | null> { + const trusted = await getTrustedOrigins(); + const entry = trusted.find((t) => t.origin === origin && t.appId === appId); + return entry?.grantedCapabilities ?? null; +} + +/** + * Persists critical state to chrome.storage.session. (#8) + * + * chrome.storage.session is: + * - Encrypted at rest + * - Scoped to the browser session (cleared when browser closes) + * - Survives service worker restarts (unlike in-memory variables) + * + * We persist: walletUnlocked, pendingTransactions. + * We do NOT persist: CryptoKey (lives in offscreen), active SDK sessions + * (the SDK handler can't be serialized; dApps must reconnect after SW restart). + */ +async function persistState(): Promise { + try { + await chrome.storage.session.set({ + sw_walletUnlocked: walletUnlocked, + sw_pendingTransactions: pendingTransactions, + }); + } catch (err) { + log.warn("[background] Failed to persist state:", err); + } +} + +async function restoreState(): Promise { + try { + const data = await chrome.storage.session.get([ + "sw_walletUnlocked", + "sw_pendingTransactions", + ]); + walletUnlocked = data.sw_walletUnlocked ?? false; + pendingTransactions = data.sw_pendingTransactions ?? []; + log.debug( + "[background] Restored state: unlocked =", + walletUnlocked, + ", pendingTx =", + pendingTransactions.length, + ); + + // Validate: if the offscreen document was torn down, the cached master key is gone. + // Probe offscreen to confirm — if it fails, the wallet needs re-unlock. + if (walletUnlocked) { + try { + await sendToOffscreen({ type: MessageTypes.GET_ACCOUNTS }); + } catch { + log.warn( + "[background] Offscreen unreachable after restore — marking wallet as locked", + ); + walletUnlocked = false; + await persistState(); + } + } + } catch (err) { + log.warn("[background] Failed to restore state:", err); + } +} + +/** + * Background task tracker for long-running operations. + * Tasks survive popup close/reopen. The popup receives real-time updates + * via a persistent port connection. + */ +let backgroundTasks: BackgroundTask[] = []; + +function startBackgroundTask(type: string, promise: Promise): string { + const id = `${type}-${Date.now()}`; + const task: BackgroundTask = { + id, + type, + status: "running", + startedAt: Date.now(), + }; + backgroundTasks.push(task); + + promise + .then((result) => { + task.status = "success"; + task.result = result; + notifyPopup({ type: "task-update", task: { ...task } }); + }) + .catch((error) => { + task.status = "error"; + task.error = getErrorMessage(error); + notifyPopup({ type: "task-update", task: { ...task } }); + }); + + notifyPopup({ type: "task-update", task: { ...task } }); + return id; +} + +/** + * Cleans up completed tasks after 5 minutes. (#13) + * 5 minutes gives the popup time to see completed tasks even if it was closed briefly. + */ +function cleanupTasks() { + const FIVE_MINUTES = 5 * 60 * 1000; + const cutoff = Date.now() - FIVE_MINUTES; + backgroundTasks = backgroundTasks.filter( + (t) => t.status === "running" || t.startedAt > cutoff, + ); +} + +/** + * Persistent port connection to the popup. + * Allows the background to push real-time updates without polling. + * The port disconnects automatically when the popup closes. + */ +let popupPort: chrome.runtime.Port | null = null; + +chrome.runtime.onConnect.addListener((port) => { + if (port.name !== "popup") return; + + log.debug("[background] Popup connected"); + popupPort = port; + + port.onDisconnect.addListener(() => { + log.debug("[background] Popup disconnected"); + popupPort = null; + }); + + // Send current state immediately on connect + pushStateToPopup(); +}); + +function notifyPopup(message: any) { + if (popupPort) { + try { + popupPort.postMessage(message); + } catch { + popupPort = null; + } + } +} + +function pushStateToPopup() { + notifyPopup({ type: "state", data: getFullState() }); +} + +/** + * Opens the popup via chrome.windows.create() — works without a user gesture. + * Used for discovery requests which are triggered by content script messages + * where chrome.action.openPopup() silently no-ops. + */ +function openPopupWindow() { + if (popupPort) { + pushStateToPopup(); + return; + } + chrome.windows + .create({ + url: chrome.runtime.getURL("popup/popup.html"), + type: "popup", + width: 400, + height: 600, + focused: true, + }) + .catch((err) => { + log.error("[background] Failed to create popup window:", err); + }); +} + +/** + * Opens the popup via chrome.action.openPopup() with windows.create() fallback. + * Used for flows that originate from a user gesture in the popup (tx approval, + * session verification) where openPopup() works reliably. + */ +function openPopupWithFallback() { + if (popupPort) { + pushStateToPopup(); + return; + } + chrome.action.openPopup().catch(() => { + chrome.windows.create({ + url: chrome.runtime.getURL("popup/popup.html"), + type: "popup", + width: 400, + height: 600, + focused: true, + }); + }); +} + +function getFullState() { + cleanupTasks(); + const connectedSites = handler.getActiveSessions().map((s) => ({ + sessionId: s.sessionId, + origin: s.origin, + appId: s.appId, + connectedAt: s.connectedAt, + })); + + return { + discoveries: handler.getPendingDiscoveries(), + transactions: pendingTransactions, + pendingSessionVerifications, + pendingCapabilities, + connectedSites, + tasks: backgroundTasks, + }; +} + +/** + * Auto-lock via chrome.alarms. (#28) + * Resets the timer on every popup interaction. + */ +const AUTO_LOCK_ALARM = "aztec-auto-lock"; + +function resetAutoLockTimer() { + if (walletUnlocked) { + chrome.alarms.create(AUTO_LOCK_ALARM, { + delayInMinutes: AUTO_LOCK_MINUTES, + }); + } +} + +chrome.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name === AUTO_LOCK_ALARM) { + log.debug("[background] Auto-lock triggered"); + walletUnlocked = false; + await persistState(); + // Tell offscreen to clear the cached CryptoKey + try { + await sendToOffscreen({ type: MessageTypes.LOCK_WALLET }); + } catch { + // Offscreen may not exist yet + } + pushStateToPopup(); + } +}); + +/** + * Updates the extension badge to show pending items count. + */ +function updateBadge() { + const count = + pendingTransactions.length + + handler.getPendingDiscoveryCount() + + pendingSessionVerifications.length + + pendingCapabilities.length; + chrome.action.setBadgeText({ text: count > 0 ? count.toString() : "" }); + chrome.action.setBadgeBackgroundColor({ color: "#FF6B00" }); + pushStateToPopup(); + persistState(); +} + +// docs:start:transport +const transport: BackgroundTransport = { + sendToTab: (tabId, message) => { + log.debug( + "[background] sendToTab:", + tabId, + message.type, + message.sessionId, + ); + chrome.tabs.sendMessage(tabId, message); + }, + addContentListener: (handler) => { + chrome.runtime.onMessage.addListener((message, sender) => { + // Skip targeted messages (popup, offscreen), storage proxy, and progress updates + if (message.target) return; + if (message.type === "storage-get" || message.type === "storage-set") + return; + + log.debug( + "[background] Content message received:", + message.origin, + message.type, + "from tab:", + sender.tab?.id, + ); + handler(message, { + tab: sender.tab + ? { id: sender.tab.id, url: sender.tab.url } + : undefined, + }); + }); + }, +}; +// docs:end:transport + +/** + * Returns the list of accounts grantable to dApps — only the active account, + * or the first account as fallback. Used by both processWalletMessage (for + * getAccounts/getRegisteredAccounts filtering) and APPROVE_CAPABILITIES. + */ +async function getGrantableAccounts(): Promise< + Array<{ alias: string; item: string }> +> { + const [accountsData, activeData] = await Promise.all([ + chrome.storage.local.get(STORAGE_KEYS.ACCOUNTS), + chrome.storage.local.get(STORAGE_KEYS.ACTIVE_ACCOUNT), + ]); + const allAccounts = accountsData[STORAGE_KEYS.ACCOUNTS] || []; + const activeAddress = activeData[STORAGE_KEYS.ACTIVE_ACCOUNT]; + const activeAccount = allAccounts.find( + (a: any) => a.address === activeAddress, + ); + if (activeAccount) { + return [{ alias: activeAccount.alias, item: activeAccount.address }]; + } + return allAccounts + .slice(0, 1) + .map((a: any) => ({ alias: a.alias, item: a.address })); +} + +/** + * Processes a wallet method call from the ExtensionWallet proxy. + * Extracted so it can be called from onWalletMessage and when flushing the queue. + */ +async function processWalletMessage(session: ActiveSession, message: any) { + // Allow requestCapabilities to pass through (it's the mechanism for getting approved). + // Block all other wallet methods until capabilities have been approved for this session. + if ( + message.type !== "requestCapabilities" && + !capabilitiesApprovedSessions.has(session.sessionId) + ) { + log.warn( + "[background] Rejecting wallet method before capabilities approved:", + message.type, + ); + await handler.sendResponse(session.sessionId, { + messageId: message.messageId, + error: "Capabilities not yet approved. Call requestCapabilities() first.", + walletId: WALLET_CONFIG.walletId, + }); + return; + } + + // docs:start:approval-check + // sendTx always requires approval — it's a state-changing operation. + // batch requires approval only if it contains a sendTx (e.g. BatchCall.send()). + // Read-only batches (simulateTx, executeUtility, etc.) auto-execute. + const needsApproval = + message.type === "sendTx" || + (message.type === "batch" && + Array.isArray(message.args?.[0]) && + message.args[0].some((m: any) => m.name === "sendTx")); + + if (needsApproval) { + // Extract `from` address from the method args: + // - sendTx args: [executionPayload, sendOptions] → from is in sendOptions + // - batch args: [methodsArray] → find the sendTx entry and get from from its opts + let from = ""; + if (message.type === "sendTx") { + from = message.args?.[1]?.from?.toString?.() || ""; + } else if (message.type === "batch") { + const sendTxMethod = message.args[0].find( + (m: any) => m.name === "sendTx", + ); + from = sendTxMethod?.args?.[1]?.from?.toString?.() || ""; + } + + const pending: PendingTransaction = { + sessionId: session.sessionId, + messageId: message.messageId, + method: message.type, + args: message.args, + from, + origin: session.origin, + timestamp: Date.now(), + }; + + pendingTransactions.push(pending); + updateBadge(); + log.debug("[background] Transaction pending approval:", pending.method); + + openPopupWithFallback(); + return; + } + // docs:end:approval-check + + // Capability requests require user approval — push to pending state. + if (message.type === "requestCapabilities") { + // Auto-approve for trusted origins if the requested capabilities match what was previously granted + const requestedManifest = message.args?.[0]; + const requestedCaps: any[] = requestedManifest?.capabilities || []; + if (await isTrustedOrigin(session.origin, session.appId)) { + const savedCaps = await getStoredCapabilities( + session.origin, + session.appId, + ); + if (savedCaps) { + // Verify the requested capability types match the previously approved set + const requestedTypes = requestedCaps.map((c: any) => c.type).sort(); + const savedTypes = savedCaps.map((c: any) => c.type).sort(); + const capsMatch = + requestedTypes.length === savedTypes.length && + requestedTypes.every((t: string, i: number) => t === savedTypes[i]); + + if (capsMatch) { + const grantedAccounts = await getGrantableAccounts(); + const granted = savedCaps.map((cap: any) => { + if (cap.type === "accounts") { + return { ...cap, accounts: grantedAccounts }; + } + return { ...cap }; + }); + + await handler.sendResponse(session.sessionId, { + messageId: message.messageId, + result: { + version: "1.0", + granted, + wallet: { + name: WALLET_CONFIG.walletName, + version: WALLET_CONFIG.walletVersion, + }, + }, + walletId: WALLET_CONFIG.walletId, + }); + capabilitiesApprovedSessions.add(session.sessionId); + log.debug( + "[background] Auto-approved capabilities for trusted origin:", + session.origin, + ); + return; + } + log.debug( + "[background] Requested capabilities differ from saved — requiring approval", + ); + } + } + + // Not trusted, no saved caps, or requested caps differ — require user approval + const manifest = requestedManifest; + const pending: PendingCapabilities = { + sessionId: session.sessionId, + messageId: message.messageId, + origin: session.origin, + appMetadata: manifest?.metadata || { + name: "Unknown App", + version: "0.0.0", + }, + capabilities: requestedCaps, + timestamp: Date.now(), + }; + pendingCapabilities.push(pending); + updateBadge(); + openPopupWithFallback(); + return; + } + + // All other wallet methods — execute silently (no task banner). + // These are read-only or background calls (executeUtility, simulateTx, batch + // without sendTx, getAccounts, etc.) that don't need user visibility. + (async () => { + let result = await sendToOffscreen({ + type: MessageTypes.WALLET_METHOD, + method: message.type, + args: message.args, + }); + + // MetaMask-like behavior: return only the active account for getAccounts + if ( + message.type === "getAccounts" || + message.type === "getRegisteredAccounts" + ) { + const activeData = await chrome.storage.local.get( + STORAGE_KEYS.ACTIVE_ACCOUNT, + ); + const activeAddress = activeData[STORAGE_KEYS.ACTIVE_ACCOUNT]; + if (activeAddress && Array.isArray(result)) { + const activeAccount = result.find( + (acc: any) => + acc.item === activeAddress || + acc.item?.toString() === activeAddress, + ); + result = activeAccount ? [activeAccount] : result; + } + } + + await handler.sendResponse(session.sessionId, { + messageId: message.messageId, + result, + walletId: WALLET_CONFIG.walletId, + }); + })().catch(async (error: any) => { + log.error( + "[background] Error handling wallet message:", + message.type, + error, + ); + await handler.sendResponse(session.sessionId, { + messageId: message.messageId, + error: error.message, + walletId: WALLET_CONFIG.walletId, + }); + }); +} + +// docs:start:callbacks +const callbacks: BackgroundConnectionCallbacks = { + onPendingDiscovery: async (discovery) => { + log.debug( + "[background] Pending discovery:", + discovery.requestId, + "from", + discovery.origin, + ); + + // Clean up stale sessions from this tab (e.g. page refresh creates a new + // discovery while the old session is still in activeSessions). + for (const session of handler.getActiveSessions()) { + if (session.tabId === discovery.tabId) { + log.debug( + "[background] Terminating stale session for tab:", + discovery.tabId, + session.sessionId, + ); + capabilitiesApprovedSessions.delete(session.sessionId); + queuedMessages.delete(session.sessionId); + handler.terminateSession(session.sessionId); + } + } + + // Deduplicate: reject any existing discovery from the same tab + const existing = handler + .getPendingDiscoveries() + .find( + (d) => + d.tabId === discovery.tabId && d.requestId !== discovery.requestId, + ); + if (existing) { + handler.rejectDiscovery(existing.requestId); + } + + // Auto-approve if origin is already trusted (reconnection after page refresh) + if (await isTrustedOrigin(discovery.origin, discovery.appId)) { + log.debug( + "[background] Auto-approving trusted origin:", + discovery.origin, + ); + handler.approveDiscovery(discovery.requestId); + return; + } + + updateBadge(); + + openPopupWithFallback(); + }, + + onSessionEstablished: async (session: ActiveSession) => { + log.debug("[background] Session established:", session.sessionId); + + // Auto-confirm if origin is already trusted (skip emoji verification) + if (await isTrustedOrigin(session.origin, session.appId)) { + log.debug( + "[background] Auto-confirming trusted session:", + session.sessionId, + ); + + // Pre-approve capabilities if previously granted (enables seamless reconnect) + const savedCaps = await getStoredCapabilities( + session.origin, + session.appId, + ); + if (savedCaps) { + capabilitiesApprovedSessions.add(session.sessionId); + } + + // Flush any queued messages immediately (same logic as CONFIRM_SESSION handler) + const queued = queuedMessages.get(session.sessionId) ?? []; + queuedMessages.delete(session.sessionId); + for (const { session: s, message: msg } of queued) { + processWalletMessage(s, msg); + } + pushStateToPopup(); + return; + } + + // New origin — require emoji verification + log.debug( + "[background] Awaiting emoji verification for:", + session.sessionId, + ); + // SDK automatically removes the discovery when key exchange completes. + // Show emojis in approvals so user can compare with the webapp + pendingSessionVerifications.push({ + sessionId: session.sessionId, + origin: session.origin, + appId: session.appId, + verificationHash: session.verificationHash, + timestamp: Date.now(), + }); + updateBadge(); + + // Only open popup if not already connected — calling openPopup() on an + // already-open popup rejects, and the fallback creates a second window + // that steals the popupPort from the original. + if (!popupPort) { + openPopupWithFallback(); + } + + pushStateToPopup(); + }, + + // docs:start:on-wallet-message + /** + * Handles wallet method calls from the ExtensionWallet proxy. + * Messages are queued while emoji verification is pending — the extension + * user must confirm before any dApp calls are processed. + */ + onWalletMessage: async (session: ActiveSession, message: any) => { + log.debug( + "[background] Wallet message:", + message.type, + "from session:", + session.sessionId, + ); + + // Block wallet messages until the user confirms emoji verification in the extension. + // The dApp's calls (e.g. getAccounts) will wait until the extension user approves. + const awaitingVerification = pendingSessionVerifications.some( + (v) => v.sessionId === session.sessionId, + ); + if (awaitingVerification) { + log.debug( + "[background] Session awaiting verification, queuing message:", + message.type, + ); + const queue = queuedMessages.get(session.sessionId) ?? []; + queue.push({ session, message }); + queuedMessages.set(session.sessionId, queue); + return; + } + + await processWalletMessage(session, message); + }, + // docs:end:on-wallet-message +}; +// docs:end:callbacks + +const handler = new BackgroundConnectionHandler( + { ...WALLET_CONFIG, logger: log }, + transport, + callbacks, +); +handler.initialize(); + +// Clean up sessions when a tab is closed +chrome.tabs.onRemoved.addListener((tabId) => { + const sessions = handler.getActiveSessions().filter((s) => s.tabId === tabId); + for (const session of sessions) { + capabilitiesApprovedSessions.delete(session.sessionId); + queuedMessages.delete(session.sessionId); + } + handler.terminateForTab(tabId); + pushStateToPopup(); +}); + +// docs:start:popup-messages +/** + * Handle messages from popup and offscreen for approvals and account management. + */ +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + /** + * Storage proxy for the offscreen document. (#7) + * Validates that the request comes from the extension itself (not content scripts or external). + */ + if (message.type === "storage-get" || message.type === "storage-set") { + // Security: only allow storage proxy from extension pages (offscreen, popup) (#7) + // Content scripts have sender.tab set; extension pages (offscreen, popup) do not. + if (sender.tab) { + log.warn( + "[background] Rejected storage proxy from content script, tab:", + sender.tab.id, + ); + sendResponse({ + success: false, + error: "Storage proxy not allowed from content scripts", + }); + return false; + } + + if (message.type === "storage-get") { + chrome.storage.local + .get(message.key) + .then((result) => { + sendResponse({ success: true, result: result[message.key] }); + }) + .catch((err) => { + sendResponse({ success: false, error: err.message }); + }); + } else { + chrome.storage.local + .set(message.data) + .then(() => { + sendResponse({ success: true }); + }) + .catch((err) => { + sendResponse({ success: false, error: err.message }); + }); + } + return true; // async response (#23) + } + + if (message.target !== MessageTarget.BACKGROUND) { + return false; + } + + log.debug("[background] Popup message:", message.type); + + // Reset auto-lock on any popup interaction (#28) + resetAutoLockTimer(); + + switch (message.type) { + case MessageTypes.APPROVE_CONNECTION: { + handler.approveDiscovery(message.requestId); + updateBadge(); + sendResponse({ success: true }); + return false; // sync response (#23) + } + + case MessageTypes.REJECT_CONNECTION: { + handler.rejectDiscovery(message.requestId); + updateBadge(); + sendResponse({ success: true }); + return false; + } + + case MessageTypes.APPROVE_TRANSACTION: { + const pending = pendingTransactions.find( + (t) => t.messageId === message.messageId, + ); + if (pending) { + pendingTransactions = pendingTransactions.filter( + (t) => t.messageId !== message.messageId, + ); + updateBadge(); + + const taskId = startBackgroundTask( + `tx:${pending.method}`, + handleTransactionApproval(pending), + ); + sendResponse({ success: true, result: { taskId } }); + } else { + sendResponse({ success: false, error: "Transaction not found" }); + } + return false; + } + + case MessageTypes.REJECT_TRANSACTION: { + const pending = pendingTransactions.find( + (t) => t.messageId === message.messageId, + ); + if (pending) { + handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + error: "Transaction rejected by user", + walletId: WALLET_CONFIG.walletId, + }); + + pendingTransactions = pendingTransactions.filter( + (t) => t.messageId !== message.messageId, + ); + updateBadge(); + } + sendResponse({ success: true }); + return false; + } + + case MessageTypes.APPROVE_CAPABILITIES: { + const pending = pendingCapabilities.find( + (c) => c.messageId === message.messageId, + ); + if (pending) { + pendingCapabilities = pendingCapabilities.filter( + (c) => c.messageId !== message.messageId, + ); + + // Build granted capabilities using the shared active-account helper + getGrantableAccounts() + .then((grantedAccounts) => { + const granted = pending.capabilities.map((cap: any) => { + if (cap.type === "accounts") { + return { ...cap, accounts: grantedAccounts }; + } + return { ...cap }; + }); + + return handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + result: { + version: "1.0", + granted, + wallet: { + name: WALLET_CONFIG.walletName, + version: WALLET_CONFIG.walletVersion, + }, + }, + walletId: WALLET_CONFIG.walletId, + }); + }) + .then(async () => { + capabilitiesApprovedSessions.add(pending.sessionId); + + // Persist granted capabilities for auto-reconnect + const approvedSession = handler.getSession(pending.sessionId); + if (approvedSession) { + const trusted = await getTrustedOrigins(); + const entry = trusted.find( + (t) => + t.origin === approvedSession.origin && + t.appId === approvedSession.appId, + ); + if (entry) { + entry.grantedCapabilities = pending.capabilities.map( + (cap: any) => ({ ...cap }), + ); + await chrome.storage.local.set({ + [TRUSTED_ORIGINS_KEY]: trusted, + }); + } + } + + updateBadge(); + sendResponse({ success: true }); + }) + .catch((err) => { + log.error("[background] Failed to approve capabilities:", err); + sendResponse({ success: false, error: getErrorMessage(err) }); + }); + } else { + sendResponse({ success: false, error: "Capability request not found" }); + return false; + } + return true; + } + + case MessageTypes.REJECT_CAPABILITIES: { + const pending = pendingCapabilities.find( + (c) => c.messageId === message.messageId, + ); + if (pending) { + pendingCapabilities = pendingCapabilities.filter( + (c) => c.messageId !== message.messageId, + ); + + handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + result: { + version: "1.0", + granted: [], + wallet: { + name: WALLET_CONFIG.walletName, + version: WALLET_CONFIG.walletVersion, + }, + }, + walletId: WALLET_CONFIG.walletId, + }); + + updateBadge(); + } + sendResponse({ success: true }); + return false; + } + + case MessageTypes.CONFIRM_SESSION: { + // User confirmed emojis match — session is now fully active. + // Flush any wallet messages that were queued while awaiting verification. + pendingSessionVerifications = pendingSessionVerifications.filter( + (v) => v.sessionId !== message.sessionId, + ); + const queued = queuedMessages.get(message.sessionId) ?? []; + queuedMessages.delete(message.sessionId); + for (const { session, message: msg } of queued) { + log.debug("[background] Flushing queued message:", msg.type); + processWalletMessage(session, msg); + } + + // Remember this origin as trusted for future reconnections (#30) + const confirmedSession = handler.getSession(message.sessionId); + if (confirmedSession) { + addTrustedOrigin(confirmedSession.origin, confirmedSession.appId); + } + + updateBadge(); + sendResponse({ success: true }); + return false; + } + + case MessageTypes.REJECT_SESSION: { + // User rejected emoji verification — reject queued messages and terminate the session. + pendingSessionVerifications = pendingSessionVerifications.filter( + (v) => v.sessionId !== message.sessionId, + ); + const rejected = queuedMessages.get(message.sessionId) ?? []; + queuedMessages.delete(message.sessionId); + for (const { session, message: msg } of rejected) { + handler.sendResponse(session.sessionId, { + messageId: msg.messageId, + error: "Session verification rejected by user", + walletId: WALLET_CONFIG.walletId, + }); + } + handler.terminateSession(message.sessionId); + updateBadge(); + sendResponse({ success: true }); + return false; + } + + case MessageTypes.DISCONNECT_SESSION: { + // (#29) Allow users to disconnect a specific dApp session + // Remove from trusted origins so next connection requires full approval (#30) + const disconnectedSession = handler.getSession(message.sessionId); + if (disconnectedSession) { + removeTrustedOrigin( + disconnectedSession.origin, + disconnectedSession.appId, + ); + } + capabilitiesApprovedSessions.delete(message.sessionId); + handler.terminateSession(message.sessionId); + pushStateToPopup(); + sendResponse({ success: true }); + return false; + } + + case "getPendingItems": { + sendResponse({ + success: true, + result: getFullState(), + }); + return false; + } + + case MessageTypes.GET_ACCOUNTS: { + chrome.storage.local + .get(STORAGE_KEYS.ACCOUNTS) + .then((data) => { + const accounts = (data[STORAGE_KEYS.ACCOUNTS] || []).map( + (acc: any) => ({ + address: acc.address, + alias: acc.alias, + isDeployed: acc.isDeployed, + }), + ); + sendResponse({ success: true, result: accounts }); + }) + .catch((error) => + sendResponse({ success: false, error: error.message }), + ); + return true; // async (#23) + } + + case MessageTypes.GET_ACTIVE_ACCOUNT: { + chrome.storage.local + .get(STORAGE_KEYS.ACTIVE_ACCOUNT) + .then((data) => { + sendResponse({ + success: true, + result: data[STORAGE_KEYS.ACTIVE_ACCOUNT] || null, + }); + }) + .catch((error) => + sendResponse({ success: false, error: error.message }), + ); + return true; + } + + case MessageTypes.SET_ACTIVE_ACCOUNT: { + chrome.storage.local + .set({ [STORAGE_KEYS.ACTIVE_ACCOUNT]: message.address }) + .then(() => sendResponse({ success: true })) + .catch((error) => + sendResponse({ success: false, error: error.message }), + ); + return true; + } + + case MessageTypes.UNLOCK_WALLET: { + const taskId = startBackgroundTask( + "unlock", + sendToOffscreen({ + type: MessageTypes.UNLOCK_WALLET, + password: message.password, + }).then((result) => { + walletUnlocked = true; + persistState(); + resetAutoLockTimer(); + return result; + }), + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + case MessageTypes.GET_WALLET_STATUS: { + chrome.storage.local + .get(STORAGE_KEYS.PASSWORD_DATA) + .then((data) => { + sendResponse({ + success: true, + result: { + unlocked: walletUnlocked, + hasPassword: !!data[STORAGE_KEYS.PASSWORD_DATA], + }, + }); + }) + .catch((error) => + sendResponse({ success: false, error: error.message }), + ); + return true; + } + + case MessageTypes.SETUP_PASSWORD: { + const taskId = startBackgroundTask( + "setup-password", + sendToOffscreen({ + type: MessageTypes.SETUP_PASSWORD, + password: message.password, + }).then((result) => { + walletUnlocked = true; + persistState(); + resetAutoLockTimer(); + return result; + }), + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + case MessageTypes.MARK_DEPLOYED: { + chrome.storage.local + .get(STORAGE_KEYS.ACCOUNTS) + .then((data) => { + const accounts = data[STORAGE_KEYS.ACCOUNTS] || []; + const account = accounts.find( + (a: any) => a.address === message.address, + ); + if (account) { + account.isDeployed = true; + return chrome.storage.local.set({ + [STORAGE_KEYS.ACCOUNTS]: accounts, + }); + } + }) + .then(() => sendResponse({ success: true, result: { success: true } })) + .catch((error) => + sendResponse({ success: false, error: error.message }), + ); + return true; + } + + case MessageTypes.CREATE_ACCOUNT: { + const taskId = startBackgroundTask( + "create-account", + sendToOffscreen({ + type: MessageTypes.CREATE_ACCOUNT, + alias: message.alias, + }), + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + case MessageTypes.DEPLOY_ACCOUNT: { + const taskId = startBackgroundTask( + "deploy-account", + sendToOffscreen({ + type: MessageTypes.DEPLOY_ACCOUNT, + address: message.address, + }), + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + case MessageTypes.EXPORT_WALLET: { + const taskId = startBackgroundTask( + "export-wallet", + sendToOffscreen({ type: MessageTypes.EXPORT_WALLET }), + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + case MessageTypes.IMPORT_WALLET: { + // Wipe wallet data from chrome.storage.local and lock the wallet + chrome.storage.local.remove([ + STORAGE_KEYS.ACCOUNTS, + STORAGE_KEYS.PASSWORD_DATA, + STORAGE_KEYS.ACTIVE_ACCOUNT, + ]); + walletUnlocked = false; + persistState(); + // Tell offscreen to clear cached key + sendToOffscreen({ type: MessageTypes.LOCK_WALLET }).catch(() => {}); + sendResponse({ success: true, result: { success: true } }); + return false; + } + + case MessageTypes.IMPORT_WALLET_ACCOUNTS: { + const taskId = startBackgroundTask( + "import-wallet-accounts", + sendToOffscreen({ + type: MessageTypes.IMPORT_WALLET_ACCOUNTS, + accounts: message.accounts, + activeAccount: message.activeAccount, + }).then((result) => { + walletUnlocked = true; + persistState(); + resetAutoLockTimer(); + return result; + }), + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + default: { + log.warn("[background] Unknown message type:", message.type); + return false; + } + } +}); + +async function handleTransactionApproval( + pending: PendingTransaction, +): Promise { + try { + const result = await sendToOffscreen({ + type: MessageTypes.WALLET_METHOD, + method: pending.method, + args: pending.args, + }); + + await handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + result, + walletId: WALLET_CONFIG.walletId, + }); + + return result; + } catch (error: any) { + log.error( + "[background] Transaction approval failed:", + pending.method, + error, + ); + await handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + error: error.message, + walletId: WALLET_CONFIG.walletId, + }); + throw error; + } +} +// docs:end:popup-messages + +/** + * Extension lifecycle handlers. (#17) + */ +chrome.runtime.onInstalled.addListener((details) => { + log.debug("[background] Extension installed/updated:", details.reason); + + // Clear stale pending state — sessions don't survive extension reload + pendingTransactions = []; + pendingSessionVerifications = []; + pendingCapabilities = []; + queuedMessages.clear(); + capabilitiesApprovedSessions.clear(); + persistState(); + + if (details.reason === "install") { + // First install — nothing to migrate + log.debug("[background] First install, no migration needed"); + } else if (details.reason === "update") { + // Version update — could add migration logic here + log.debug("[background] Updated from", details.previousVersion); + } +}); + +// Restore state on service worker startup (#8) +restoreState().then(() => { + log.debug("[background] Service worker initialized"); +}); + +// Eagerly preload the offscreen document so WASM and PXE deps are warm +ensureOffscreenDocument() + .then(() => { + log.debug("[background] Offscreen document preloaded"); + }) + .catch((err) => { + log.error( + "[background] Offscreen preload failed (will retry on demand):", + err, + ); + }); diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/config.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/config.ts new file mode 100644 index 00000000000..00c7e3e00ee --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/config.ts @@ -0,0 +1,96 @@ +// docs:start:wallet-config +/** + * Configuration for the Aztec Tutorial Wallet extension. + * Uses SponsoredFPC for fee payment. + */ + +/** Aztec node URL — defaults to a local sandbox. */ +export const NODE_URL = 'http://localhost:8080'; + +/** Current @aztec/* package version, injected at build time by Vite. */ +declare const __AZTEC_PACKAGES_VERSION__: string; +export const AZTEC_PACKAGES_VERSION: string = + typeof __AZTEC_PACKAGES_VERSION__ !== 'undefined' ? __AZTEC_PACKAGES_VERSION__ : 'unknown'; + +/** Wallet identification for the SDK protocol */ +export const WALLET_CONFIG = { + walletId: 'aztec-tutorial-wallet', + walletName: 'Aztec Tutorial Wallet', + walletVersion: '1.0.0', + walletIcon: 'data:image/svg+xml,🔮', +}; + +/** Auto-lock timeout in minutes. The wallet locks after this period of inactivity. (#28) */ +export const AUTO_LOCK_MINUTES = 15; + +/** Message types for internal extension communication */ +export const MessageTypes = { + // Account management + GET_ACCOUNTS: 'get-accounts', + MARK_DEPLOYED: 'mark-deployed', + + // Full account creation in extension (uses Barretenberg) + CREATE_ACCOUNT: 'create-account', + DEPLOY_ACCOUNT: 'deploy-account', + + // Master password + wallet unlock + SETUP_PASSWORD: 'setup-password', + UNLOCK_WALLET: 'unlock-wallet', + GET_WALLET_STATUS: 'get-wallet-status', + + // PXE operations + INIT_PXE: 'init-pxe', + REGISTER_ACCOUNT: 'register-account', + + // Active account management + GET_ACTIVE_ACCOUNT: 'get-active-account', + SET_ACTIVE_ACCOUNT: 'set-active-account', + + // Wallet export/import + EXPORT_WALLET: 'export-wallet', + IMPORT_WALLET: 'import-wallet', + IMPORT_WALLET_ACCOUNTS: 'import-wallet-accounts', + + // Wallet SDK protocol — dispatches to BaseWallet + WALLET_METHOD: 'wallet-method', + + // Auto-lock + LOCK_WALLET: 'lock-wallet', + + // Popup -> Background + APPROVE_CONNECTION: 'approve-connection', + REJECT_CONNECTION: 'reject-connection', + APPROVE_TRANSACTION: 'approve-transaction', + REJECT_TRANSACTION: 'reject-transaction', + CONFIRM_SESSION: 'confirm-session', + REJECT_SESSION: 'reject-session', + DISCONNECT_SESSION: 'disconnect-session', + APPROVE_CAPABILITIES: 'approve-capabilities', + REJECT_CAPABILITIES: 'reject-capabilities', +} as const; + +/** Union type of all message type values — use for exhaustive checking. */ +export type MessageType = (typeof MessageTypes)[keyof typeof MessageTypes]; + +/** Targets for chrome.runtime messages */ +export const MessageTarget = { + OFFSCREEN: 'offscreen', + POPUP: 'popup', + BACKGROUND: 'background', +} as const; + + +/** + * Conditional logging. (#26) + * Strips verbose logs in production while keeping errors visible. + * Set DEBUG=true in the build to enable verbose logging. + */ +const DEBUG = process.env.NODE_ENV !== 'production'; // Toggle via build environment + +export const log = { + debug: (...args: unknown[]) => { if (DEBUG) console.log(...args); }, + info: (...args: unknown[]) => { if (DEBUG) console.info(...args); }, + warn: (...args: unknown[]) => console.warn(...args), + error: (...args: unknown[]) => console.error(...args), +}; +// docs:end:wallet-config diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/content-script.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/content-script.ts new file mode 100644 index 00000000000..55efa040639 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/content-script.ts @@ -0,0 +1,21 @@ +// docs:start:content-script +import { + ContentScriptConnectionHandler, + type ContentScriptTransport, +} from '@aztec-labs/wallet-sdk/extension/handlers'; + +const transport: ContentScriptTransport = { + sendToBackground: (message) => { + chrome.runtime.sendMessage(message); + }, + addBackgroundListener: (handler) => { + chrome.runtime.onMessage.addListener((message) => { + handler(message); + }); + }, +}; + +const handler = new ContentScriptConnectionHandler(transport); +handler.start(); +console.log('[content-script] Wallet SDK handler started'); +// docs:end:content-script diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/console-intercept.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/console-intercept.ts new file mode 100644 index 00000000000..2a0a08ad3be --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/console-intercept.ts @@ -0,0 +1,24 @@ +/** + * Console.info interception — MUST be imported before any module that creates + * pino loggers (e.g. @aztec-labs/pxe). + * + * Pino's browser transport captures `console.info` at logger-creation time. + * If our override runs after pino grabs the reference, pino bypasses it. + * By isolating the override in its own module and importing it first, + * ES module execution order guarantees it runs before any other dependency. + */ + +const callbacks: Array<(args: any[]) => void> = []; + +const originalInfo = console.info.bind(console); +console.info = (...args: any[]) => { + originalInfo(...args); + for (const cb of callbacks) { + cb(args); + } +}; + +/** Register a callback invoked on every console.info call. */ +export function onConsoleInfo(cb: (args: any[]) => void): void { + callbacks.push(cb); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen-dist.html b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen-dist.html new file mode 100644 index 00000000000..e93fda5db81 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen-dist.html @@ -0,0 +1,15 @@ + + + + + Aztec Wallet Offscreen + + + + + + diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.html b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.html new file mode 100644 index 00000000000..be9c9f75682 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.html @@ -0,0 +1,15 @@ + + + + + Aztec Wallet Offscreen + + + + + + diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts new file mode 100644 index 00000000000..2e384b4c185 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts @@ -0,0 +1,777 @@ +/** Offscreen document for the Aztec Tutorial Wallet. */ + +// CRITICAL: console-intercept MUST be the very first import. +// Pino (used by PXE) captures `console.info` at logger-creation time. +// By overriding it in a separate module imported first, ES module execution +// order guarantees the override is in place before any pino logger is created. +import { onConsoleInfo } from './console-intercept'; + +import { NODE_URL, MessageTypes, AZTEC_PACKAGES_VERSION, log } from '../config'; +import type { WalletExportData } from '../shared-types'; +import { + createAccount, + getAccounts, + getAccountSecret, + markDeployed, + storeAccount, + getActiveAccount, + setActiveAccount, +} from '../wallet/wallet-impl'; +import type { PXE } from '@aztec-labs/pxe/client/lazy'; +import type { Account } from '@aztec-labs/aztec.js/account'; +import type { AztecNode } from '@aztec-labs/aztec.js/node'; +import { createAztecNodeClient } from '@aztec-labs/aztec.js/node'; +import { getPXEConfig } from '@aztec-labs/pxe/config'; +import { createPXE } from '@aztec-labs/pxe/client/lazy'; + +// ============================================================================ +// CRITICAL: Enable SharedArrayBuffer support for Barretenberg WASM +// ============================================================================ +// +// Chrome extensions have SharedArrayBuffer available but crossOriginIsolated=false. +// bb.js checks crossOriginIsolated to decide which WASM binary to load and +// whether to create shared WebAssembly.Memory. +// +// This patches the main thread. Worker files are patched at build time by the +// patchWorkersCrossOriginIsolated Vite plugin (see vite.extension.config.ts). +// ============================================================================ +if (typeof SharedArrayBuffer !== 'undefined' && !(globalThis as any).crossOriginIsolated) { + Object.defineProperty(globalThis, 'crossOriginIsolated', { + value: true, + writable: false, + configurable: true, + }); +} + +import { getChromeRuntime, getErrorMessage } from '../utils'; +import { getAztecCore, getAztecWallet, getAztecDeploy } from '../aztec-imports'; +import { instantiateAccount } from '../account-utils'; + +const chromeRuntime = getChromeRuntime(); +log.debug('[offscreen] Offscreen document loaded. Storage proxied through background. Barretenberg enabled via crossOriginIsolated patch.'); + +/** + * Port connection from the background script. + * Set when the background connects via chrome.runtime.connect({ name: 'offscreen' }). + */ +let backgroundPort: chrome.runtime.Port | null = null; + +/** + * Sends a progress update to the background script for display in the popup. + * Fire-and-forget — uses the persistent port instead of broadcast sendMessage. + */ +function reportProgress(stage: string) { + log.debug('[offscreen] Progress:', stage); + backgroundPort?.postMessage({ type: 'task-progress', stage }); +} + +/** + * PXE log matchers — match pino browser log messages and relay as progress updates. + * + * Pino browser mode with `asObject: false` calls: + * console.info(bindingsObj, dataObj, messageString) + * where bindingsObj contains `{ module: 'pxe:service' }` etc. + * + * The interception is set up in console-intercept.ts (imported first) so that + * pino captures our wrapped console.info, not the original. + */ +const PXE_STAGE_MATCHERS: Array<{ module: string; pattern: RegExp; stage: string }> = [ + { module: 'pxe:service', pattern: /^Simulating transaction/, stage: 'Simulating transaction...' }, + { module: 'pxe:service', pattern: /^Simulation completed/, stage: 'Simulation complete, proving...' }, + { module: 'pxe:private-kernel-execution-prover', pattern: /^Private kernel witness generation/, stage: 'Kernel witness generated, creating proof...' }, + { module: 'pxe:bb:wasm:bundle', pattern: /^Generating ClientIVC proof/, stage: 'Generating ZK proof (this takes a while)...' }, + { module: 'pxe:bb:wasm:bundle', pattern: /^Generated ClientIVC proof/, stage: 'Proof generated, sending...' }, + { module: 'wallet-sdk:base_wallet', pattern: /^Sent transaction/, stage: 'Transaction sent, awaiting confirmation...' }, +]; + +onConsoleInfo((args) => { + const bindings = args.find((a) => typeof a === 'object' && a !== null && typeof a.module === 'string'); + const message = [...args].reverse().find((a) => typeof a === 'string'); + + if (bindings && message) { + for (const matcher of PXE_STAGE_MATCHERS) { + if (bindings.module === matcher.module && matcher.pattern.test(message)) { + reportProgress(matcher.stage); + break; + } + } + } +}); + +/** + * Master CryptoKey — cached in memory after unlock. (#2) + * + * This is a non-extractable AES-GCM CryptoKey derived from the user's password + * via PBKDF2. The raw password string is NEVER stored; only this opaque key + * object is kept in memory. Even if an attacker gets a reference to this object, + * they cannot extract the underlying key material (WebCrypto enforces this). + */ +let cachedMasterKey: CryptoKey | null = null; + +function getCachedMasterKey(): CryptoKey { + if (!cachedMasterKey) { + throw new Error('Wallet is locked. Please unlock first.'); + } + return cachedMasterKey; +} + +/** Clear the cached key (used for auto-lock). */ +export function clearCachedKey(): void { + cachedMasterKey = null; +} + +// docs:start:pxe-instance +/** + * PXE + node — lazily initialized as a pair, with dedup on the inflight promise. + */ +let pxeState: { pxe: PXE; node: AztecNode } | null = null; +let pxeInitializing: Promise<{ pxe: PXE; node: AztecNode }> | null = null; + +async function ensurePXE(nodeUrl: string = NODE_URL): Promise<{ pxe: PXE; node: AztecNode }> { + if (pxeState) return pxeState; + if (pxeInitializing) return pxeInitializing; + + log.debug('[offscreen] Initializing PXE with node:', nodeUrl); + pxeInitializing = (async () => { + try { + const node = createAztecNodeClient(nodeUrl); + const config = getPXEConfig(); + config.rollupAddress = (await node.getL1ContractAddresses()).rollupAddress; + const isLocal = nodeUrl.includes('localhost') || nodeUrl.includes('127.0.0.1'); + config.proverEnabled = !isLocal; + + const pxe = await createPXE(node, config, {}); + log.debug('[offscreen] PXE initialized, connected to node at:', nodeUrl); + + pxeState = { pxe, node }; + return pxeState; + } finally { + pxeInitializing = null; // Always clear so a retry can re-attempt + } + })(); + + return pxeInitializing; +} +// docs:end:pxe-instance + +// docs:start:wallet-instance +/** Single wallet class used for all operations. (#18, #20) */ + +import type { BaseWallet } from '@aztec-labs/wallet-sdk/base-wallet'; + +/** + * The wallet instance holds a BaseWallet subclass with an additional + * registerAccount method for tracking which accounts we can sign for. + * BaseWallet is dynamically imported at runtime; using `import type` gives + * us the type without a runtime dependency. (#20) + */ +type OffscreenWalletType = BaseWallet & { registerAccount(address: string, account: Account): void }; + +let walletInstance: OffscreenWalletType | null = null; + +/** + * Creates a SponsoredFPC contract instance from its artifact and well-known salt. + * Shared between OffscreenWallet.ensureSponsoredFPC() and handleDeployAccount(). + */ +async function getSponsoredFPCInstance() { + const { Fr, SponsoredFPCContract, SPONSORED_FPC_SALT, getContractInstanceFromInstantiationParams } = await getAztecDeploy(); + return getContractInstanceFromInstantiationParams( + SponsoredFPCContract.artifact, + { salt: new Fr(SPONSORED_FPC_SALT) }, + ); +} + +async function getWallet() { + if (walletInstance) return walletInstance; + + const { BaseWallet, NO_FROM } = await getAztecWallet(); + const { pxe, node } = await ensurePXE(); + + // AccountFeePaymentMethodOptions.EXTERNAL = 0 — fee is paid by an external FPC + const EXTERNAL_FEE_PAYMENT = 0; + + class OffscreenWallet extends BaseWallet { + protected minFeePadding = 1.0; // 100% padding for fee estimation variance + private accounts: Map = new Map(); + private sponsoredFPCAddress: any | null = null; + + constructor(pxeInstance: PXE, aztecNode: AztecNode) { + super(pxeInstance, aztecNode); + } + + registerAccount(address: string, account: Account) { + this.accounts.set(address, account); + } + + protected async getAccountFromAddress(address: any): Promise { + const key = address.toString(); + const account = this.accounts.get(key); + if (!account) { + throw new Error(`Account not found for address: ${key}`); + } + return account; + } + + async getAccounts() { + return Array.from(this.accounts.entries()).map(([, acc]) => ({ + alias: '', + item: acc.getAddress(), + })); + } + + /** Lazily registers the SponsoredFPC contract and caches its address. */ + private async ensureSponsoredFPC() { + if (this.sponsoredFPCAddress) return this.sponsoredFPCAddress; + const { SponsoredFPCContract } = await getAztecDeploy(); + const sponsoredFPCInstance = await getSponsoredFPCInstance(); + await this.registerContract(sponsoredFPCInstance, SponsoredFPCContract.artifact); + this.sponsoredFPCAddress = sponsoredFPCInstance.address; + return this.sponsoredFPCAddress; + } + + // docs:start:complete-fee-options + /** + * Always uses SponsoredFPC for fee payment, mirroring the deployment flow. + * The tutorial wallet doesn't hold fee juice, so every tx is sponsor-paid. + * + * If the execution payload already has a feePayer (e.g. DeployAccountMethod + * embeds SponsoredFPC in its own payload), we skip injecting a wallet-level + * payment method to avoid calling sponsor_unconditionally() twice, which + * would trigger "Cannot enter the revertible phase twice". + */ + protected async completeFeeOptions(config: any) { + const base = await super.completeFeeOptions(config); + // If the payload already includes a fee payer, don't inject another one + if (config.feePayer) { + return { + ...base, + accountFeePaymentMethodOptions: EXTERNAL_FEE_PAYMENT, + }; + } + const address = await this.ensureSponsoredFPC(); + const { SponsoredFeePaymentMethod } = await getAztecDeploy(); + return { + ...base, + walletFeePaymentMethod: new SponsoredFeePaymentMethod(address), + accountFeePaymentMethodOptions: config.from ? EXTERNAL_FEE_PAYMENT : base.accountFeePaymentMethodOptions, + }; + } + // docs:end:complete-fee-options + + /** + * Overrides sendTx to auto-extract auth witnesses from offchain effects. + * + * dApps like gregoswap don't explicitly create auth witnesses. Instead, they + * expect the wallet to handle it: simulate with a stub account (which passes + * all auth checks), extract the authorization requests emitted by + * `#[authorize_once]` in Noir contracts, sign them, and include them in the + * real transaction. + */ + async sendTx(executionPayload: any, opts: any): Promise { + if (executionPayload.authWitnesses.length === 0 && opts.from && opts.from !== NO_FROM) { + try { + await this.extractAndInjectAuthWitnesses(executionPayload, opts.from, opts.fee?.gasSettings); + } catch (err: any) { + log.error('[offscreen] Auth witness extraction failed, proceeding without:', err.message, err.stack); + } + } + return super.sendTx(executionPayload, opts); + } + + /** + * Simulates the tx with a stub account to collect offchain effects, + * parses CallAuthorizationRequest objects, and creates real auth witnesses. + */ + private async extractAndInjectAuthWitnesses(executionPayload: any, from: any, feeGasSettings?: any) { + const { Fr } = await getAztecCore(); + + // Step 1: Create a stub account that passes all auth checks unconditionally + log.info('[offscreen] Step 1: Loading stub account module...'); + const realAccount = await this.getAccountFromAddress(from); + const originalAddress = realAccount.getCompleteAddress(); + log.info('[offscreen] Got complete address:', originalAddress.address.toString()); + + // The stub has to match the type of the account it stands in for. All accounts in this wallet are Schnorr. + const { createStubSchnorrAccount, getStubSchnorrAccountContractArtifact } = await import('@aztec-labs/accounts/schnorr/stub/lazy'); + log.info('[offscreen] Loaded @aztec-labs/accounts/schnorr/stub/lazy'); + + const stubArtifact = await getStubSchnorrAccountContractArtifact(); + log.info('[offscreen] Loaded stub artifact:', stubArtifact.name); + + const stubAccount = createStubSchnorrAccount(originalAddress); + + // The override below swaps the account's class for the stub's, which the PXE looks up among registered classes. + await this.pxe.registerContractClass(stubArtifact); + const { getContractClassFromArtifact } = await import('@aztec-labs/stdlib/contract'); + const { id: stubClassId } = await getContractClassFromArtifact(stubArtifact); + const accountInstance = await this.pxe.getContractInstance(from); + if (!accountInstance) { + throw new Error(`No contract instance registered in the PXE for account ${from.toString()}`); + } + log.info('[offscreen] Created stub account and registered stub class'); + + // Step 2: Simulate with the stub account swapped in via PXE overrides + log.info('[offscreen] Step 2: Simulating tx with stub account...'); + const feeOptions = await this.completeFeeOptions({ + from, + feePayer: executionPayload.feePayer, + gasSettings: feeGasSettings, + }); + const chainInfo = await this.getChainInfo(); + const txRequest = await stubAccount.createTxExecutionRequest( + executionPayload, + feeOptions.gasSettings, + chainInfo, + { txNonce: Fr.random(), cancellable: false, feePaymentMethodOptions: feeOptions.accountFeePaymentMethodOptions }, + ); + log.info('[offscreen] Created tx execution request, simulating...'); + + const simResult = await this.pxe.simulateTx(txRequest, { + simulatePublic: true, + skipTxValidation: true, + skipFeeEnforcement: true, + overrides: { + contracts: { [from.toString()]: { instance: { ...accountInstance, currentContractClassId: stubClassId } } }, + }, + scopes: [from], + senderForTags: from, + }); + log.info('[offscreen] Simulation succeeded'); + + // Step 3: Extract auth witness requests from offchain effects + log.info('[offscreen] Step 3: Extracting offchain effects...'); + const { collectOffchainEffects } = await import('@aztec-labs/stdlib/tx'); + const { CallAuthorizationRequest } = await import('@aztec-labs/aztec.js/authorization'); + + if (!simResult.privateExecutionResult) { + log.warn('[offscreen] No privateExecutionResult in simulation result'); + return; + } + + const effects = collectOffchainEffects(simResult.privateExecutionResult); + log.info(`[offscreen] Found ${effects.length} offchain effect(s)`); + + // Pre-filter by CallAuthorizationRequest selector (matching e2e test pattern) + const callAuthSelector = await CallAuthorizationRequest.getSelector(); + const authEffects = effects.filter((e: any) => + e.data.length > 0 && e.data[0].equals(callAuthSelector.toField()), + ); + log.info(`[offscreen] ${authEffects.length} are CallAuthorizationRequest(s)`); + + // Step 4: Create auth witnesses from parsed authorization requests + let count = 0; + for (const effect of authEffects) { + const authRequest = await CallAuthorizationRequest.fromFields(effect.data); + log.info(`[offscreen] Auth request: consumer=${effect.contractAddress.toString()}, innerHash=${authRequest.innerHash.toString()}`); + const wit = await this.createAuthWit(from, { + consumer: effect.contractAddress, + innerHash: authRequest.innerHash, + }); + executionPayload.authWitnesses.push(wit); + count++; + log.info(`[offscreen] Created auth witness #${count}: messageHash=${wit.requestHash.toString()}`); + } + + log.info(`[offscreen] Auth witness extraction complete: ${count} witness(es) from ${effects.length} effect(s)`); + } + } + + walletInstance = new OffscreenWallet(pxe, node); + return walletInstance; +} +// docs:end:wallet-instance + +// docs:start:message-handler +/** + * Handles messages from the background script via a persistent port. + * The background connects with chrome.runtime.connect({ name: 'offscreen' }). + * Each message includes a messageId for request/response correlation. + */ +chromeRuntime.runtime.onConnect.addListener((port: chrome.runtime.Port) => { + if (port.name !== 'offscreen') return; + + log.debug('[offscreen] Background port connected'); + backgroundPort = port; + + port.onMessage.addListener((message: any) => { + log.debug('[offscreen] Received message:', message.type); + + handleMessage(message) + .then((result) => { + log.debug('[offscreen] Sending response for:', message.type); + port.postMessage({ messageId: message.messageId, success: true, result }); + }) + .catch((error: unknown) => { + const msg = getErrorMessage(error); + log.error('[offscreen] Error:', msg, error); + port.postMessage({ messageId: message.messageId, success: false, error: msg }); + }); + }); + + port.onDisconnect.addListener(() => { + log.debug('[offscreen] Background port disconnected'); + backgroundPort = null; + }); +}); + +async function handleMessage(message: any): Promise { + switch (message.type) { + case MessageTypes.GET_ACCOUNTS: + return handleGetAccounts(); + + case MessageTypes.MARK_DEPLOYED: + return handleMarkDeployed(message.address); + + case MessageTypes.WALLET_METHOD: + return handleWalletMethod(message.method, message.args); + + case MessageTypes.SETUP_PASSWORD: + return handleSetupPassword(message.password); + + case MessageTypes.CREATE_ACCOUNT: + return handleCreateAccount(message.alias); + + case MessageTypes.DEPLOY_ACCOUNT: + return handleDeployAccount(message.address); + + case MessageTypes.UNLOCK_WALLET: + return handleUnlockWallet(message.password); + + case MessageTypes.INIT_PXE: + return handleInitPXE(message.nodeUrl); + + case MessageTypes.REGISTER_ACCOUNT: + return handleRegisterAccount(message.address, message.secret, message.salt); + + case MessageTypes.EXPORT_WALLET: + return handleExportWallet(); + + case MessageTypes.IMPORT_WALLET_ACCOUNTS: + return handleImportWalletAccounts(message.accounts, message.activeAccount); + + // Lock the wallet (clear cached key) — used by auto-lock (#28) + case MessageTypes.LOCK_WALLET: + cachedMasterKey = null; + walletInstance = null; + return { success: true }; + + default: + throw new Error(`Unknown message type: ${message.type}`); + } +} +// docs:end:message-handler + +async function handleGetAccounts() { + return getAccounts(); +} + +async function handleMarkDeployed(address: string) { + await markDeployed(address); + return { success: true }; +} + +// docs:start:wallet-method-handler +/** + * Handles wallet method calls from the ExtensionWallet proxy via the SDK protocol. + * + * Serialization notes: + * 1. ARGS: Arrive as plain JSON. We use WalletSchema to parse them back into + * proper Aztec types (AztecAddress, Fr, ExecutionPayload, etc.). + * 2. RESULT: Contains class instances that lose prototypes through Chrome messaging. + * We serialize with jsonStringify before returning. + */ +async function handleWalletMethod(method: string, args: any[]): Promise { + log.debug('[offscreen] Handling wallet method:', method); + + const wallet = await getWallet(); + + // Dynamic dispatch: the wallet protocol sends method names as strings. + // Cast to Record for dynamic access since TypeScript can't know the method at compile time. + const walletObj = wallet as unknown as Record any>; + if (typeof walletObj[method] !== 'function') { + throw new Error(`Unknown wallet method: ${method}`); + } + + const { WalletSchema, jsonStringify, schemaHasMethod, getSchemaParameters, parseWithOptionals } = await getAztecWallet(); + + // Parse args through WalletSchema to reconstruct proper Aztec types (Buffer, Fr, etc.) + // from their JSON representations. A zod tuple requires all positional elements even if + // some are optional, so a plain parse would reject calls that omit trailing optional args. + let parsedArgs: any[] = args || []; + if (schemaHasMethod(WalletSchema, method)) { + try { + parsedArgs = await parseWithOptionals(args || [], getSchemaParameters(WalletSchema[method])); + } catch (parseErr: any) { + log.warn('[offscreen] Args parse warning for', method, ':', parseErr.message); + parsedArgs = args || []; + } + } + + // Report initial progress for long-running methods so the popup shows something + // before the PXE log matchers kick in + const longRunningMethods = ['sendTx', 'simulateTx', 'profileTx']; + if (longRunningMethods.includes(method)) { + reportProgress(`Starting ${method}...`); + } + + const result = await walletObj[method](...parsedArgs); + + // Serialize to JSON-safe format before returning through Chrome messaging + // Methods like registerContract return nothing, and JSON.parse rejects what jsonStringify returns for undefined. + const jsonSafe = result === undefined ? undefined : JSON.parse(jsonStringify(result)); + log.debug('[offscreen] Wallet method completed:', method); + return jsonSafe; +} +// docs:end:wallet-method-handler + +/** + * Sets the master password for the first time. (#1, #2) + * Returns the CryptoKey (which we cache), never stores the password. + */ +async function handleSetupPassword(password: string) { + log.debug('[offscreen] Setting up master password'); + const { setupPassword, hasPassword: checkHasPassword } = await import('../wallet/storage'); + const exists = await checkHasPassword(); + if (exists) { + throw new Error('Master password already set'); + } + cachedMasterKey = await setupPassword(password); + // password string goes out of scope here — only the CryptoKey survives + return { success: true }; +} + +/** + * Creates an account using the cached master CryptoKey. (#2, #3) + */ +async function handleCreateAccount(alias: string) { + const masterKey = getCachedMasterKey(); + log.debug('[offscreen] Creating account with alias:', alias); + const result = await createAccount(masterKey, alias); + log.debug('[offscreen] Account created:', result.address); + return { address: result.address }; +} + +// docs:start:deploy-account +/** + * Deploys an account contract onchain using SponsoredFPC for fee payment. + * Uses the cached master CryptoKey to decrypt the account secret. (#2, #4) + */ +async function handleDeployAccount(address: string) { + const masterKey = getCachedMasterKey(); + log.debug('[offscreen] Deploying account:', address); + + // 1. Decrypt the account secret + reportProgress('Decrypting account secret...'); + const secretData = await getAccountSecret(address, masterKey); + if (!secretData) { + throw new Error(`Account not found: ${address}`); + } + + // 2. Ensure PXE is initialized (needed by the wallet) + reportProgress('Connecting to PXE...'); + await ensurePXE(); + + // 3. Register account with PXE and wallet (shared with unlock flow) + reportProgress('Registering account contract...'); + const { accountManager } = await registerAccountInWallet(address, secretData.secret, secretData.salt); + + // 4. Register SponsoredFPC contract with PXE (shared helper) + reportProgress('Registering fee payment contract...'); + const { NO_FROM, SponsoredFeePaymentMethod, SponsoredFPCContract } = await getAztecDeploy(); + const sponsoredFPCInstance = await getSponsoredFPCInstance(); + const wallet = await getWallet(); + await wallet.registerContract(sponsoredFPCInstance, SponsoredFPCContract.artifact); + + // 5. Deploy with SponsoredFPC fee payment. + // PXE log matchers (PXE_STAGE_MATCHERS) provide granular progress updates + // (simulating → proving → proof generated → sending → awaiting confirmation). + reportProgress('Starting deploy tx...'); + + const paymentMethod = new SponsoredFeePaymentMethod(sponsoredFPCInstance.address); + const deployMethod = await accountManager.getDeployMethod(); + const { receipt } = await deployMethod.send({ + from: NO_FROM, + fee: { paymentMethod }, + wait: { timeout: 2400 }, + }); + + // 6. Mark deployed in storage + await markDeployed(address); + reportProgress('Deploy complete!'); + + log.debug('[offscreen] Account deployed:', address, 'txHash:', receipt.txHash?.toString()); + return { success: true, txHash: receipt.txHash?.toString() }; +} +// docs:end:deploy-account + +// docs:start:load-accounts +/** + * Unlocks the wallet: verifies password, caches CryptoKey, initializes PXE, + * registers all stored accounts. (#1, #2) + * + * After unlock: + * - cachedMasterKey holds the non-extractable CryptoKey + * - The password string is discarded (goes out of scope) + * - All accounts are registered with PXE and the BaseWallet + */ +async function handleUnlockWallet(password: string) { + log.debug('[offscreen] Unlocking wallet...'); + + reportProgress('Verifying password...'); + const { verifyAndDeriveMasterKey, hasPassword: checkHasPassword } = await import('../wallet/storage'); + + if (await checkHasPassword()) { + const masterKey = await verifyAndDeriveMasterKey(password); + if (!masterKey) { + throw new Error('Incorrect password'); + } + cachedMasterKey = masterKey; + // password string goes out of scope — only the CryptoKey survives + } else { + throw new Error('No password set. Please set up your wallet first.'); + } + + // Initialize PXE + reportProgress('Initializing PXE (loading WASM)...'); + await ensurePXE(); + log.debug('[offscreen] PXE initialized for unlock'); + + // Register all stored accounts + const storedAccounts = await getAccounts(); + reportProgress(`Registering ${storedAccounts.length} account(s)...`); + log.debug('[offscreen] Registering', storedAccounts.length, 'accounts with PXE'); + + const failedAccounts: string[] = []; + for (const account of storedAccounts) { + try { + const secretData = await getAccountSecret(account.address, cachedMasterKey); + if (secretData) { + await registerAccountInWallet(account.address, secretData.secret, secretData.salt); + log.debug('[offscreen] Registered account:', account.address); + } + } catch (err: any) { + log.error('[offscreen] Failed to register account:', account.address, err.message); + failedAccounts.push(account.address); + // Continue with remaining accounts — partial unlock is better than full lockout + } + } + + if (failedAccounts.length === storedAccounts.length && storedAccounts.length > 0) { + // ALL accounts failed — password is likely wrong + cachedMasterKey = null; + throw new Error('Failed to unlock: wrong password or corrupted data'); + } + if (failedAccounts.length > 0) { + log.warn('[offscreen] Partial unlock:', failedAccounts.length, 'account(s) failed to register'); + } + + log.debug('[offscreen] Wallet unlocked,', storedAccounts.length - failedAccounts.length, 'of', storedAccounts.length, 'accounts registered'); + return { success: true }; +} +// docs:end:load-accounts + +/** + * Registers an account with both PXE and the BaseWallet. + * Returns the AccountManager so callers (e.g. deploy) can use it for further operations. + */ +async function registerAccountInWallet(address: string, secret: string, salt: string) { + const { secretFr, saltFr, accountContract, artifact, instance } = + await instantiateAccount(secret, salt); + + const { AccountManager } = await getAztecCore(); + + const wallet = await getWallet(); + await wallet.registerContract(instance, artifact, secretFr); + + const accountManager = await AccountManager.create(wallet, secretFr, accountContract, { salt: saltFr }); + const account = await accountManager.getAccount(); + wallet.registerAccount(address, account); + + log.debug('[offscreen] Account registered in PXE and wallet:', address); + return { success: true, address, accountManager }; +} + +async function handleRegisterAccount(address: string, secret: string, salt: string) { + return registerAccountInWallet(address, secret, salt); +} + +/** + * Exports the entire wallet: decrypts all account secrets and builds a WalletExportData object. + */ +async function handleExportWallet(): Promise { + const masterKey = getCachedMasterKey(); + const allAccounts = await getAccounts(); + const activeAddr = await getActiveAccount(); + + reportProgress(`Decrypting ${allAccounts.length} account(s)...`); + + const exportedAccounts: WalletExportData['accounts'] = []; + for (const account of allAccounts) { + const secretData = await getAccountSecret(account.address, masterKey); + if (!secretData) { + throw new Error(`Failed to decrypt account: ${account.address}`); + } + exportedAccounts.push({ + address: account.address, + secret: secretData.secret, + salt: secretData.salt, + alias: account.alias, + isDeployed: account.isDeployed, + }); + } + + return { + version: 1, + aztecPackagesVersion: AZTEC_PACKAGES_VERSION, + exportedAt: new Date().toISOString(), + accounts: exportedAccounts, + activeAccount: activeAddr, + }; +} + +/** + * Imports accounts into the wallet: re-encrypts each account with the new master key, + * stores them, marks deployed ones, sets active account, and registers all with PXE. + */ +async function handleImportWalletAccounts( + accounts: WalletExportData['accounts'], + activeAccount: string | null, +): Promise<{ success: true }> { + const masterKey = getCachedMasterKey(); + + reportProgress(`Importing ${accounts.length} account(s)...`); + + for (const account of accounts) { + await storeAccount( + account.address, + account.secret, + account.salt, + masterKey, + account.alias, + ); + if (account.isDeployed) { + await markDeployed(account.address); + } + } + + if (activeAccount) { + await setActiveAccount(activeAccount); + } + + // Initialize PXE and register all accounts + reportProgress('Initializing PXE...'); + await ensurePXE(); + + for (const account of accounts) { + reportProgress(`Registering ${account.alias || account.address.slice(0, 10)}...`); + await registerAccountInWallet(account.address, account.secret, account.salt); + } + + reportProgress('Import complete!'); + return { success: true }; +} + +async function handleInitPXE(nodeUrl?: string) { + await ensurePXE(nodeUrl); + return { success: true }; +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx new file mode 100644 index 00000000000..ff08b667435 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import type { StoredAccount } from './types'; +import { truncateAddress } from './helpers'; + +interface AccountSwitcherProps { + accounts: StoredAccount[]; + activeAccount: string | null; + onSelect: (address: string) => void; + onCreateNew: () => void; +} + +export function AccountSwitcher({ accounts, activeAccount, onSelect, onCreateNew }: AccountSwitcherProps) { + return ( +
+ {accounts.map((account) => { + const isActive = activeAccount === account.address; + return ( +
onSelect(account.address)} + > +
+
+ {account.alias || 'Unnamed Account'} +
+
+ {truncateAddress(account.address)} +
+
+
+ + {account.isDeployed ? 'Deployed' : 'Not Deployed'} + + {isActive && ✓} +
+
+ ); + })} + + +
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx new file mode 100644 index 00000000000..92aefb1f864 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx @@ -0,0 +1,509 @@ +import React, { useState } from 'react'; + +import { MessageTypes } from '../config'; +import { getOriginHost } from '../utils'; +import { hashToEmoji } from '@aztec-labs/wallet-sdk/crypto'; +import type { PendingDiscovery } from '@aztec-labs/wallet-sdk/extension/handlers'; +import type { PendingTransaction, PendingCapabilities, PendingSessionVerification } from '../shared-types'; +import { sendToBackground, truncateAddress } from './helpers'; + +interface ApprovalsViewProps { + discoveries: PendingDiscovery[]; + transactions: PendingTransaction[]; + pendingCapabilities: PendingCapabilities[]; + onRefresh: () => void; +} + +export function ApprovalsView({ + discoveries, + transactions, + pendingCapabilities, + onRefresh, +}: ApprovalsViewProps) { + const [processing, setProcessing] = useState(null); + const [error, setError] = useState(null); + + /** Generic approval/rejection handler — eliminates per-type boilerplate. */ + const handleAction = async (type: string, payload: Record, key: string) => { + try { + setProcessing(key); + setError(null); + await sendToBackground({ type, ...payload }); + } catch (err: any) { + // "Transaction not found" means it was already processed — just refresh + if (type !== MessageTypes.APPROVE_TRANSACTION) { + setError(err.message); + } else { + console.debug('Approve failed (likely stale):', err.message); + } + } finally { + setProcessing(null); + onRefresh(); + } + }; + + const handleApproveConnection = (requestId: string) => + handleAction(MessageTypes.APPROVE_CONNECTION, { requestId }, requestId); + const handleRejectConnection = (requestId: string) => + handleAction(MessageTypes.REJECT_CONNECTION, { requestId }, requestId); + const handleApproveTx = (messageId: string) => + handleAction(MessageTypes.APPROVE_TRANSACTION, { messageId }, messageId); + const handleRejectTx = (messageId: string) => + handleAction(MessageTypes.REJECT_TRANSACTION, { messageId }, messageId); + const handleApproveCapabilities = (messageId: string) => + handleAction(MessageTypes.APPROVE_CAPABILITIES, { messageId }, messageId); + const handleRejectCapabilities = (messageId: string) => + handleAction(MessageTypes.REJECT_CAPABILITIES, { messageId }, messageId); + + if (discoveries.length === 0 && transactions.length === 0 && pendingCapabilities.length === 0) { + return ( +
+
✓
+
No pending approvals
+
+ ); + } + + return ( +
+ {error &&
{error}
} + + {discoveries.map((discovery) => ( + handleApproveConnection(discovery.requestId)} + onReject={() => handleRejectConnection(discovery.requestId)} + processing={processing === discovery.requestId} + /> + ))} + + {transactions.map((tx) => ( + handleApproveTx(tx.messageId)} + onReject={() => handleRejectTx(tx.messageId)} + processing={processing === tx.messageId} + /> + ))} + + {pendingCapabilities.map((cap) => ( + handleApproveCapabilities(cap.messageId)} + onReject={() => handleRejectCapabilities(cap.messageId)} + processing={processing === cap.messageId} + /> + ))} +
+ ); +} + +export function SessionVerificationView({ verification, onConfirm, onReject }: { + verification: PendingSessionVerification; + onConfirm: () => void; + onReject: () => void; +}) { + const emojis = hashToEmoji(verification.verificationHash); + + return ( +
+
+
+
🔐
+
+
{getOriginHost(verification.origin)}
+
Verify Connection
+
+
+ +
+

+ Confirm these emojis match what the dApp shows: +

+
{emojis}
+
+ +
+ + +
+
+
+ ); +} + +// docs:start:connection-approval +interface ConnectionApprovalProps { + discovery: PendingDiscovery; + onApprove: () => void; + onReject: () => void; + processing: boolean; +} + +function ConnectionApproval({ + discovery, + onApprove, + onReject, + processing, +}: ConnectionApprovalProps) { + return ( +
+
+
🔗
+
+
{getOriginHost(discovery.origin)}
+
Connection Request
+
+
+ +
+
+ Origin + {discovery.origin} +
+ {discovery.appId && ( +
+ App ID + {discovery.appId} +
+ )} +
+ +
+ + +
+
+ ); +} +// docs:end:connection-approval + +// docs:start:transaction-approval +interface TransactionApprovalProps { + transaction: PendingTransaction; + onApprove: () => void; + onReject: () => void; + processing: boolean; +} + +function TransactionApproval({ + transaction, + onApprove, + onReject, + processing, +}: TransactionApprovalProps) { + const methodLabels: Record = { + sendTx: 'Send Transaction', + simulateTx: 'Simulate Transaction', + createAuthWit: 'Create Authorization', + profileTx: 'Profile Transaction', + batch: 'Batch Transaction', + }; + + return ( +
+
+
📝
+
+
{getOriginHost(transaction.origin)}
+
+ {methodLabels[transaction.method] || transaction.method} +
+
+
+ +
+
+ From + {truncateAddress(transaction.from)} +
+
+ Method + {transaction.method} +
+ + {/* sendTx: show function calls from the execution payload (args[0]) */} + {transaction.method === 'sendTx' && transaction.args?.[0]?.calls && ( +
+
Function Calls:
+ {transaction.args[0].calls.map((call: any, i: number) => ( +
+
{call.name || 'Unknown Function'}
+
Contract: {truncateAddress(call.to?.toString?.() || '')}
+
+ ))} +
+ )} + + {/* batch: show list of batched operations and their function calls */} + {transaction.method === 'batch' && Array.isArray(transaction.args?.[0]) && ( +
+
Batched Operations:
+ {transaction.args[0].map((method: any, i: number) => ( +
+
+ {methodLabels[method.name] || method.name} +
+ {method.name === 'sendTx' && method.args?.[0]?.calls?.map((call: any, j: number) => ( +
+ {call.name || 'Unknown'} → {truncateAddress(call.to?.toString?.() || '')} +
+ ))} +
+ ))} +
+ )} +
+ +
+ + +
+
+ ); +} +// docs:end:transaction-approval + +interface ExpandableSection { + summary: string; + items: string[]; +} + +interface CapabilityDescription { + label: string; + details: string[]; + expandable: ExpandableSection[]; +} + +function formatPatterns(scope: any): { summary: string; items: string[] } { + if (!Array.isArray(scope)) return { summary: 'unknown', items: [] }; + const items = scope.map((p: any) => { + const contract = p.contract === '*' ? '*' : truncateAddress(String(p.contract)); + const fn = p.function || '*'; + return `${contract}:${fn}`; + }); + return { summary: `${scope.length} specific pattern(s)`, items }; +} + +function describeCapability(cap: any): CapabilityDescription { + switch (cap.type) { + case 'accounts': { + const details = []; + if (cap.canGet) details.push('View account addresses'); + if (cap.canCreateAuthWit) details.push('Create authentication witnesses'); + return { label: 'Account Access', details: details.length ? details : ['Basic account access'], expandable: [] }; + } + case 'contracts': { + const expandable: ExpandableSection[] = []; + let scopeText: string; + if (cap.contracts === '*') { + scopeText = 'Scope: All contracts'; + } else if (Array.isArray(cap.contracts)) { + scopeText = `Scope: ${cap.contracts.length} specific contract(s)`; + expandable.push({ + summary: `${cap.contracts.length} specific contract(s)`, + items: cap.contracts.map((c: any) => truncateAddress(String(c))), + }); + } else { + scopeText = 'Scope: unknown'; + } + return { + label: 'Contract Access', + details: [ + cap.canRegister ? 'Register contracts' : '', + cap.canGetMetadata ? 'Query contract metadata' : '', + scopeText, + ].filter(Boolean), + expandable, + }; + } + case 'transaction': { + const expandable: ExpandableSection[] = []; + let scopeText: string; + if (cap.scope === '*') { + scopeText = 'Scope: Any transaction'; + } else if (Array.isArray(cap.scope)) { + const { summary, items } = formatPatterns(cap.scope); + scopeText = `Scope: ${summary}`; + expandable.push({ summary, items }); + } else { + scopeText = 'Scope: unknown'; + } + return { label: 'Send Transactions', details: [scopeText], expandable }; + } + case 'simulation': { + const expandable: ExpandableSection[] = []; + const details: string[] = []; + if (cap.transactions) { + if (cap.transactions.scope === '*') { + details.push('Tx simulation: any'); + } else if (Array.isArray(cap.transactions.scope)) { + const { summary, items } = formatPatterns(cap.transactions.scope); + details.push(`Tx simulation: ${summary}`); + expandable.push({ summary: `Tx: ${summary}`, items }); + } + } + if (cap.utilities) { + if (cap.utilities.scope === '*') { + details.push('Utility calls: any'); + } else if (Array.isArray(cap.utilities.scope)) { + const { summary, items } = formatPatterns(cap.utilities.scope); + details.push(`Utility calls: ${summary}`); + expandable.push({ summary: `Util: ${summary}`, items }); + } + } + return { label: 'Simulate Transactions', details, expandable }; + } + case 'data': + return { + label: 'Data Access', + details: [ + cap.addressBook ? 'Address book' : '', + cap.privateEvents ? 'Private events' : '', + ].filter(Boolean), + expandable: [], + }; + default: + return { label: cap.type, details: ['Unknown capability'], expandable: [] }; + } +} + +interface CapabilitiesApprovalProps { + pending: PendingCapabilities; + onApprove: () => void; + onReject: () => void; + processing: boolean; +} + +function CapabilitiesApproval({ + pending, + onApprove, + onReject, + processing, +}: CapabilitiesApprovalProps) { + const [expanded, setExpanded] = useState>(new Set()); + + const toggleExpanded = (key: string) => { + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + + return ( +
+
+
🔒
+
+
{getOriginHost(pending.origin)}
+
Capabilities Request
+
+
+ +
+
+ App + + {pending.appMetadata.name} v{pending.appMetadata.version} + +
+ {pending.appMetadata.description && ( +
+ Description + {pending.appMetadata.description} +
+ )} +
+ Origin + {pending.origin} +
+ +
+
Requested Permissions:
+ {pending.capabilities.map((cap, i) => { + const desc = describeCapability(cap); + return ( +
+
{desc.label}
+ {desc.details.map((detail, j) => ( +
{detail}
+ ))} + {desc.expandable.map((section, k) => { + const key = `${i}-${k}`; + const isExpanded = expanded.has(key); + return ( +
+ + {isExpanded && ( +
+ {section.items.map((item, l) => ( +
{item}
+ ))} +
+ )} +
+ ); + })} +
+ ); + })} +
+
+ +
+ + +
+
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx new file mode 100644 index 00000000000..2201124a29f --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; + +import { createAndActivateAccount } from './helpers'; + +export function CreateAccountView({ onCreated }: { onCreated: () => void }) { + const [alias, setAlias] = useState(''); + const [creating, setCreating] = useState(false); + const [error, setError] = useState(null); + + const handleCreate = async () => { + setCreating(true); + setError(null); + try { + await createAndActivateAccount(alias || 'My Account'); + onCreated(); + } catch (err: any) { + setError(err.message); + } finally { + setCreating(false); + } + }; + + return ( +
+ {error &&
{error}
} + +
+ + setAlias(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleCreate()} + placeholder="My Account" + disabled={creating} + /> +
+ + +
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/Header.tsx b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/Header.tsx new file mode 100644 index 00000000000..610405811bb --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/Header.tsx @@ -0,0 +1,104 @@ +import React, { useState, useEffect, useRef } from 'react'; + +import { getOriginHost } from '../utils'; +import type { ConnectedSite } from './types'; + +export function Header({ pendingCount, onApprovalClick, connectedSites, onDisconnect, onSettingsClick }: { + pendingCount: number; + onApprovalClick: () => void; + connectedSites: ConnectedSite[]; + onDisconnect: (sessionId: string) => void; + onSettingsClick: () => void; +}) { + const [showDropdown, setShowDropdown] = useState(false); + const dropdownRef = useRef(null); + const connected = connectedSites.length > 0; + + // Close dropdown when clicking outside + useEffect(() => { + if (!showDropdown) return; + const handleClick = (e: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setShowDropdown(false); + } + }; + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [showDropdown]); + + return ( +
+

Aztec Wallet

+
+ local + +
+ + + {showDropdown && connected && ( +
+ {connectedSites.map((site) => ( +
+ {getOriginHost(site.origin)} + +
+ ))} +
+ )} +
+ + + + +
+
+ ); +} + +export function SubHeader({ title, onBack }: { title: string; onBack: () => void }) { + return ( +
+ + {title} +
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx new file mode 100644 index 00000000000..16a4677133b --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; + +import { MessageTypes } from '../config'; +import { sendToBackground, waitForTask } from './helpers'; + +export function LockScreen({ onUnlocked }: { onUnlocked: () => void }) { + const [password, setPassword] = useState(''); + const [unlocking, setUnlocking] = useState(false); + const [error, setError] = useState(null); + + const handleUnlock = async () => { + if (!password) return; + setUnlocking(true); + setError(null); + try { + const { taskId } = await sendToBackground({ + type: MessageTypes.UNLOCK_WALLET, + password, + }); + await waitForTask(taskId); + onUnlocked(); + } catch (err: any) { + setError(err.message); + } finally { + setUnlocking(false); + } + }; + + return ( +
+
🔒
+

Wallet Locked

+

+ Enter your password to unlock. +

+ + {error &&
{error}
} + +
+ setPassword(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleUnlock()} + placeholder="Enter password" + disabled={unlocking} + /> +
+ + +
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/MainScreen.tsx b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/MainScreen.tsx new file mode 100644 index 00000000000..5b8fdc9c1b7 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/MainScreen.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; + +import { MessageTypes } from '../config'; +import type { StoredAccount } from './types'; +import { sendToBackground, waitForTask } from './helpers'; + +// docs:start:main-page +interface MainPageProps { + account: StoredAccount; + busy: boolean; + onSwitcherOpen: () => void; + onRefresh: () => void; +} + +export function MainPage({ account, busy, onSwitcherOpen, onRefresh }: MainPageProps) { + const [deployError, setDeployError] = useState(null); + const [copied, setCopied] = useState(false); + + const handleDeploy = async () => { + setDeployError(null); + try { + const { taskId } = await sendToBackground({ + type: MessageTypes.DEPLOY_ACCOUNT, + address: account.address, + }); + await waitForTask(taskId); + onRefresh(); + } catch (err: any) { + setDeployError(err.message); + } + }; + + const copyAddress = () => { + navigator.clipboard.writeText(account.address); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ {/* Account selector pill */} + + + {/* Account detail card */} +
+
+ + {account.isDeployed ? 'Deployed' : 'Not Deployed'} + +
+ +
+ {account.address} +
+ + +
+ + {/* Deploy button */} + {!account.isDeployed && ( +
+ {deployError &&
{deployError}
} + +
+ )} + +
+ ); +} +// docs:end:main-page diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx new file mode 100644 index 00000000000..d6f8debe060 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx @@ -0,0 +1,154 @@ +import React, { useState, useRef } from 'react'; + +import { MessageTypes, AZTEC_PACKAGES_VERSION } from '../config'; +import type { WalletExportData } from '../shared-types'; +import { sendToBackground, waitForTask } from './helpers'; + +export function SettingsPage({ onImportStart }: { onImportStart: (data: WalletExportData) => void }) { + const [exporting, setExporting] = useState(false); + const [importPreview, setImportPreview] = useState(null); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + + const handleExport = async () => { + setExporting(true); + setError(null); + try { + const { taskId } = await sendToBackground({ type: MessageTypes.EXPORT_WALLET }); + const result = await waitForTask(taskId); + + const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const date = new Date().toISOString().slice(0, 10); + const a = document.createElement('a'); + a.href = url; + a.download = `aztec-wallet-backup-${date}.json`; + a.click(); + URL.revokeObjectURL(url); + } catch (err: any) { + setError(err.message); + } finally { + setExporting(false); + } + }; + + const handleFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setError(null); + setImportPreview(null); + + const reader = new FileReader(); + reader.onload = (ev) => { + try { + const data = JSON.parse(ev.target?.result as string) as WalletExportData; + if (data.version !== 1) { + setError('Unsupported backup version'); + return; + } + if (!Array.isArray(data.accounts) || data.accounts.some((a) => !a.address || !a.secret || !a.salt)) { + setError('Invalid backup file: missing account data'); + return; + } + setImportPreview(data); + } catch { + setError('Failed to parse backup file'); + } + }; + reader.readAsText(file); + // Reset input so the same file can be selected again + e.target.value = ''; + }; + + const versionMismatch = importPreview && importPreview.aztecPackagesVersion !== AZTEC_PACKAGES_VERSION; + + return ( +
+ {error &&
{error}
} + + {/* Export section */} +
+
Export Wallet Backup
+
+ Download a JSON file containing all your accounts with decrypted secrets. + Store this file securely. +
+ +
+ + {/* Import section */} +
+
Import Wallet Backup
+
+ Restore accounts from a previously exported backup file. +
+ + + + + + {importPreview && ( +
+
Backup Preview
+
+ Accounts + {importPreview.accounts.length} +
+
+ Aztec Version + {importPreview.aztecPackagesVersion} +
+
+ Exported + {new Date(importPreview.exportedAt).toLocaleDateString()} +
+ + {versionMismatch && ( +
+ Version mismatch! This backup was created with {importPreview.aztecPackagesVersion} but + the current wallet uses {AZTEC_PACKAGES_VERSION}. Imported addresses may not match. +
+ )} + +
+ This will wipe your current wallet. You will need to set a new master password. +
+ + +
+ )} +
+ + {/* Version info */} +
+ Aztec Packages Version + {AZTEC_PACKAGES_VERSION} +
+
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx new file mode 100644 index 00000000000..9a27d42df45 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; + +import { MessageTypes } from '../config'; +import { sendToBackground, waitForTask, createAndActivateAccount } from './helpers'; + +export function SetupScreen({ onComplete, skipAccountCreation = false }: { onComplete: () => void; skipAccountCreation?: boolean }) { + const [step, setStep] = useState<'password' | 'account'>('password'); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [alias, setAlias] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSetPassword = async () => { + if (password.length < 8) { + setError('Password must be at least 8 characters'); + return; + } + if (password !== confirmPassword) { + setError('Passwords do not match'); + return; + } + + setLoading(true); + setError(null); + try { + const { taskId } = await sendToBackground({ type: MessageTypes.SETUP_PASSWORD, password }); + await waitForTask(taskId); + if (skipAccountCreation) { + onComplete(); + return; + } + setStep('account'); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleCreateFirstAccount = async () => { + setLoading(true); + setError(null); + try { + await createAndActivateAccount(alias || 'Account 1'); + onComplete(); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + if (step === 'password') { + return ( +
+
🔒
+

Welcome to Aztec Wallet

+

+ Set a master password to protect your accounts. +

+ + {error &&
{error}
} + +
+ setPassword(e.target.value)} + placeholder="Password (min 8 characters)" + disabled={loading} + /> +
+ +
+ setConfirmPassword(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSetPassword()} + placeholder="Confirm password" + disabled={loading} + /> +
+ + +
+ ); + } + + return ( +
+
👤
+

Create Your First Account

+

+ Choose a name for your first Aztec account. +

+ + {error &&
{error}
} + +
+ setAlias(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleCreateFirstAccount()} + placeholder="Account name (e.g. Account 1)" + disabled={loading} + /> +
+ + +
+ ); +} diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/helpers.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/helpers.ts new file mode 100644 index 00000000000..252c5d3a22a --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/helpers.ts @@ -0,0 +1,100 @@ +import { MessageTarget, MessageTypes } from '../config'; +import type { BackgroundTask } from '../shared-types'; + +// docs:start:send-message +/** + * Sends a message to the background script via chrome.runtime.sendMessage. + * Used for simple request/response calls (accounts, status, approvals). + */ +export function sendToBackground(message: any): Promise { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + { ...message, target: MessageTarget.BACKGROUND }, + (response) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + return; + } + if (response?.success) { + resolve(response.result); + } else { + reject(new Error(response?.error || 'Unknown error')); + } + } + ); + }); +} +// docs:end:send-message + +/** + * Registry of pending task promises. (#12) + * + * When the background pushes a task-update via the port, we resolve/reject + * the corresponding promise. If the popup was closed and reopened, the initial + * state push includes all recent tasks — we process completed ones immediately + * so waitForTask resolves even if the task finished while we were closed. + */ +export const pendingTaskCallbacks = new Map void; + reject: (error: Error) => void; +}>(); + +export function waitForTask(taskId: string, timeoutMs = 300000): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + pendingTaskCallbacks.delete(taskId); + reject(new Error('Task timed out')); + }, timeoutMs); + + pendingTaskCallbacks.set(taskId, { + resolve: (value: any) => { + clearTimeout(timeoutId); + pendingTaskCallbacks.delete(taskId); + resolve(value); + }, + reject: (error: Error) => { + clearTimeout(timeoutId); + pendingTaskCallbacks.delete(taskId); + reject(error); + }, + }); + }); +} + +export function handleTaskUpdate(task: BackgroundTask) { + const callbacks = pendingTaskCallbacks.get(task.id); + if (!callbacks) return; + + if (task.status === 'success') { + callbacks.resolve(task.result); + } else if (task.status === 'error') { + callbacks.reject(new Error(task.error || 'Task failed')); + } +} + +// docs:start:helpers +export function truncateAddress(address: string): string { + if (!address) return ''; + if (address.length <= 16) return address; + return `${address.slice(0, 8)}...${address.slice(-6)}`; +} + +/** + * Creates an account and sets it as active. + * Shared between SetupScreen (first account) and CreateAccountView (additional accounts). + */ +export async function createAndActivateAccount(alias: string): Promise { + const { taskId } = await sendToBackground({ + type: MessageTypes.CREATE_ACCOUNT, + alias, + }); + const result = await waitForTask(taskId); + + if (result?.address) { + await sendToBackground({ + type: MessageTypes.SET_ACTIVE_ACCOUNT, + address: result.address, + }); + } +} +// docs:end:helpers diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/popup.tsx b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/popup.tsx new file mode 100644 index 00000000000..e1f84f551d0 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/popup.tsx @@ -0,0 +1,467 @@ +/** + * Popup UI for the Aztec Tutorial Wallet. + * + * MetaMask-like layout with: + * - Setup screen (first-time password + first account) + * - Lock screen (unlock with master password) + * - Main page (active account detail, deploy button) + * - Account switcher overlay + * - Create account sub-page + * - Approvals view for connection/transaction requests + * + * Communication: + * - Persistent port to background for real-time push updates (no polling) + * - Port auto-reconnects if background disconnects (#9) + * - Initial state comes from the port, not separate fetches (#10) + */ + +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { MessageTypes } from '../config'; +import type { WalletExportData, PublicAccountInfo, PendingTransaction, PendingCapabilities, ConnectedSite, PendingSessionVerification, BackgroundTask, View } from '../shared-types'; +import type { PendingDiscovery } from '@aztec-labs/wallet-sdk/extension/handlers'; +import { sendToBackground, waitForTask, handleTaskUpdate } from './helpers'; +import { Header, SubHeader } from './Header'; +import { SetupScreen } from './SetupScreen'; +import { LockScreen } from './LockScreen'; +import { MainPage } from './MainScreen'; +import { AccountSwitcher } from './AccountSwitcher'; +import { CreateAccountView } from './CreateAccountView'; +import { ApprovalsView, SessionVerificationView } from './ApprovalView'; +import { SettingsPage } from './SettingsPage'; + +// docs:start:main-app +function App() { + const [view, setView] = useState('loading'); + const [accounts, setAccounts] = useState([]); + const [activeAccount, setActiveAccount] = useState(null); + const [discoveries, setDiscoveries] = useState([]); + const [transactions, setTransactions] = useState([]); + const [connectedSites, setConnectedSites] = useState([]); + const [sessionVerifications, setSessionVerifications] = useState([]); + const [pendingCapabilities, setPendingCapabilities] = useState([]); + const [runningTasks, setRunningTasks] = useState([]); + const [error, setError] = useState(null); + const [elapsed, setElapsed] = useState(0); + const [pendingImportData, setPendingImportData] = useState(null); + const portRef = useRef(null); + const reconnectTimerRef = useRef | null>(null); + + const pendingCount = discoveries.length + transactions.length + sessionVerifications.length + pendingCapabilities.length; + + /** + * Applies state pushed from the background via the port. (#10) + * This is the single source of truth for pending items, tasks, and connected sites. + */ + const applyBackgroundState = useCallback((data: any) => { + if (data.discoveries) setDiscoveries(data.discoveries); + if (data.transactions) setTransactions(data.transactions); + if (data.pendingSessionVerifications) setSessionVerifications(data.pendingSessionVerifications); + if (data.pendingCapabilities) setPendingCapabilities(data.pendingCapabilities); + if (data.connectedSites) setConnectedSites(data.connectedSites); + if (data.tasks) { + setRunningTasks(data.tasks.filter((t: BackgroundTask) => t.status === 'running')); + // Resolve any waitForTask promises for completed tasks (#12) + for (const task of data.tasks) { + handleTaskUpdate(task); + } + } + }, []); + + /** + * Loads account data and determines the initial view. + * Pending items come from the port push, NOT from a separate fetch. (#10) + */ + const loadData = useCallback(async () => { + try { + setError(null); + + const [accountsResult, activeAccountResult, statusResult] = await Promise.all([ + sendToBackground({ type: MessageTypes.GET_ACCOUNTS }), + sendToBackground({ type: MessageTypes.GET_ACTIVE_ACCOUNT }), + sendToBackground({ type: MessageTypes.GET_WALLET_STATUS }), + ]); + + setAccounts(accountsResult || []); + setActiveAccount(activeAccountResult || null); + + const unlocked = statusResult?.unlocked || false; + const hasPassword = statusResult?.hasPassword || false; + const hasAccounts = (accountsResult || []).length > 0; + + if (!hasPassword && !hasAccounts) { + setView('setup'); + } else if (!unlocked) { + setView('lock'); + } else { + setView((prev) => prev === 'loading' ? 'main' : prev); + } + } catch (err: any) { + console.error('Failed to load data:', err); + setError(err.message); + setView('setup'); + } + }, []); + + /** + * Connect persistent port to background. (#9) + * Reconnects automatically if the background disconnects (e.g., SW restart). + */ + const connectPort = useCallback(() => { + if (reconnectTimerRef.current) { + clearTimeout(reconnectTimerRef.current); + reconnectTimerRef.current = null; + } + + try { + const port = chrome.runtime.connect({ name: 'popup' }); + portRef.current = port; + + port.onMessage.addListener((message: any) => { + if (message.type === 'state') { + applyBackgroundState(message.data); + + // Auto-navigate to approvals/verification if there are pending items + const d = message.data.discoveries?.length || 0; + const t = message.data.transactions?.length || 0; + const sv = message.data.pendingSessionVerifications?.length || 0; + const c = message.data.pendingCapabilities?.length || 0; + if (sv > 0) { + setView((prev) => (prev === 'main' || prev === 'loading' || prev === 'approvals') ? 'verifySession' : prev); + } else if (d > 0 || t > 0 || c > 0) { + setView((prev) => (prev === 'main' || prev === 'loading') ? 'approvals' : prev); + } + } else if (message.type === 'task-update') { + const task: BackgroundTask = message.task; + handleTaskUpdate(task); + + setRunningTasks((prev) => { + if (task.status === 'running') { + const existing = prev.findIndex((t) => t.id === task.id); + if (existing >= 0) { + const updated = [...prev]; + updated[existing] = task; + return updated; + } + return [...prev, task]; + } + return prev.filter((t) => t.id !== task.id); + }); + + // Refresh account data if a state-changing task completed + if (task.status === 'success') { + const refreshTypes = ['create-account', 'deploy-account', 'unlock', 'setup-password', 'import-wallet-accounts']; + if (refreshTypes.includes(task.type)) { + loadData(); + } + } + } + }); + + port.onDisconnect.addListener(() => { + console.log('[popup] Port disconnected, will reconnect...'); + portRef.current = null; + // Reconnect after a short delay (SW may be restarting) (#9) + reconnectTimerRef.current = setTimeout(connectPort, 1000); + }); + + } catch (err) { + console.error('[popup] Failed to connect port:', err); + // Retry connection (#9) + reconnectTimerRef.current = setTimeout(connectPort, 2000); + } + }, [applyBackgroundState, loadData]); + + useEffect(() => { + connectPort(); + loadData(); + + return () => { + if (reconnectTimerRef.current) { + clearTimeout(reconnectTimerRef.current); + } + if (portRef.current) { + portRef.current.disconnect(); + portRef.current = null; + } + }; + }, [connectPort, loadData]); + + // Reactive auto-navigation: ensures the popup shows the right view whenever + // pending items exist, even if the port message handler's auto-nav fired while + // the popup was on a non-target view (e.g. 'setup' or 'lock'). + useEffect(() => { + if (sessionVerifications.length > 0 && + (view === 'main' || view === 'loading' || view === 'approvals')) { + setView('verifySession'); + } else if ((discoveries.length > 0 || transactions.length > 0 || pendingCapabilities.length > 0) && + (view === 'main' || view === 'loading')) { + setView('approvals'); + } + }, [sessionVerifications, discoveries, transactions, pendingCapabilities, view]); + + // Tick elapsed time while tasks are running + useEffect(() => { + if (runningTasks.length === 0) { + setElapsed(0); + return; + } + const oldest = Math.min(...runningTasks.map((t) => t.startedAt)); + setElapsed(Math.round((Date.now() - oldest) / 1000)); + const timer = setInterval(() => { + setElapsed(Math.round((Date.now() - oldest) / 1000)); + }, 1000); + return () => clearInterval(timer); + }, [runningTasks]); + + const handleUnlocked = () => { + setView('main'); + loadData(); + }; + + const handleSetupComplete = async () => { + if (pendingImportData) { + try { + const { taskId } = await sendToBackground({ + type: MessageTypes.IMPORT_WALLET_ACCOUNTS, + accounts: pendingImportData.accounts, + activeAccount: pendingImportData.activeAccount, + }); + await waitForTask(taskId); + setPendingImportData(null); + } catch (err: any) { + console.error('Failed to import accounts:', err); + setError(err.message); + setPendingImportData(null); + } + } + setView('main'); + loadData(); + }; + + const handleImportStart = (data: WalletExportData) => { + setPendingImportData(data); + sendToBackground({ type: MessageTypes.IMPORT_WALLET }).then(() => { + setView('setup'); + }).catch((err) => { + console.error('Failed to wipe wallet:', err); + setError(err.message); + setPendingImportData(null); + }); + }; + + const handleDisconnectSite = async (sessionId: string) => { + try { + await sendToBackground({ type: MessageTypes.DISCONNECT_SESSION, sessionId }); + } catch (err) { + console.error('Failed to disconnect session:', err); + } + }; + + const handleConfirmSession = async (sessionId: string) => { + try { + await sendToBackground({ type: MessageTypes.CONFIRM_SESSION, sessionId }); + setView('main'); + } catch (err) { + console.error('Failed to confirm session:', err); + } + }; + + const handleRejectSession = async (sessionId: string) => { + try { + await sendToBackground({ type: MessageTypes.REJECT_SESSION, sessionId }); + setView('main'); + } catch (err) { + console.error('Failed to reject session:', err); + } + }; + + const activeAccountData = accounts.find((a) => a.address === activeAccount) || accounts[0] || null; + + const handleApprovalClick = () => { + if (sessionVerifications.length > 0) { + setView('verifySession'); + } else { + setView('approvals'); + } + }; + + const noopDisconnect = () => {}; + + if (view === 'loading') { + return ( +
+
{}} connectedSites={[]} onDisconnect={noopDisconnect} onSettingsClick={() => {}} /> +
+
+ Loading... +
+
+ ); + } + + if (view === 'setup') { + return ( +
+
{}} connectedSites={[]} onDisconnect={noopDisconnect} onSettingsClick={() => {}} /> + +
+ ); + } + + if (view === 'lock') { + return ( +
+
{}} connectedSites={[]} onDisconnect={noopDisconnect} onSettingsClick={() => {}} /> + +
+ ); + } + + if (view === 'approvals') { + return ( +
+
{}} connectedSites={connectedSites} onDisconnect={handleDisconnectSite} onSettingsClick={() => setView('settings')} /> + setView('main')} /> + +
+ ); + } + + if (view === 'verifySession') { + const currentVerification = sessionVerifications[0]; + return ( +
+
{}} connectedSites={connectedSites} onDisconnect={handleDisconnectSite} onSettingsClick={() => setView('settings')} /> + setView('main')} /> + {currentVerification ? ( + handleConfirmSession(currentVerification.sessionId)} + onReject={() => handleRejectSession(currentVerification.sessionId)} + /> + ) : ( +
+
✓
+
No pending verifications
+
+ )} +
+ ); + } + + if (view === 'switcher') { + return ( +
+
setView('settings')} /> + setView('main')} /> + { + sendToBackground({ type: MessageTypes.SET_ACTIVE_ACCOUNT, address }) + .then(() => { setActiveAccount(address); setView('main'); }) + .catch((err) => console.error('Failed to switch account:', err)); + }} + onCreateNew={() => setView('createAccount')} + /> +
+ ); + } + + if (view === 'createAccount') { + return ( +
+
setView('settings')} /> + setView('switcher')} /> + { setView('main'); loadData(); }} /> +
+ ); + } + + if (view === 'settings') { + return ( +
+
{}} /> + setView('main')} /> + +
+ ); + } + + // Main view + return ( +
+
setView('settings')} /> + + {error &&
{error}
} + + {runningTasks.length > 0 && ( +
+
+
+ {runningTasks.map((t) => { + const labels: Record = { + 'deploy-account': 'Deploying account...', + 'create-account': 'Creating account...', + 'unlock': 'Unlocking wallet...', + 'setup-password': 'Setting up password...', + 'export-wallet': 'Exporting wallet...', + 'import-wallet-accounts': 'Importing accounts...', + }; + const genericLabel = t.type.startsWith('wallet:') + ? `Processing ${t.type.replace('wallet:', '')}...` + : t.type.startsWith('tx:') + ? `Executing ${t.type.replace('tx:', '')}...` + : labels[t.type] || 'Processing...'; + return ( +
+
{t.progress || genericLabel}
+ {t.progress && ( +
{genericLabel}
+ )} +
+ ); + })} +
+ Elapsed: {Math.floor(elapsed / 60)}:{String(elapsed % 60).padStart(2, '0')} +
+
+
+ )} + + {activeAccountData ? ( + 0} + onSwitcherOpen={() => setView('switcher')} + onRefresh={loadData} + /> + ) : ( +
+
👛
+
No accounts yet
+ +
+ )} +
+ ); +} +// docs:end:main-app + +// Mount the app +const root = createRoot(document.getElementById('root')!); +root.render(); diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/types.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/types.ts new file mode 100644 index 00000000000..16e1e8f4ab4 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/types.ts @@ -0,0 +1,18 @@ +/** + * Re-export shared types for the popup. + * The canonical definitions live in shared-types.ts — import from there + * or from this module (which re-exports everything the popup needs). + */ +export type { + PublicAccountInfo, + PendingTransaction, + PendingSessionVerification, + PendingCapabilities, + ConnectedSite, + BackgroundTask, + WalletExportData, + View, +} from '../shared-types'; + +/** Alias for popup components that display account info. */ +export type { PublicAccountInfo as StoredAccount } from '../shared-types'; diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/shared-types.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/shared-types.ts new file mode 100644 index 00000000000..4f8cc4c4448 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/shared-types.ts @@ -0,0 +1,97 @@ +/** + * Shared type definitions for the Aztec Tutorial Wallet extension. + * Single source of truth for types used across background, offscreen, and popup. + */ + +/** + * Public account info — the subset of account data safe to expose to the UI. + * Does NOT include encrypted secrets, IVs, or contract salts. + */ +export interface PublicAccountInfo { + address: string; + alias: string; + isDeployed: boolean; +} + +/** + * Pending transaction awaiting user approval. + */ +export interface PendingTransaction { + sessionId: string; + messageId: string; + method: string; + args: any; + from: string; + origin: string; + timestamp: number; +} + +/** + * Session pending emoji verification. + * After key exchange completes, we hold the session here until + * the user confirms the verification emojis match. + */ +export interface PendingSessionVerification { + sessionId: string; + origin: string; + appId: string; + verificationHash: string; + timestamp: number; +} + +/** + * Pending capability request awaiting user approval. + */ +export interface PendingCapabilities { + sessionId: string; + messageId: string; + origin: string; + /** App metadata from the capability manifest */ + appMetadata: { + name: string; + version: string; + description?: string; + url?: string; + icon?: string; + }; + /** Raw requested capabilities array */ + capabilities: Array<{ type: string; [key: string]: any }>; + timestamp: number; +} + +/** Connected site info for display in the popup. */ +export interface ConnectedSite { + sessionId: string; + origin: string; + appId: string; + connectedAt: number; +} + +/** Background task for long-running operations. */ +export interface BackgroundTask { + id: string; + type: string; + status: 'running' | 'success' | 'error'; + progress?: string; + result?: any; + error?: string; + startedAt: number; +} + +/** Wallet export data format for backup/restore. */ +export interface WalletExportData { + version: 1; + aztecPackagesVersion: string; + exportedAt: string; + accounts: Array<{ + address: string; + secret: string; + salt: string; + alias: string; + isDeployed: boolean; + }>; + activeAccount: string | null; +} + +/** Popup view state. */ +export type View = 'loading' | 'setup' | 'lock' | 'main' | 'switcher' | 'createAccount' | 'approvals' | 'verifySession' | 'settings'; diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/utils.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/utils.ts new file mode 100644 index 00000000000..a6d5d42a4b0 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/utils.ts @@ -0,0 +1,44 @@ +/** + * Shared utilities for the Aztec Tutorial Wallet extension. + * Extracted from multiple files to eliminate duplication. + */ + +/** + * Resolves the real Chrome runtime object. + * Needed in offscreen documents where polyfills may shadow the global. + */ +export function getChromeRuntime(): typeof chrome { + const candidates = [ + typeof self !== 'undefined' ? (self as any).chrome : undefined, + typeof window !== 'undefined' ? (window as any).chrome : undefined, + (globalThis as any).chrome, + ]; + + for (const candidate of candidates) { + if (candidate?.runtime?.sendMessage) { + return candidate; + } + } + + throw new Error('Chrome runtime API not available'); +} + +/** + * Extracts a human-readable error message from any thrown value. + */ +export function getErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + return String(error); +} + +/** + * Extracts the hostname from an origin URL, falling back to the raw string. + */ +export function getOriginHost(origin: string): string { + try { + return new URL(origin).host; + } catch { + return origin; + } +} + diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/storage.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/storage.ts new file mode 100644 index 00000000000..703afdc1383 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/storage.ts @@ -0,0 +1,333 @@ +/** + * Encrypted key storage for the Aztec Tutorial Wallet. + * + * Security design: + * - Master password is NEVER stored or cached as a string (#2) + * - Password verification uses PBKDF2 + AES-GCM (encrypt known plaintext) (#1) + * - A non-extractable CryptoKey is derived once at unlock and cached in memory + * - All account secrets are encrypted with this master CryptoKey + * - Each account gets a unique random IV for AES-GCM + * + * WARNING: This is for tutorial purposes. Production wallets should use + * hardware security modules, secure enclaves, or platform keychain APIs. + */ + +/** Known plaintext used to verify the master password is correct */ +const VERIFICATION_PLAINTEXT = 'aztec-wallet-verify'; + +/** + * PBKDF2 iteration count. + * OWASP 2023 recommends >= 600,000 for SHA-256. + * Higher = slower brute force, but also slower unlock. + */ +const PBKDF2_ITERATIONS = 600_000; + +/** Stored account data structure */ +export interface StoredAccount { + /** Aztec address as hex string */ + address: string; + /** Encrypted secret key (base64 encoded) */ + encryptedSecret: string; + /** IV for AES-GCM (base64 encoded) — unique per account */ + iv: string; + /** User-friendly alias */ + alias: string; + /** Whether the account contract is deployed */ + isDeployed: boolean; + /** Account contract salt as hex string */ + contractSalt: string; +} + +/** Storage keys — shared with background.ts for direct chrome.storage access. */ +export const STORAGE_KEYS = { + ACCOUNTS: 'aztec_accounts', + PASSWORD_DATA: 'aztec_password_data', + ACTIVE_ACCOUNT: 'aztec_active_account', +} as const; + +/** Encode bytes to base64 */ +function bytesToBase64(bytes: Uint8Array): string { + return btoa(String.fromCharCode(...bytes)); +} + +/** Decode base64 to bytes */ +function base64ToBytes(b64: string): Uint8Array { + return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); +} + +// docs:start:derive-master-key +/** + * Derives a non-extractable AES-GCM CryptoKey from a password and salt using PBKDF2. + * + * The key is non-extractable: once created, the raw key material cannot be read + * from JavaScript. This means even if an attacker has a reference to the CryptoKey + * object, they cannot extract the underlying bytes. + */ +export async function deriveMasterKey( + password: string, + salt: Uint8Array +): Promise { + const encoder = new TextEncoder(); + const passwordKey = await crypto.subtle.importKey( + 'raw', + encoder.encode(password), + 'PBKDF2', + false, + ['deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations: PBKDF2_ITERATIONS, + hash: 'SHA-256', + }, + passwordKey, + { name: 'AES-GCM', length: 256 }, + false, // non-extractable + ['encrypt', 'decrypt'] + ); +} +// docs:end:derive-master-key + +// docs:start:encrypt-decrypt +/** + * Encrypts a secret using a CryptoKey. + * Each call generates a fresh random IV for AES-GCM. + */ +export async function encryptWithKey( + secret: string, + key: CryptoKey +): Promise<{ encrypted: string; iv: string }> { + const iv = crypto.getRandomValues(new Uint8Array(12)); + const encoder = new TextEncoder(); + + const ciphertext = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + encoder.encode(secret) + ); + + return { + encrypted: bytesToBase64(new Uint8Array(ciphertext)), + iv: bytesToBase64(iv), + }; +} + +/** + * Decrypts a secret using a CryptoKey. + */ +export async function decryptWithKey( + encrypted: string, + iv: string, + key: CryptoKey +): Promise { + const ivBytes = base64ToBytes(iv); + const ciphertextBytes = base64ToBytes(encrypted); + + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: ivBytes }, + key, + ciphertextBytes + ); + + return new TextDecoder().decode(decrypted); +} +// docs:end:encrypt-decrypt + +import { getChromeRuntime } from '../utils'; + +/** + * Chrome runtime for messaging (available in offscreen documents). + * Offscreen documents don't have direct chrome.storage access, + * so all storage operations are proxied through the background script. + */ + +/** + * Storage operations are proxied through the background script because + * offscreen documents have limited chrome API access (no chrome.storage). + */ +const STORAGE_PROXY_TIMEOUT_MS = 30_000; // 30 seconds + +async function storageGet(key: string): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Storage proxy timed out')), STORAGE_PROXY_TIMEOUT_MS); + getChromeRuntime().runtime.sendMessage( + { type: 'storage-get', key }, + (response: any) => { + clearTimeout(timer); + if (response?.success) { + resolve(response.result); + } else { + reject(new Error(response?.error || 'Storage get failed')); + } + } + ); + }); +} + +async function storageSet(data: Record): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Storage proxy timed out')), STORAGE_PROXY_TIMEOUT_MS); + getChromeRuntime().runtime.sendMessage( + { type: 'storage-set', data }, + (response: any) => { + clearTimeout(timer); + if (response?.success) { + resolve(); + } else { + reject(new Error(response?.error || 'Storage set failed')); + } + } + ); + }); +} + +// docs:start:password-management +/** + * Sets the master password for the first time. (#1) + * + * Instead of storing an unsalted SHA-256 hash (vulnerable to rainbow tables), + * we derive a CryptoKey via PBKDF2 with a random salt, then encrypt a known + * plaintext. Verification = re-derive key + try to decrypt. + * + * Returns the derived master CryptoKey so the caller can cache it immediately. + */ +export async function setupPassword(password: string): Promise { + const salt = crypto.getRandomValues(new Uint8Array(32)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const masterKey = await deriveMasterKey(password, salt); + + const encoder = new TextEncoder(); + const ciphertext = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + masterKey, + encoder.encode(VERIFICATION_PLAINTEXT) + ); + + await storageSet({ + [STORAGE_KEYS.PASSWORD_DATA]: { + salt: bytesToBase64(salt), + iv: bytesToBase64(iv), + verifier: bytesToBase64(new Uint8Array(ciphertext)), + }, + }); + + return masterKey; +} + +/** + * Verifies the password and returns the derived master CryptoKey. (#1, #2) + * + * If the password is correct, returns the non-extractable CryptoKey. + * If wrong, returns null (AES-GCM decryption fails with wrong key). + * The caller should cache the CryptoKey and discard the password string. + */ +export async function verifyAndDeriveMasterKey(password: string): Promise { + const data = await storageGet(STORAGE_KEYS.PASSWORD_DATA); + if (!data) return null; + + const salt = base64ToBytes(data.salt); + const iv = base64ToBytes(data.iv); + const verifier = base64ToBytes(data.verifier); + + const masterKey = await deriveMasterKey(password, salt); + + try { + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + masterKey, + verifier + ); + const decoded = new TextDecoder().decode(decrypted); + if (decoded === VERIFICATION_PLAINTEXT) { + return masterKey; + } + return null; + } catch { + // AES-GCM decryption throws on wrong key (authentication tag mismatch) + return null; + } +} + +/** + * Checks if a master password has been set. + */ +export async function hasPassword(): Promise { + const data = await storageGet(STORAGE_KEYS.PASSWORD_DATA); + return !!data; +} +// docs:end:password-management + +// docs:start:account-operations +/** + * Saves an account to storage. + */ +export async function saveAccount(account: StoredAccount): Promise { + const accounts = await getStoredAccounts(); + const existingIndex = accounts.findIndex((a) => a.address === account.address); + + if (existingIndex >= 0) { + accounts[existingIndex] = account; + } else { + accounts.push(account); + } + + await storageSet({ [STORAGE_KEYS.ACCOUNTS]: accounts }); +} + +/** + * Retrieves all stored accounts. + */ +export async function getStoredAccounts(): Promise { + const result = await storageGet(STORAGE_KEYS.ACCOUNTS); + return result || []; +} + +/** + * Gets a specific account by address. + */ +export async function getStoredAccount( + address: string +): Promise { + const accounts = await getStoredAccounts(); + return accounts.find((a) => a.address === address); +} + +/** + * Updates an account's deployment status. + */ +export async function markAccountDeployed(address: string): Promise { + const accounts = await getStoredAccounts(); + const account = accounts.find((a) => a.address === address); + if (account) { + account.isDeployed = true; + await storageSet({ [STORAGE_KEYS.ACCOUNTS]: accounts }); + } +} + +/** + * Removes an account from storage. + */ +export async function removeAccount(address: string): Promise { + const accounts = await getStoredAccounts(); + const filtered = accounts.filter((a) => a.address !== address); + await storageSet({ [STORAGE_KEYS.ACCOUNTS]: filtered }); +} + +/** + * Gets the active account address. + */ +export async function getActiveAccount(): Promise { + const result = await storageGet(STORAGE_KEYS.ACTIVE_ACCOUNT); + return result || null; +} + +/** + * Sets the active account address. + */ +export async function setActiveAccount(address: string): Promise { + await storageSet({ [STORAGE_KEYS.ACTIVE_ACCOUNT]: address }); +} +// docs:end:account-operations diff --git a/docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts new file mode 100644 index 00000000000..348ceaa5ba7 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts @@ -0,0 +1,157 @@ +/** + * Wallet implementation for the Aztec Tutorial Wallet extension. + * + * With the crossOriginIsolated monkey-patch, Barretenberg WASM works in Chrome + * extension offscreen documents. This allows full cryptographic operations: + * - Fr.random() for secure random field elements + * - deriveKeys() and deriveSecretKeyFromSigningKey() for key derivation + * - Address computation with SchnorrAccountContract + * + * All encryption uses a non-extractable CryptoKey — the raw password + * is never stored or passed around after initial derivation. (#2) + */ + +import { Fr, GrumpkinScalar } from "@aztec-labs/aztec.js/fields"; + +import { instantiateAccount } from "../account-utils"; +import type { PublicAccountInfo } from "../shared-types"; +import { + type StoredAccount, + getStoredAccounts, + getStoredAccount, + saveAccount, + encryptWithKey, + decryptWithKey, + markAccountDeployed, + getActiveAccount, + setActiveAccount, +} from "./storage"; +import { log } from "../config"; + +/** + * Generates a new secret and salt for account creation using real Aztec primitives. + * Uses Fr.random() which is cryptographically secure via Barretenberg. + */ +export function generateSecret(): { secret: string; salt: string } { + log.debug("[wallet-manager] Generating new signing key and salt..."); + // The account's root is its signing key; the privacy secret is derived from it in instantiateAccount. + const secret = GrumpkinScalar.random().toString(); + const salt = Fr.random().toString(); + return { secret, salt }; +} + +/** + * Computes the account address from a secret and salt. + * Runs in the extension offscreen document where Barretenberg works. + */ +export async function computeAddress( + secretHex: string, + saltHex: string, +): Promise { + log.debug("[wallet-manager] Computing address..."); + const { instance } = await instantiateAccount(secretHex, saltHex); + log.debug("[wallet-manager] Computed address:", instance.address.toString()); + return instance.address.toString(); +} + +/** + * Creates a complete account: generates secret/salt, computes address, and stores encrypted. + * Takes a CryptoKey (not a password string) for encryption. (#2) + */ +// docs:start:create-new-account +export async function createAccount( + masterKey: CryptoKey, + alias: string = "", +): Promise<{ address: string; secret: string; salt: string }> { + log.debug("[wallet-manager] Creating account..."); + + const { secret, salt } = generateSecret(); + const address = await computeAddress(secret, salt); + await storeAccount(address, secret, salt, masterKey, alias); + + // Auto-set as active if this is the first account + const currentActive = await getActiveAccount(); + if (!currentActive) { + await setActiveAccount(address); + log.debug("[wallet-manager] Set as active account (first account)"); + } + + log.debug("[wallet-manager] Account created:", address); + return { address, secret, salt }; +} +// docs:end:create-new-account + +/** + * Stores an account with encrypted secret. + * Uses the master CryptoKey for AES-GCM encryption with a per-account random IV. + */ +export async function storeAccount( + address: string, + secret: string, + salt: string, + masterKey: CryptoKey, + alias: string = "", +): Promise { + log.debug("[wallet-manager] Storing account:", address); + + const { encrypted, iv } = await encryptWithKey(secret, masterKey); + + const storedAccount: StoredAccount = { + address, + encryptedSecret: encrypted, + iv, + alias, + isDeployed: false, + contractSalt: salt, + }; + + await saveAccount(storedAccount); + log.debug("[wallet-manager] Account stored successfully"); +} + +/** + * Gets the decrypted secret for an account. + * Takes a CryptoKey (not a password string). (#2) + */ +export async function getAccountSecret( + address: string, + masterKey: CryptoKey, +): Promise<{ secret: string; salt: string } | null> { + const stored = await getStoredAccount(address); + if (!stored) { + return null; + } + + const secret = await decryptWithKey( + stored.encryptedSecret, + stored.iv, + masterKey, + ); + + return { + secret, + salt: stored.contractSalt, + }; +} + +/** + * Gets all stored accounts (without secrets). + */ +export async function getAccounts(): Promise { + const accounts = await getStoredAccounts(); + return accounts.map((acc) => ({ + address: acc.address, + alias: acc.alias, + isDeployed: acc.isDeployed, + })); +} + +/** + * Marks an account as deployed. + */ +export async function markDeployed(address: string): Promise { + await markAccountDeployed(address); +} + +// Re-export storage functions for convenience +export { getActiveAccount, setActiveAccount }; diff --git a/docs/lines/v6/examples/webapp-tutorial/tests/e2e/embedded-wallet.test.ts b/docs/lines/v6/examples/webapp-tutorial/tests/e2e/embedded-wallet.test.ts new file mode 100644 index 00000000000..35578f1b07a --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/tests/e2e/embedded-wallet.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import puppeteer, { type Browser, type Page } from 'puppeteer'; +import { exec, type ChildProcess } from 'node:child_process'; +import { createServer } from 'node:http'; +import { readFileSync, existsSync } from 'node:fs'; +import { resolve, join, extname } from 'node:path'; + +const TUTORIAL_DIR = resolve(import.meta.dirname, '../..'); +const DIST_DIR = join(TUTORIAL_DIR, 'dist'); +const APP_PORT = 4173; +const APP_URL = `http://localhost:${APP_PORT}`; + +const MIME_TYPES: Record = { + '.html': 'text/html', + '.js': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.wasm': 'application/wasm', + '.svg': 'image/svg+xml', + '.png': 'image/png', +}; + +describe('Embedded wallet E2E', () => { + let browser: Browser; + let page: Page; + let server: ReturnType; + + beforeAll(async () => { + // Verify dist exists (must run `yarn build` first) + if (!existsSync(join(DIST_DIR, 'index.html'))) { + throw new Error( + 'dist/index.html not found. Run `yarn build` in the webapp-tutorial directory first.', + ); + } + + // Start a static HTTP server with COOP/COEP headers for SharedArrayBuffer + server = createServer((req, res) => { + const filePath = join(DIST_DIR, req.url === '/' ? 'index.html' : req.url!); + if (!existsSync(filePath)) { + // SPA fallback + const indexPath = join(DIST_DIR, 'index.html'); + const content = readFileSync(indexPath); + res.writeHead(200, { + 'Content-Type': 'text/html', + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }); + res.end(content); + return; + } + const ext = extname(filePath); + const contentType = MIME_TYPES[ext] || 'application/octet-stream'; + const content = readFileSync(filePath); + res.writeHead(200, { + 'Content-Type': contentType, + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }); + res.end(content); + }); + + await new Promise((resolve) => server.listen(APP_PORT, resolve)); + + browser = await puppeteer.launch({ + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-web-security', + ], + }); + + page = await browser.newPage(); + page.setDefaultTimeout(60_000); + }, 120_000); + + afterAll(async () => { + await browser?.close(); + server?.close(); + }); + + it('loads the app and shows wallet connect', async () => { + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + + // App should show the wallet connect section + const walletConnect = await page.waitForSelector('.wallet-connect'); + expect(walletConnect).toBeTruthy(); + + // Network picker defaults to local + const selectValue = await page.$eval('.network-picker select', (el) => + (el as HTMLSelectElement).value, + ); + expect(selectValue).toBe('local'); + }); + + it('shows test account selector for local network', async () => { + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + + // Local network should show the local connect section + const localConnect = await page.waitForSelector('.local-connect'); + expect(localConnect).toBeTruthy(); + + // Should have the test account dropdown + const selectOptions = await page.$$eval('.local-connect select option', (opts) => + opts.map((o) => (o as HTMLOptionElement).textContent), + ); + expect(selectOptions).toEqual(['Account 1', 'Account 2', 'Account 3']); + + // Connect button should be present + const button = await page.$eval('.local-connect button', (el) => el.textContent); + expect(button).toBe('Connect Test Account'); + }); + + it( + 'connects test account when sandbox is running', + async () => { + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + + // Click "Connect Test Account" + await page.click('.local-connect button'); + + // Wait for status to indicate PXE initialization + await page.waitForFunction( + () => document.querySelector('.status')?.textContent?.includes('Initializing PXE'), + { timeout: 10_000 }, + ); + + // Wait for connection (this requires a running sandbox) + try { + await page.waitForFunction( + () => document.querySelector('.status')?.textContent?.includes('Connected!'), + { timeout: 90_000 }, + ); + + // Should transition to lobby + const lobby = await page.waitForSelector('.game-lobby', { timeout: 5_000 }); + expect(lobby).toBeTruthy(); + } catch { + // If sandbox is not running, we expect an error status + const status = await page.$eval('.status', (el) => el.textContent); + console.log('Connection result (sandbox may not be running):', status); + expect(status).toMatch(/Error|Connected/); + } + }, + 120_000, + ); +}); diff --git a/docs/lines/v6/examples/webapp-tutorial/tests/e2e/extension-wallet.test.ts b/docs/lines/v6/examples/webapp-tutorial/tests/e2e/extension-wallet.test.ts new file mode 100644 index 00000000000..5c2406ef29a --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/tests/e2e/extension-wallet.test.ts @@ -0,0 +1,197 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import puppeteer, { type Browser, type Page } from 'puppeteer'; +import { createServer } from 'node:http'; +import { readFileSync, existsSync } from 'node:fs'; +import { resolve, join, extname } from 'node:path'; + +const TUTORIAL_DIR = resolve(import.meta.dirname, '../..'); +const DIST_DIR = join(TUTORIAL_DIR, 'dist'); +const EXTENSION_DIR = join(TUTORIAL_DIR, 'test-extension'); +const APP_PORT = 4174; // Different port from embedded-wallet tests +const APP_URL = `http://localhost:${APP_PORT}`; + +const MIME_TYPES: Record = { + '.html': 'text/html', + '.js': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.wasm': 'application/wasm', + '.svg': 'image/svg+xml', + '.png': 'image/png', +}; + +describe('Extension wallet E2E', () => { + let browser: Browser; + let page: Page; + let server: ReturnType; + + beforeAll(async () => { + if (!existsSync(join(DIST_DIR, 'index.html'))) { + throw new Error( + 'dist/index.html not found. Run `yarn build` in the webapp-tutorial directory first.', + ); + } + + if (!existsSync(join(EXTENSION_DIR, 'dist', 'background.js'))) { + throw new Error( + 'test-extension/dist/background.js not found. Run `yarn build:test-extension` first.', + ); + } + + // Start static server with COOP/COEP headers + server = createServer((req, res) => { + const filePath = join(DIST_DIR, req.url === '/' ? 'index.html' : req.url!); + if (!existsSync(filePath)) { + const indexPath = join(DIST_DIR, 'index.html'); + const content = readFileSync(indexPath); + res.writeHead(200, { + 'Content-Type': 'text/html', + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }); + res.end(content); + return; + } + const ext = extname(filePath); + const contentType = MIME_TYPES[ext] || 'application/octet-stream'; + const content = readFileSync(filePath); + res.writeHead(200, { + 'Content-Type': contentType, + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }); + res.end(content); + }); + + await new Promise((resolve) => server.listen(APP_PORT, resolve)); + + browser = await puppeteer.launch({ + headless: false, // Extensions require non-headless mode + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + `--disable-extensions-except=${EXTENSION_DIR}`, + `--load-extension=${EXTENSION_DIR}`, + ], + }); + + page = await browser.newPage(); + page.setDefaultTimeout(30_000); + }, 60_000); + + afterAll(async () => { + await browser?.close(); + server?.close(); + }); + + it('loads the app and can switch to remote mode', async () => { + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + + // Switch to remote network + await page.select('.network-picker select', 'remote'); + + // Should show the extension list area + const extensionList = await page.waitForSelector('.extension-list'); + expect(extensionList).toBeTruthy(); + }); + + it( + 'discovers the test extension wallet', + async () => { + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + + // Switch to remote + await page.select('.network-picker select', 'remote'); + + // Wait for the test extension to be discovered + // The test extension auto-approves, so a connect button should appear + const walletButton = await page.waitForFunction( + () => { + const buttons = document.querySelectorAll('.extension-list button'); + return buttons.length > 0 ? buttons[0] : null; + }, + { timeout: 15_000 }, + ); + expect(walletButton).toBeTruthy(); + + // Button text should contain the wallet name + const buttonText = await page.$eval('.extension-list button', (el) => el.textContent); + expect(buttonText).toContain('Aztec Test Wallet'); + }, + 30_000, + ); + + it( + 'shows emoji verification after clicking connect', + async () => { + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + await page.select('.network-picker select', 'remote'); + + // Wait for wallet to be discovered + await page.waitForFunction( + () => document.querySelectorAll('.extension-list button').length > 0, + { timeout: 15_000 }, + ); + + // Click the wallet connect button + await page.click('.extension-list button'); + + // Wait for emoji verification to appear + const emojiGrid = await page.waitForSelector('.emoji-grid', { timeout: 10_000 }); + expect(emojiGrid).toBeTruthy(); + + // Emoji grid should contain emojis + const emojiText = await page.$eval('.emoji-grid', (el) => el.textContent); + expect(emojiText!.length).toBeGreaterThan(0); + + // Verify Connection heading should be visible + const heading = await page.$eval('.emoji-verification h3', (el) => el.textContent); + expect(heading).toBe('Verify Connection'); + }, + 30_000, + ); + + it( + 'confirms connection after emoji verification', + async () => { + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + await page.select('.network-picker select', 'remote'); + + // Wait for wallet discovery + await page.waitForFunction( + () => document.querySelectorAll('.extension-list button').length > 0, + { timeout: 15_000 }, + ); + + // Connect to wallet + await page.click('.extension-list button'); + + // Wait for emoji verification + await page.waitForSelector('.emoji-verification', { timeout: 10_000 }); + + // Click confirm button + await page.click('.emoji-verification button'); + + // Wait for connection confirmation or transition + try { + await page.waitForFunction( + () => { + const status = document.querySelector('.status')?.textContent; + return status?.includes('Connected') || status?.includes('Error'); + }, + { timeout: 15_000 }, + ); + + const status = await page.$eval('.status', (el) => el.textContent); + // The connection may succeed or fail depending on whether the full + // wallet protocol flow completes (the test extension only handles + // getRegisteredAccounts). Either outcome validates the discovery + + // key exchange + emoji verification flow works. + expect(status).toMatch(/Connected|Error|Confirming/); + } catch { + // Timeout is acceptable — the flow up to emoji verification is validated + } + }, + 30_000, + ); +}); diff --git a/docs/lines/v6/examples/webapp-tutorial/tests/unit/crypto.test.ts b/docs/lines/v6/examples/webapp-tutorial/tests/unit/crypto.test.ts new file mode 100644 index 00000000000..2e8d37895ab --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/tests/unit/crypto.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect } from 'vitest'; +import { + generateKeyPair, + exportPublicKey, + importPublicKey, + deriveSessionKeys, + encrypt, + decrypt, + hashToEmoji, + DEFAULT_EMOJI_GRID_SIZE, +} from '@aztec-labs/wallet-sdk/crypto'; + +describe('crypto', () => { + describe('ECDH key pair generation + export/import roundtrip', () => { + it('generates a key pair with public and private keys', async () => { + const keyPair = await generateKeyPair(); + expect(keyPair.publicKey).toBeInstanceOf(CryptoKey); + expect(keyPair.privateKey).toBeInstanceOf(CryptoKey); + }); + + it('exports and re-imports a public key', async () => { + const keyPair = await generateKeyPair(); + const exported = await exportPublicKey(keyPair.publicKey); + + expect(exported.kty).toBe('EC'); + expect(exported.crv).toBe('P-256'); + expect(typeof exported.x).toBe('string'); + expect(typeof exported.y).toBe('string'); + + const imported = await importPublicKey(exported); + expect(imported).toBeInstanceOf(CryptoKey); + + // Re-export should produce same JWK + const reExported = await exportPublicKey(imported); + expect(reExported).toEqual(exported); + }); + }); + + describe('deriveSessionKeys — matching verificationHash', () => { + it('both parties derive matching verificationHash', async () => { + const appKeyPair = await generateKeyPair(); + const walletKeyPair = await generateKeyPair(); + + const appSession = await deriveSessionKeys( + appKeyPair, + walletKeyPair.publicKey, + true, // isApp + ); + const walletSession = await deriveSessionKeys( + walletKeyPair, + appKeyPair.publicKey, + false, // isApp (wallet side) + ); + + expect(appSession.verificationHash).toBe(walletSession.verificationHash); + expect(typeof appSession.verificationHash).toBe('string'); + expect(appSession.verificationHash.length).toBeGreaterThan(0); + }); + + it('different key pairs produce different verificationHashes', async () => { + const a = await generateKeyPair(); + const b = await generateKeyPair(); + const c = await generateKeyPair(); + + const sessionAB = await deriveSessionKeys(a, b.publicKey, true); + const sessionAC = await deriveSessionKeys(a, c.publicKey, true); + + expect(sessionAB.verificationHash).not.toBe(sessionAC.verificationHash); + }); + }); + + describe('AES-GCM encrypt/decrypt roundtrip', () => { + it('encrypts and decrypts data correctly', async () => { + const appKeyPair = await generateKeyPair(); + const walletKeyPair = await generateKeyPair(); + + const appSession = await deriveSessionKeys( + appKeyPair, + walletKeyPair.publicKey, + true, + ); + const walletSession = await deriveSessionKeys( + walletKeyPair, + appKeyPair.publicKey, + false, + ); + + const message = { hello: 'world', count: 42 }; + const encrypted = await encrypt(appSession.encryptionKey, JSON.stringify(message)); + + expect(typeof encrypted.iv).toBe('string'); + expect(typeof encrypted.ciphertext).toBe('string'); + + // Decrypt with the other party's key + const decrypted = await decrypt(walletSession.encryptionKey, encrypted); + expect(decrypted).toEqual(message); + }); + + it('decrypt with wrong key throws', async () => { + const appKeyPair = await generateKeyPair(); + const walletKeyPair = await generateKeyPair(); + const wrongKeyPair = await generateKeyPair(); + + const appSession = await deriveSessionKeys( + appKeyPair, + walletKeyPair.publicKey, + true, + ); + // Derive keys with wrong peer + const wrongSession = await deriveSessionKeys( + wrongKeyPair, + appKeyPair.publicKey, + false, + ); + + const encrypted = await encrypt(appSession.encryptionKey, JSON.stringify({ secret: true })); + + await expect(decrypt(wrongSession.encryptionKey, encrypted)).rejects.toThrow(); + }); + }); + + describe('hashToEmoji', () => { + it('returns a string of emojis with default count', () => { + const hash = 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789'; + const emojis = hashToEmoji(hash); + // Default count is 9 emojis + expect(emojis.length).toBeGreaterThan(0); + }); + + it('is deterministic — same hash produces same emojis', () => { + const hash = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const first = hashToEmoji(hash); + const second = hashToEmoji(hash); + expect(first).toBe(second); + }); + + it('different hashes produce different emojis', () => { + const hash1 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + const hash2 = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + const emojis1 = hashToEmoji(hash1); + const emojis2 = hashToEmoji(hash2); + expect(emojis1).not.toBe(emojis2); + }); + + it('respects custom count parameter', () => { + const hash = 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789'; + const emojis4 = hashToEmoji(hash, 4); + const emojis9 = hashToEmoji(hash, DEFAULT_EMOJI_GRID_SIZE); + // The 4-emoji version should be a prefix of the 9-emoji version + // (each emoji is picked from successive bytes) + expect(emojis4.length).toBeLessThan(emojis9.length); + }); + }); +}); diff --git a/docs/lines/v6/examples/webapp-tutorial/tests/unit/wallet-manager.test.ts b/docs/lines/v6/examples/webapp-tutorial/tests/unit/wallet-manager.test.ts new file mode 100644 index 00000000000..52d284e88d5 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/tests/unit/wallet-manager.test.ts @@ -0,0 +1,111 @@ +// @vitest-environment jsdom +import { describe, it, expect } from 'vitest'; +import { Fr } from '@aztec-labs/aztec.js/fields'; +import { WalletManager } from '@aztec-labs/wallet-sdk/manager'; + +describe('WalletManager', () => { + describe('configure', () => { + it('creates a WalletManager instance with extension config', () => { + const manager = WalletManager.configure({ + extensions: { enabled: true }, + }); + expect(manager).toBeDefined(); + expect(manager).toBeInstanceOf(WalletManager); + }); + + it('creates a WalletManager with allow list', () => { + const manager = WalletManager.configure({ + extensions: { + enabled: true, + allowList: ['extension-id-1', 'extension-id-2'], + }, + }); + expect(manager).toBeDefined(); + }); + + it('creates a WalletManager with block list', () => { + const manager = WalletManager.configure({ + extensions: { + enabled: true, + blockList: ['malicious-extension'], + }, + }); + expect(manager).toBeDefined(); + }); + + it('creates a WalletManager with extensions disabled', () => { + const manager = WalletManager.configure({ + extensions: { enabled: false }, + }); + expect(manager).toBeDefined(); + }); + }); + + describe('getAvailableWallets', () => { + it('returns a DiscoverySession with expected shape', () => { + const manager = WalletManager.configure({ + extensions: { enabled: true }, + }); + + const session = manager.getAvailableWallets({ + chainInfo: { + chainId: new Fr(31337), + version: new Fr(1), + }, + appId: 'test-app', + timeout: 100, + }); + + // Verify the discovery session has the expected API shape + expect(session).toBeDefined(); + expect(typeof session.cancel).toBe('function'); + expect(session.wallets).toBeDefined(); + expect(session.done).toBeInstanceOf(Promise); + + // Clean up + session.cancel(); + }); + + it('invokes onWalletDiscovered callback when provided', () => { + const manager = WalletManager.configure({ + extensions: { enabled: true }, + }); + + const discovered: unknown[] = []; + const session = manager.getAvailableWallets({ + chainInfo: { + chainId: new Fr(31337), + version: new Fr(1), + }, + appId: 'test-app', + timeout: 100, + onWalletDiscovered: (provider) => { + discovered.push(provider); + }, + }); + + // No wallets will be discovered in unit test (no extensions running), + // but the callback should be accepted without error + expect(discovered).toHaveLength(0); + session.cancel(); + }); + + it('cancel stops discovery without error', () => { + const manager = WalletManager.configure({ + extensions: { enabled: true }, + }); + + const session = manager.getAvailableWallets({ + chainInfo: { + chainId: new Fr(31337), + version: new Fr(1), + }, + appId: 'test-app', + timeout: 5000, + }); + + // Should not throw + expect(() => session.cancel()).not.toThrow(); + }); + }); +}); diff --git a/docs/lines/v6/examples/webapp-tutorial/tsconfig.app.json b/docs/lines/v6/examples/webapp-tutorial/tsconfig.app.json new file mode 100644 index 00000000000..61c76eb32bb --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/tsconfig.app.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true + }, + "include": ["src"] +} diff --git a/docs/lines/v6/examples/webapp-tutorial/tsconfig.json b/docs/lines/v6/examples/webapp-tutorial/tsconfig.json new file mode 100644 index 00000000000..82a8007eb97 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }] +} diff --git a/docs/lines/v6/examples/webapp-tutorial/vite.config.ts b/docs/lines/v6/examples/webapp-tutorial/vite.config.ts new file mode 100644 index 00000000000..b46be4265b2 --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/vite.config.ts @@ -0,0 +1,67 @@ +// docs:start:vite-config +import { defineConfig, loadEnv, type Plugin } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import { + type PolyfillOptions, + nodePolyfills, +} from "vite-plugin-node-polyfills"; + +// Unfortunate, but needed due to https://github.com/davidmyersdev/vite-plugin-node-polyfills/issues/81 +const nodePolyfillsFix = (options?: PolyfillOptions): Plugin => ({ + ...nodePolyfills(options), + resolveId(source: string) { + const m = + /^vite-plugin-node-polyfills\/shims\/(buffer|global|process)$/.exec( + source, + ); + if (m) { + return `./node_modules/vite-plugin-node-polyfills/shims/${m[1]}/dist/index.cjs`; + } + }, +}); + +export default defineConfig(({ mode }) => ({ + // process.env is only a shim in the browser, so substitute the node URL from .env (or the shell) at build time. + define: { + "process.env.AZTEC_NODE_URL": JSON.stringify( + loadEnv(mode, process.cwd(), "").AZTEC_NODE_URL ?? "", + ), + }, + plugins: [ + react(), + nodePolyfillsFix({ + globals: { + process: true, + Buffer: true, + }, + }), + ], + server: { + // Headers required for SharedArrayBuffer (needed by bb WASM) + headers: { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", + }, + }, + // Exclude WASM-containing packages from pre-bundling + optimizeDeps: { + include: ["pino", "pino/browser"], + exclude: [ + "@aztec-foundation/noir-noirc_abi", + "@aztec-foundation/noir-acvm_js", + "@aztec-foundation/bb.js", + "@aztec-foundation/noir-noir_js", + ], + }, + build: { + // Aztec's prover (bb.js, about 5 MB) and circuit artifacts load lazily but exceed Vite's + // 500 kB default; warn only above them. + chunkSizeWarningLimit: 6000, + }, + resolve: { + // Keep linked @aztec packages under this app so plugin-injected shim imports + // resolve from the webapp tutorial's node_modules. + preserveSymlinks: true, + }, +})); +// docs:end:vite-config diff --git a/docs/lines/v6/examples/webapp-tutorial/vite.extension.config.ts b/docs/lines/v6/examples/webapp-tutorial/vite.extension.config.ts new file mode 100644 index 00000000000..220c0b0ad3e --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/vite.extension.config.ts @@ -0,0 +1,121 @@ +/** + * Vite configuration for building the wallet extension. + * Uses the same polyfills as the main webapp. + */ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; +import { nodePolyfills } from 'vite-plugin-node-polyfills'; +import { resolve } from 'path'; +import { readFileSync } from 'fs'; + +/** Read the installed @aztec-labs/aztec.js version so it can be injected at build time. */ +let aztecVersion = 'unknown'; +try { + aztecVersion = JSON.parse(readFileSync('./node_modules/@aztec-labs/aztec.js/package.json', 'utf8')).version; +} catch { + // node_modules may not exist yet during initial config loading +} + +/** + * Vite plugin: patch Barretenberg worker files with crossOriginIsolated = true. + * + * Chrome extensions have SharedArrayBuffer but crossOriginIsolated === false. + * bb.js checks crossOriginIsolated in workers to decide whether to create + * shared WebAssembly.Memory. Without this patch, workers create non-shared + * memory then crash when instantiating barretenberg-threads.wasm. + * + * We prepend the patch at build time so it runs before any worker code. + * This avoids runtime Worker constructor overrides (which would require blob + * URLs that violate the extension's Content Security Policy). + */ +function patchWorkersCrossOriginIsolated() { + const PATCH = `Object.defineProperty(globalThis,'crossOriginIsolated',{value:true,writable:false,configurable:true});\n`; + return { + name: 'patch-workers-cross-origin-isolated', + generateBundle(_: unknown, bundle: Record) { + for (const [fileName, chunk] of Object.entries(bundle)) { + if (fileName.includes('.worker') && chunk.type === 'chunk') { + chunk.code = PATCH + chunk.code; + } + // Also handle worker assets (Vite may emit workers as assets, not chunks) + if (fileName.includes('.worker') && chunk.type === 'asset' && typeof chunk.source === 'string') { + chunk.source = PATCH + chunk.source; + } + } + }, + }; +} + +export default defineConfig({ + // Extension entry scripts live in test-extension/dist/. + // Vite's modulepreload helper builds absolute URLs from `base`; + // without this the preload URLs resolve to /chunks/... (extension root) + // instead of /dist/chunks/... where the files actually live. + base: '/dist/', + plugins: [ + react(), + nodePolyfills({ + globals: { + process: true, + Buffer: true, + }, + // Don't polyfill these - they might interfere with extension APIs + exclude: ['chrome'], + }), + patchWorkersCrossOriginIsolated(), + ], + define: { + // Ensure chrome isn't replaced by any polyfill + 'globalThis.chrome': 'globalThis.chrome', + // Inject the @aztec/* package version so config.ts can use it at runtime + '__AZTEC_PACKAGES_VERSION__': JSON.stringify(aztecVersion), + }, + build: { + outDir: 'test-extension/dist', + emptyOutDir: true, // Clean stale chunks between builds (#16). Content-script is built AFTER Vite. + rollupOptions: { + input: { + background: resolve(__dirname, 'test-extension/src/background.ts'), + // content-script built separately with esbuild (requires IIFE format) + offscreen: resolve(__dirname, 'test-extension/src/offscreen/offscreen.ts'), + popup: resolve(__dirname, 'test-extension/src/popup/popup.tsx'), + }, + output: { + entryFileNames: '[name].js', + chunkFileNames: 'chunks/[name]-[hash].js', + // Ensure WASM files are placed in a predictable location + assetFileNames: (assetInfo) => { + if (assetInfo.name?.endsWith('.wasm') || assetInfo.name?.endsWith('.wasm.gz')) { + return 'wasm/[name][extname]'; + } + return 'assets/[name]-[hash][extname]'; + }, + format: 'es', + }, + }, + target: 'esnext', + minify: false, // Keep readable for debugging + sourcemap: true, + // Ensure WASM files are bundled/copied, not externalized + assetsInlineLimit: 0, // Don't inline any assets, including small WASM files + }, + // Exclude WASM-containing packages from pre-bundling + optimizeDeps: { + include: ['pino', 'pino/browser'], + exclude: [ + '@aztec-foundation/noir-noirc_abi', + '@aztec-foundation/noir-acvm_js', + '@aztec-foundation/bb.js', + '@aztec-foundation/noir-noir_js', + ], + }, + resolve: { + // Keep linked @aztec packages under this app so plugin-injected shim imports + // resolve from the webapp tutorial's node_modules. + preserveSymlinks: true, + alias: { + // Ensure crypto polyfill for browser + crypto: 'crypto-browserify', + }, + }, +}); diff --git a/docs/lines/v6/examples/webapp-tutorial/vitest.config.ts b/docs/lines/v6/examples/webapp-tutorial/vitest.config.ts new file mode 100644 index 00000000000..3a8ca3e1bee --- /dev/null +++ b/docs/lines/v6/examples/webapp-tutorial/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + }, + resolve: { + // Linked @aztec-labs packages resolve outside node_modules, so vitest would inline them and + // bind their `buffer` import to the npm polyfill while externalized deps use Node's Buffer. + preserveSymlinks: true, + }, +}); diff --git a/docs/lines/v6/sidebars-developer.js b/docs/lines/v6/sidebars-developer.js new file mode 100644 index 00000000000..27cb21f6f18 --- /dev/null +++ b/docs/lines/v6/sidebars-developer.js @@ -0,0 +1,55 @@ +// @ts-check +// Sidebar configuration for Build (developers) documentation + +/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ +const sidebars = { + sidebar: [ + { + type: "html", + value: 'Overview', + className: "sidebar-title", + }, + { + type: "doc", + id: "overview", + }, + { + type: "doc", + id: "getting_started_on_local_network", + }, + { + type: "doc", + id: "getting_started_on_testnet", + }, + { + type: "doc", + id: "ai_tooling", + }, + { + type: "doc", + id: "support", + label: "Support", + }, + { + type: "link", + label: "Aztec Starter Repository", + href: "https://github.com/AztecProtocol/aztec-starter", + }, + { + type: "html", + value: 'Docs', + className: "sidebar-title", + }, + { + type: "autogenerated", + dirName: "docs", + }, + { + type: "link", + label: "Get Inspired", + href: "https://github.com/AztecProtocol/horizon", + }, + ], +}; + +module.exports = sidebars; diff --git a/docs/lines/v6/sidebars-operate.js b/docs/lines/v6/sidebars-operate.js new file mode 100644 index 00000000000..8bde31ae871 --- /dev/null +++ b/docs/lines/v6/sidebars-operate.js @@ -0,0 +1,280 @@ +// @ts-check +// Sidebar configuration for the "Run a Node" docs (operator-facing). +// +// Reorg per deliverable-5 docs-layout proposal: +// - "Operate" navbar item renamed to "Run a Node" +// - The landing page (operators/index) is a role-picker +// - One unified sidebar (operatorsSidebar) is used everywhere under /operate. +// "Guided setup" lists all four roles as subsections so an operator can +// switch roles from any page in one click. +// - Prerequisites no longer live at the sidebar root; each role's first +// setup page covers them. + +const dividerHtml = { + type: "html", + value: '', +}; + +const overviewItem = { + type: "doc", + id: "operators/index", + label: "Overview", +}; + +const guidedSetupTitle = { + type: "html", + value: 'Guided setup', + className: "sidebar-title", +}; + +const advancedOperationsTitle = { + type: "html", + value: 'Advanced operations', + className: "sidebar-title", +}; + +const operatorToolingTitle = { + type: "html", + value: 'Operator tooling', + className: "sidebar-title", +}; + +const referenceTitle = { + type: "html", + value: 'Reference', + className: "sidebar-title", +}; + +const fullNodeCategory = { + type: "category", + label: "Full node", + collapsible: true, + collapsed: true, + link: { + type: "doc", + id: "operators/full-node/overview", + }, + items: [ + "operators/full-node/setup", + "operators/full-node/verification", + "operators/full-node/troubleshooting", + ], +}; + +const soloSequencerCategory = { + type: "category", + label: "Solo sequencer", + collapsible: true, + collapsed: true, + link: { + type: "doc", + id: "operators/solo-sequencer/index", + }, + items: [ + "operators/solo-sequencer/pre-flight", + "operators/solo-sequencer/install-toolchain", + "operators/solo-sequencer/generate-keystore", + "operators/solo-sequencer/fund-accounts", + "operators/solo-sequencer/configure-environment", + "operators/solo-sequencer/docker-compose", + "operators/solo-sequencer/start-node", + "operators/solo-sequencer/register-sequencer", + "operators/solo-sequencer/verify", + "operators/solo-sequencer/day-2-essentials", + "operators/solo-sequencer/dappnode", + ], +}; + +const stakingProviderCategory = { + type: "category", + label: "Staking provider", + collapsible: true, + collapsed: true, + link: { + type: "doc", + id: "operators/provider/index", + }, + items: [ + "operators/provider/pre-flight", + "operators/provider/install-toolchain", + "operators/provider/generate-keystore", + "operators/provider/fund-accounts", + "operators/provider/configure-environment", + "operators/provider/docker-compose", + "operators/provider/start-node", + "operators/provider/register-sequencer", + "operators/provider/verify", + "operators/provider/day-2-essentials", + "operators/provider/provider-operations", + ], +}; + +const proverCategory = { + type: "category", + label: "Prover", + collapsible: true, + collapsed: true, + link: { + type: "doc", + id: "operators/prover/overview", + }, + items: [ + "operators/prover/setup", + "operators/prover/verification", + "operators/prover/claiming-rewards", + "operators/prover/troubleshooting", + ], +}; + +const essentialsCategory = { + type: "category", + label: "Essentials", + collapsible: true, + collapsed: true, + items: [ + "operators/concepts/identity-model", + "operators/concepts/hardware", + "operators/concepts/l1-rpc", + "operators/concepts/claiming-rewards", + "operators/concepts/slashing", + "operators/concepts/monitoring", + "operators/concepts/sequencer-troubleshooting", + ], +}; + +const topologyCategory = { + type: "category", + label: "Topology and infrastructure", + collapsible: true, + collapsed: true, + items: [ + "operators/setup/high_availability_sequencers", + "operators/setup/bootnode_operation", + "operators/setup/syncing_best_practices", + "operators/setup/building_from_source", + ], +}; + +const monitoringCategory = { + type: "category", + label: "Monitoring", + collapsible: true, + collapsed: true, + link: { + type: "doc", + id: "operators/monitoring/monitoring", + }, + items: [ + { + type: "category", + label: "Setup guides", + items: [ + "operators/monitoring/otel-setup", + "operators/monitoring/prometheus-setup", + "operators/monitoring/grafana-setup", + ], + }, + "operators/monitoring/metrics-reference", + "operators/monitoring/troubleshooting", + ], +}; + +const keystoreCategory = { + type: "category", + label: "Keystore management", + collapsible: true, + collapsed: true, + link: { + type: "doc", + id: "operators/keystore/advanced_keystore_guide", + }, + items: [ + "operators/keystore/creating_keystores", + "operators/keystore/storage-methods", + "operators/keystore/advanced-patterns", + "operators/keystore/examples", + "operators/keystore/troubleshooting", + ], +}; + +const sequencerManagementCategory = { + type: "category", + label: "Sequencer management", + collapsible: true, + collapsed: true, + link: { + type: "doc", + id: "operators/sequencer-management/sequencer_management_overview", + }, + items: [ + "operators/sequencer-management/creating_and_voting_on_proposals", + "operators/sequencer-management/slashing_and_offenses", + "operators/sequencer-management/claiming-rewards", + "operators/sequencer-management/exiting-a-sequencer", + "operators/sequencer-management/useful-commands", + ], +}; + +const faqItem = { + type: "doc", + id: "operators/operator-faq", +}; + +const operatorToolingItem = { + type: "doc", + id: "operators/tooling/index", + label: "Foundation and community tools", +}; + +const referenceItems = [ + { + type: "category", + label: "Changelog", + link: { + type: "doc", + id: "operators/reference/changelog/changelog", + }, + items: [ + "operators/reference/changelog/v6", + "operators/reference/changelog/v5.2", + "operators/reference/changelog/v4.3", + "operators/reference/changelog/v4.2", + "operators/reference/changelog/v4", + "operators/reference/changelog/v2.0.2", + ], + }, + "operators/reference/cli-reference", + "operators/reference/node_api_reference", + "operators/reference/ethereum_rpc_reference", + "operators/reference/reading_logs", + "operators/reference/glossary", +]; + +/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ +const sidebars = { + operatorsSidebar: [ + overviewItem, + dividerHtml, + guidedSetupTitle, + fullNodeCategory, + soloSequencerCategory, + stakingProviderCategory, + proverCategory, + essentialsCategory, + dividerHtml, + advancedOperationsTitle, + topologyCategory, + monitoringCategory, + keystoreCategory, + sequencerManagementCategory, + faqItem, + dividerHtml, + operatorToolingTitle, + operatorToolingItem, + dividerHtml, + referenceTitle, + ...referenceItems, + ], +}; + +module.exports = sidebars; From ea41903e15364b2d4144ffebd69b57e981f385a4 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 16:42:08 +0000 Subject: [PATCH 14/32] docs: point the v6 line at its own examples and l1-artifacts A mechanical rewrite of the line's pages only: #include_code paths and references: entries under docs/examples/ now name docs/lines/v6/examples/, and l1-artifacts snippets name the line's provisioned copy in docs/lines/v6/.aztec-node/l1-artifacts/. Every changed line differs from main's only by those prefixes. Prose links into aztec-node and reader setup instructions are unchanged. --- .../how_to_connect_to_local_network.md | 10 ++-- .../docs/aztec-js/how_to_create_account.md | 12 ++--- .../docs/aztec-js/how_to_deploy_contract.md | 28 +++++----- .../docs/aztec-js/how_to_pay_fees.md | 26 ++++----- .../docs/aztec-js/how_to_read_data.md | 18 +++---- .../docs/aztec-js/how_to_send_transaction.md | 14 ++--- .../how_to_simulate_without_signing.md | 10 ++-- .../docs/aztec-js/how_to_test.md | 16 +++--- .../docs/aztec-js/how_to_use_authwit.md | 10 ++-- .../contract_structure.md | 2 +- .../framework-description/custom_notes.md | 6 +-- .../framework-description/data_packing.md | 22 ++++---- .../ethereum_aztec_messaging.md | 6 +-- .../functions/how_to_define_functions.md | 12 ++--- .../v6/docs-developers/docs/aztec-nr/index.md | 6 +-- .../docs-developers/docs/aztec-nr/logging.md | 14 ++--- .../docs/foundational-topics/call_types.md | 2 +- .../data_structures.md | 8 +-- .../ethereum-aztec-messaging/inbox.md | 2 +- .../ethereum-aztec-messaging/outbox.md | 6 +-- .../ethereum-aztec-messaging/registry.md | 6 +-- .../contract_tutorials/counter_contract.md | 14 ++--- .../recursive_verification.md | 14 ++--- .../contract_tutorials/token_contract.md | 40 +++++++------- .../tutorials/js_tutorials/aave_bridge.md | 54 +++++++++---------- .../js_tutorials/aztecjs-getting-started.md | 16 +++--- .../tutorials/js_tutorials/token_bridge.md | 54 +++++++++---------- .../tutorials/js_tutorials/uniswap_swap.md | 54 +++++++++---------- .../wallet-extension/01-architecture.md | 10 ++-- .../wallet-extension/02-wallet-protocol.md | 10 ++-- .../wallet-extension/03-pxe-integration.md | 12 ++--- .../wallet-extension/04-accounts.md | 16 +++--- .../wallet-extension/05-transactions.md | 2 +- .../wallet-extension/06-approval-ui.md | 14 ++--- .../wallet-extension/07-testing.md | 2 +- .../js_tutorials/wallet-extension/index.md | 2 +- .../js_tutorials/webapp/01-the-contract.md | 28 +++++----- .../js_tutorials/webapp/02-project-setup.md | 4 +- .../webapp/03-network-and-wallet.md | 30 +++++------ .../webapp/04-contract-interaction.md | 14 ++--- .../webapp/05-transactions-and-fees.md | 10 ++-- .../webapp/06-putting-it-together.md | 12 ++--- .../tutorials/js_tutorials/webapp/index.md | 2 +- .../webapp/wallet-sdk/01-dapp-integration.md | 12 ++--- .../wallet-sdk/02-wallet-integration.md | 14 ++--- 45 files changed, 338 insertions(+), 338 deletions(-) diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_connect_to_local_network.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_connect_to_local_network.md index 4bee18dfe57..30a4f3eb126 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_connect_to_local_network.md +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_connect_to_local_network.md @@ -3,7 +3,7 @@ title: Connect to Local Network tags: [local_network, connection, wallet] sidebar_position: 1 description: Connect your application to the Aztec local network and interact with accounts. -references: ["docs/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/wallets/src/embedded/*", "labs/yarn-project/accounts/src/testing/*"] +references: ["docs/lines/v6/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/wallets/src/embedded/*", "labs/yarn-project/accounts/src/testing/*"] --- This guide shows you how to connect your application to the Aztec local network and interact with the network. @@ -24,7 +24,7 @@ yarn add @aztec-labs/aztec.js@#include_version_without_prefix @aztec-labs/accoun Create a node client and EmbeddedWallet to interact with the local network: -#include_code connect_to_network /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code connect_to_network /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript :::note About EmbeddedWallet `EmbeddedWallet` is a simplified wallet for local development that implements the same `Wallet` interface used in production. It handles key management, transaction signing, and proof generation in-process without external dependencies. @@ -38,13 +38,13 @@ Create a node client and EmbeddedWallet to interact with the local network: Get node information to confirm your connection: -#include_code verify_connection /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code verify_connection /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript ### Load pre-funded accounts The local network has accounts pre-funded with fee juice to pay for gas. Register them in your wallet: -#include_code load_accounts /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code load_accounts /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript These accounts are pre-funded with fee juice (the native gas token) at genesis, so you can immediately send transactions without needing to bridge funds from L1. @@ -52,7 +52,7 @@ These accounts are pre-funded with fee juice (the native gas token) at genesis, Verify that an account has fee juice for transactions: -#include_code check_fee_juice /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code check_fee_juice /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript ## Next steps diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_create_account.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_create_account.md index b8aab5ee545..cb6bcee45d2 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_create_account.md +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_create_account.md @@ -3,7 +3,7 @@ title: Creating Accounts tags: [accounts] sidebar_position: 2 description: Step-by-step guide to creating and deploying new user accounts in Aztec.js applications. -references: ["docs/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/accounts/src/schnorr/*", "labs/yarn-project/wallets/src/embedded/*", "labs/yarn-project/aztec.js/src/fee/fee_juice_payment_method_with_claim.ts"] +references: ["docs/lines/v6/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/accounts/src/schnorr/*", "labs/yarn-project/wallets/src/embedded/*", "labs/yarn-project/aztec.js/src/fee/fee_juice_payment_method_with_claim.ts"] --- This guide shows you how to create and deploy a new account on Aztec. @@ -23,7 +23,7 @@ yarn add @aztec-labs/aztec.js@#include_version_without_prefix @aztec-labs/wallet Using the [`wallet` from the connection guide](./how_to_connect_to_local_network.md), call `createSchnorrAccount` to create a new account with a random secret, salt, and signing key: -#include_code create_account /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code create_account /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript The secret derives the account's encryption keys, the signing key authenticates its transactions, and the salt ensures address uniqueness. The signing key is provided independently and is not derived from the secret: it is an ownership key, so keep it separate from the encryption secret that your PXE holds. @@ -39,7 +39,7 @@ New accounts must be deployed before they can send transactions. Deployment requ If your account doesn't have Fee Juice, use the [Sponsored FPC](./how_to_pay_fees.md#sponsored-fpc): -#include_code deploy_account_sponsored_fpc /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code deploy_account_sponsored_fpc /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript :::info See the [guide on fees](./how_to_pay_fees.md#sponsored-fpc) for more details on the Sponsored FPC and what this snippet means. @@ -51,11 +51,11 @@ If your account has Fee Juice from a [bridge from L1](./how_to_pay_fees.md#bridg Create a new Schnorr account for this path: -#include_code create_fee_juice_account /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code create_fee_juice_account /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript Claim the bridged Fee Juice and deploy in one step: -#include_code bridge_fee_juice_claim /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code bridge_fee_juice_claim /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript If the account already has Fee Juice on L2 (for example, from a faucet or a previously claimed bridge), no special payment method is needed — just call `send({ from: NO_FROM })` and Fee Juice is used automatically. @@ -65,7 +65,7 @@ The `from: NO_FROM` signals that this transaction should be executed without acc Confirm the account was deployed successfully. Substitute the account variable for whichever path you used above (`newAccount` for the Sponsored FPC path, `feeJuiceAccount` for the Fee Juice path): -#include_code verify_account_deployment /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code verify_account_deployment /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript ## Next steps diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_deploy_contract.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_deploy_contract.md index 9e8baf6c151..aea73dc4d58 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_deploy_contract.md +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_deploy_contract.md @@ -3,7 +3,7 @@ title: Deploying Contracts tags: [contracts, deployment] sidebar_position: 3 description: Deploy smart contracts to Aztec using generated TypeScript classes. -references: ["docs/examples/ts/aztecjs_advanced/index.ts", "docs/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts", "labs/yarn-project/aztec.js/src/contract/deploy_method.ts", "labs/yarn-project/aztec.js/src/deployment/*"] +references: ["docs/lines/v6/examples/ts/aztecjs_advanced/index.ts", "docs/lines/v6/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts", "labs/yarn-project/aztec.js/src/contract/deploy_method.ts", "labs/yarn-project/aztec.js/src/deployment/*"] --- This guide shows you how to deploy compiled contracts to Aztec using the generated TypeScript interfaces. @@ -52,11 +52,11 @@ In the examples below, `wallet` refers to a `Wallet` instance that manages keys How you deploy depends on how you pay for it. When paying using an account's fee juice (like a test account on the local network): -#include_code deploy_basic_local /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code deploy_basic_local /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript On testnet, your account likely won't have Fee Juice. Instead, pay fees using the [Sponsored Fee Payment Contract method](./how_to_pay_fees.md): -#include_code deploy_sponsored_fpc_contract /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code deploy_sponsored_fpc_contract /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript Here's a complete example from the test suite: @@ -68,7 +68,7 @@ Here's a complete example from the test suite: By default, the deployment's salt is random, but you can specify it (for example, if you want to get a deterministic address): -#include_code deploy_custom_salt /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code deploy_custom_salt /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript ### Deploy universally @@ -84,7 +84,7 @@ Universal deployment excludes the sender from address computation, allowing the Deploy without running the constructor: -#include_code skip_initialization /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code skip_initialization /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript ### Deploy with a specific initializer @@ -108,7 +108,7 @@ This is useful for contracts that support multiple initialization patterns, such ### Get address before deployment -#include_code calculate_address_before_deploy /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code calculate_address_before_deploy /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript :::warning This is an advanced pattern. For most use cases, deploy the contract directly and get the address from the deployed instance. @@ -120,13 +120,13 @@ This is an advanced pattern. For most use cases, deploy the contract directly an Use `NO_WAIT` to get the transaction hash immediately and track deployment: -#include_code no_wait_deploy_imports /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code no_wait_deploy_imports /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript -#include_code no_wait_deploy /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code no_wait_deploy /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript For most use cases, simply await the deployment to get the contract directly: -#include_code deploy_contract /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code deploy_contract /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript ## Deploy multiple contracts @@ -140,11 +140,11 @@ Here's an example deploying a `TokenContract` with constructor arguments for adm When one contract depends on another, deploy them sequentially and pass the first contract's address: -#include_code deploy_with_dependencies /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code deploy_with_dependencies /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript ### Deploy contracts in parallel -#include_code parallel_deploy /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code parallel_deploy /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript :::tip[Parallel deployment considerations] Parallel deployment is faster, but transactions from the same account share a nonce sequence. The wallet handles nonce assignment automatically, but if one deployment fails, subsequent deployments may also fail due to nonce gaps. For reliable parallel deployments: @@ -200,7 +200,7 @@ If you call a public function on an unpublished contract, the transaction will f ### Verify contract is callable -#include_code verify_contract_callable /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code verify_contract_callable /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript ## Register deployed contracts @@ -208,12 +208,12 @@ If you call a public function on an unpublished contract, the transaction will f If a contract was deployed by another account: -#include_code register_external_contract /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code register_external_contract /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript :::warning You need the exact deployment parameters (salt, initialization hash, etc.) to correctly register an externally deployed contract. If you don't have access to the contract instance, you can reconstruct it: -#include_code reconstruct_contract_instance /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code reconstruct_contract_instance /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript ::: diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_pay_fees.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_pay_fees.md index 089e9e0f839..df7b4df89d6 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_pay_fees.md +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_pay_fees.md @@ -3,7 +3,7 @@ title: Paying Fees tags: [fees, transactions, accounts, mana, gas] sidebar_position: 7 description: Pay transaction fees on Aztec, understand mana costs, estimate gas, and retrieve fees from receipts. -references: ["docs/examples/ts/aztecjs_advanced/index.ts", "docs/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/end-to-end/src/single-node/fees/private_payments.parallel.test.ts", "labs/yarn-project/aztec.js/src/fee/*"] +references: ["docs/lines/v6/examples/ts/aztecjs_advanced/index.ts", "docs/lines/v6/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/end-to-end/src/single-node/fees/private_payments.parallel.test.ts", "labs/yarn-project/aztec.js/src/fee/*"] --- import { General, Fees } from '@site/src/components/Snippets/general_snippets'; @@ -41,7 +41,7 @@ When using `EmbeddedWallet`, gas is estimated automatically on every `send()` ca Before sending a transaction, you can read the mana it will consume by simulating with `includeMetadata: true` and reading `gasUsed` from the result: -#include_code estimate_mana /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code estimate_mana /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript The `gasUsed` object contains the raw gas the simulation consumed: @@ -54,7 +54,7 @@ It is up to you to derive the gas limits you declare from this raw usage (typica To calculate the expected fee from the padded gas, use the `computeFee` method with current network fees: -#include_code compute_fee_from_estimate /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code compute_fee_from_estimate /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript :::tip Pad the raw `gasUsed` yourself to leave a safety margin. Multiplying by `1.1` adds 10%; use higher padding for transactions with variable gas costs. The wallet rejects any declared limit above the network's per-tx admission limit. @@ -64,11 +64,11 @@ Pad the raw `gasUsed` yourself to leave a safety margin. Multiplying by `1.1` ad After a transaction is mined, you can retrieve the fee paid from the receipt: -#include_code get_fee_from_receipt /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code get_fee_from_receipt /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript The `transactionFee` field is a `bigint` representing the total fee paid in the fee token (Fee Juice). You can also check execution status: -#include_code check_receipt_status /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code check_receipt_status /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript ## Pay with Fee Juice @@ -76,7 +76,7 @@ Fee Juice is the native fee token on Aztec. If your account has Fee Juice (for example, from a faucet), is [deployed](./how_to_create_account.md), and is registered in your wallet, it will be used automatically to pay for the fee of the transaction: -#include_code pay_with_fee_juice /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code pay_with_fee_juice /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript ## Use Fee Payment Contracts @@ -98,7 +98,7 @@ The Sponsored FPC pays fees unconditionally, enabling free transactions. It is a You can derive the Sponsored FPC address from its deployment parameters, register it with your wallet, and use it to pay for transactions: -#include_code deploy_sponsored_fpc_contract /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code deploy_sponsored_fpc_contract /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript Here's a simpler example from the test suite: @@ -170,15 +170,15 @@ Fee Juice is non-transferable on L2, but you can bridge it from L1, claim it on `aztec.js` provides helpers to simplify the process: -#include_code bridge_fee_juice_setup /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code bridge_fee_juice_setup /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript Under the hood, `L1FeeJuicePortalManager` gets the L1 addresses from the node `aztec_getNodeInfo` endpoint. It then exposes an easy method `bridgeTokensPublic` which mints fee juice on L1 and sends it to an L2 address via the L1 portal: -#include_code bridge_fee_juice_execute /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code bridge_fee_juice_execute /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript After this transaction is minted on L1 and a few blocks pass, you can claim the message on L2 and use it directly to pay for fees: -#include_code bridge_fee_juice_claim /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code bridge_fee_juice_claim /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript ## Configure gas settings @@ -199,11 +199,11 @@ The fee limit is calculated as `gasLimits × maxFeesPerGas` for each dimension. Set custom gas limits by importing from `stdlib`: -#include_code custom_gas_settings /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code custom_gas_settings /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript Then pass the settings when sending: -#include_code send_with_gas_settings /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code send_with_gas_settings /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript Note that `gasLimits` and `teardownGasLimits` use `daGas`/`l2Gas` field names, while `maxFeesPerGas` and `maxPriorityFeesPerGas` use `feePerDaGas`/`feePerL2Gas`. @@ -213,7 +213,7 @@ Note that `gasLimits` and `teardownGasLimits` use `daGas`/`l2Gas` field names, w When using `EmbeddedWallet`, gas estimation happens automatically on every `send()`; you don't need to declare gas limits at all. Reading `gasUsed` from a `simulate({ includeMetadata: true })` result is useful when you want to preview costs before sending, or to set explicit limits with a custom wallet implementation. ::: -#include_code auto_gas_estimation /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code auto_gas_estimation /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript :::tip Gas estimation runs a simulation first to determine actual gas usage, then adds padding for safety. This works with all payment methods, including FPCs. diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_read_data.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_read_data.md index 2215fc0bad8..cd19fa1677a 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_read_data.md +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_read_data.md @@ -3,7 +3,7 @@ title: Reading Contract Data tags: [functions, simulation, events, logs] sidebar_position: 5 description: How to read data from contracts including simulating functions, reading logs, and retrieving events. -references: ["docs/examples/ts/aztecjs_connection/index.ts", "docs/examples/ts/aztecjs_advanced/index.ts", "labs/yarn-project/end-to-end/src/automine/effects/event_logs.test.ts", "labs/yarn-project/aztec.js/src/contract/contract_function_interaction.ts"] +references: ["docs/lines/v6/examples/ts/aztecjs_connection/index.ts", "docs/lines/v6/examples/ts/aztecjs_advanced/index.ts", "labs/yarn-project/end-to-end/src/automine/effects/event_logs.test.ts", "labs/yarn-project/aztec.js/src/contract/contract_function_interaction.ts"] --- This guide shows you how to read data from Aztec contracts in TypeScript, including simulating function calls, reading raw logs, and retrieving typed events. @@ -19,7 +19,7 @@ import { General } from '@site/src/components/Snippets/general_snippets'; The `simulate` method executes a contract function locally and returns its result. It works with private, public, and utility functions. No transaction is created and no gas is spent. -#include_code simulate_function /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code simulate_function /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript The `from` option specifies which account context to use for the simulation. This is required for all simulations. For private functions, it determines which account's private state is accessed. For public functions, it sets the `msg_sender` context. @@ -38,7 +38,7 @@ const { result: [value1, value2] } = await contract.methods Set `includeMetadata: true` to get additional information about the simulation: -#include_code simulate_with_metadata /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code simulate_with_metadata /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript The result includes `result` (the function return value), `stats` (execution statistics), `offchainEffects`, and `gasUsed` (the raw gas the simulation consumed, with `totalGas` and `teardownGas`). Derive your own gas limits from `gasUsed` if you want to declare them explicitly; otherwise the wallet fills in the network's per-tx admission limits. @@ -46,7 +46,7 @@ The result includes `result` (the function return value), `stats` (execution sta When simulating private functions, the caller must have access to any private state being read. The PXE only has visibility into notes belonging to registered accounts. -#include_code simulate_private_access /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code simulate_private_access /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript If the caller doesn't have access to another address's notes, the simulation will fail with an error. @@ -75,11 +75,11 @@ Contracts emit data in two forms you can read: Raw public logs are carried on each block's transaction effects. Fetch a block with `includeTransactions: true` and read `body.txEffects[*].publicLogs`: -#include_code read_public_logs /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code read_public_logs /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript You can scope this to a single transaction (by locating its block and matching its tx hash) or to a block range (by reading each block's tx effects): -#include_code read_logs_by_filter /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code read_logs_by_filter /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript ## Reading events @@ -89,7 +89,7 @@ Events provide typed access to contract emissions. The event metadata from your Use the `getPublicEvents` helper to retrieve typed public events: -#include_code import_get_public_events /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code import_get_public_events /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript #include_code get_public_events labs/yarn-project/end-to-end/src/automine/effects/event_logs.test.ts typescript @@ -109,7 +109,7 @@ Each returned event includes both the decoded `event` data and `metadata` (block Private events are stored in the PXE with privacy scoping. Use `wallet.getPrivateEvents()` to retrieve them: -#include_code import_private_event_types /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code import_private_event_types /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript The `BlockNumber` type is a branded type that wraps raw numbers for type safety. Use it when setting `fromBlock` and `toBlock` in filters. @@ -134,7 +134,7 @@ collectedEvents.forEach((ev) => { To continuously monitor for new events, poll at regular intervals while tracking the last processed block: -#include_code poll_for_events /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code poll_for_events /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript For private events, use the same pattern with `wallet.getPrivateEvents()` and update the `fromBlock` in your filter accordingly. diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_send_transaction.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_send_transaction.md index d02a680f97e..4a316a896c4 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_send_transaction.md +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_send_transaction.md @@ -3,7 +3,7 @@ title: Sending Transactions sidebar_position: 4 description: Send transactions to Aztec contracts using Aztec.js with various options and error handling tags: [transactions, contracts, aztec.js] -references: ["docs/examples/ts/aztecjs_advanced/index.ts", "labs/yarn-project/aztec.js/src/contract/*"] +references: ["docs/lines/v6/examples/ts/aztecjs_advanced/index.ts", "labs/yarn-project/aztec.js/src/contract/*"] --- This guide shows you how to send transactions to smart contracts on Aztec. @@ -24,11 +24,11 @@ import { General } from '@site/src/components/Snippets/general_snippets'; After connecting to a contract: -#include_code connect_to_contract /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code connect_to_contract /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript Call a function and wait for it to be mined: -#include_code basic_send_transaction /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code basic_send_transaction /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript The `from` field specifies which account sends the transaction. If that account has Fee Juice, it pays for the transaction automatically. For other fee payment options, see [paying fees](./how_to_pay_fees.md). @@ -42,7 +42,7 @@ When using `EmbeddedWallet`, calling `send()` triggers a **simulation** step bef This means a simple `.send()` is all most apps need. You can adjust the gas padding if desired: -#include_code set_gas_padding /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code set_gas_padding /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript :::note Public authwits still need to be set explicitly before the transaction, as they require a separate onchain transaction. See [Using Authentication Witnesses](./how_to_use_authwit.md) for details. @@ -52,13 +52,13 @@ Public authwits still need to be set explicitly before the transaction, as they Use the `NO_WAIT` option to get the transaction hash immediately without waiting for inclusion: -#include_code no_wait_transaction /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code no_wait_transaction /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript ## Send batch transactions Execute multiple calls atomically using `BatchCall`: -#include_code batch_call /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code batch_call /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript :::warning All calls in a batch must succeed or the entire batch reverts. Use batch transactions when you need atomic execution of multiple operations. @@ -68,7 +68,7 @@ All calls in a batch must succeed or the entire batch reverts. Use batch transac After sending a transaction without waiting, you can query its receipt using the node: -#include_code query_tx_status /docs/examples/ts/aztecjs_advanced/index.ts typescript +#include_code query_tx_status /docs/lines/v6/examples/ts/aztecjs_advanced/index.ts typescript `getTxReceipt` always resolves to one of three lifecycle variants of the `TxReceipt` union, depending on where the transaction is in its lifecycle: diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md index 41b8d1c3dec..26918ddb67b 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md @@ -3,7 +3,7 @@ title: Simulate without signing prompts tags: [simulation, authwit, wallet] sidebar_position: 6 description: How to call .simulate() on a view function or estimate gas without prompting the user to sign authentication witnesses. -references: ["docs/examples/ts/aztecjs_kernelless_simulation/index.ts", "labs/yarn-project/aztec.js/src/contract/interaction_options.ts", "labs/yarn-project/aztec.js/src/authorization/call_authorization_request.ts", "labs/yarn-project/wallets/src/embedded/embedded_wallet.ts"] +references: ["docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/index.ts", "labs/yarn-project/aztec.js/src/contract/interaction_options.ts", "labs/yarn-project/aztec.js/src/authorization/call_authorization_request.ts", "labs/yarn-project/wallets/src/embedded/embedded_wallet.ts"] --- You want to call `.simulate()` from an app and not have the user's wallet pop up a signing prompt. This page covers the symptoms that lead to that prompt, why the obvious workarounds do not work, and the right fix. @@ -44,7 +44,7 @@ For a normal `.simulate()` you do not need to pass overrides yourself. The defau - For a transaction whose real fee path uses a fee payment contract (FPC) with private side effects (the FPC emits notes during fee payment), include that FPC in the simulation's fee options so gas estimation accounts for the FPC's side effects. Kernelless still applies; the gas number stays accurate. - If you have a stale call site that uses `from: AztecAddress.ZERO` plus a no-op fee payment method as a workaround, replace it with a real `from` (or `NO_FROM`) and drop the no-op fee contract. -#include_code simulate-view-without-signing /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript +#include_code simulate-view-without-signing /docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/index.ts typescript If you genuinely need to construct your own `SimulationOverrides` (for example, to combine a contract-instance swap with a `fastForwardContractUpdate` for upgrade testing), you can pass them through `.simulate()`: @@ -85,15 +85,15 @@ The example below uses the canonical contract-mediated pattern: Alice calls `Cro Run the simulation and filter the offchain effects by the `CallAuthorizationRequest` selector: -#include_code simulate-and-collect-effects /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript +#include_code simulate-and-collect-effects /docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/index.ts typescript Decode each effect into a `CallAuthorizationRequest`. The `innerHash` field is the piece the authorizing account needs to sign: -#include_code decode-call-authorization /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript +#include_code decode-call-authorization /docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/index.ts typescript Build a real authentication witness from each inner hash and send the transaction with the collected witnesses attached: -#include_code build-authwits-and-send /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript +#include_code build-authwits-and-send /docs/lines/v6/examples/ts/aztecjs_kernelless_simulation/index.ts typescript The app does not need to know which calls require authwits ahead of time. The simulation discovers them; the wallet signs them at send time. diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_test.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_test.md index 5f90a5240af..857b06c035d 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_test.md +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_test.md @@ -3,7 +3,7 @@ title: Testing Smart Contracts tags: [contracts, tests] sidebar_position: 8 description: Learn how to write and run tests for your Aztec smart contracts using Aztec.js and a local network. -references: ["docs/examples/ts/aztecjs_testing/index.ts", "docs/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/wallets/src/embedded/*", "labs/yarn-project/accounts/src/testing/*"] +references: ["docs/lines/v6/examples/ts/aztecjs_testing/index.ts", "docs/lines/v6/examples/ts/aztecjs_connection/index.ts", "labs/yarn-project/wallets/src/embedded/*", "labs/yarn-project/accounts/src/testing/*"] --- This guide covers how to test Aztec smart contracts by connecting to a local network, deploying contracts, and verifying their behavior. @@ -18,7 +18,7 @@ This guide covers how to test Aztec smart contracts by connecting to a local net Connect to your local Aztec network and create an embedded wallet: -#include_code connect_to_network /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code connect_to_network /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript The `EmbeddedWallet` manages accounts, tracks deployed contracts, and handles transaction proving. It connects to the Aztec node which provides access to both the Private eXecution Environment (PXE) and the network. @@ -26,19 +26,19 @@ The `EmbeddedWallet` manages accounts, tracks deployed contracts, and handles tr The local network comes with pre-funded accounts. Load them into your wallet: -#include_code load_test_accounts /docs/examples/ts/aztecjs_testing/index.ts typescript +#include_code load_test_accounts /docs/lines/v6/examples/ts/aztecjs_testing/index.ts typescript ## Deploying contracts in tests Deploy contracts using the generated contract class: -#include_code deploy_test_contract /docs/examples/ts/aztecjs_testing/index.ts typescript +#include_code deploy_test_contract /docs/lines/v6/examples/ts/aztecjs_testing/index.ts typescript ## Verifying contract state Use `.simulate()` to read contract state without creating a transaction: -#include_code simulate_function /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code simulate_function /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript Simulations are free (no gas cost) and return the function's result directly. Use them for: @@ -50,7 +50,7 @@ Simulations are free (no gas cost) and return the function's result directly. Us Send transactions and wait for confirmation: -#include_code send_transaction /docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code send_transaction /docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript The `send()` method returns when the transaction is included in a block. @@ -58,13 +58,13 @@ The `send()` method returns when the transaction is included in a block. Here's a complete test example showing the typical structure with setup, test cases, and assertions: -#include_code complete_test_example /docs/examples/ts/aztecjs_testing/index.ts typescript +#include_code complete_test_example /docs/lines/v6/examples/ts/aztecjs_testing/index.ts typescript ## Testing failure cases Test that invalid operations revert as expected: -#include_code test_revert_case /docs/examples/ts/aztecjs_testing/index.ts typescript +#include_code test_revert_case /docs/lines/v6/examples/ts/aztecjs_testing/index.ts typescript Use `.simulate()` to test reverts without spending gas. The simulation will throw if the transaction would fail onchain. diff --git a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_authwit.md b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_authwit.md index 480b16304c8..6f5057f8d1f 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_authwit.md +++ b/docs/lines/v6/docs-developers/docs/aztec-js/how_to_use_authwit.md @@ -3,7 +3,7 @@ title: Using Authentication Witnesses tags: [accounts, authwit] sidebar_position: 6 description: Step-by-step guide to implementing authentication witnesses in Aztec.js for delegated transactions. -references: ["docs/examples/ts/aztecjs_authwit/index.ts", "labs/yarn-project/aztec.js/src/utils/authwit.ts", "labs/yarn-project/aztec.js/src/wallet/*"] +references: ["docs/lines/v6/examples/ts/aztecjs_authwit/index.ts", "labs/yarn-project/aztec.js/src/utils/authwit.ts", "labs/yarn-project/aztec.js/src/wallet/*"] --- This guide shows you how to create and use authentication witnesses (authwits) to authorize other accounts to perform actions on your behalf. @@ -50,7 +50,7 @@ Private authwits authorize actions in the private domain. The authorization is i Let's say Alice wants to allow Bob to transfer tokens from her account. Alice is the **authorizer** (she owns the tokens) and Bob is the **caller** (he will execute the transfer): -#include_code private_authwit /docs/examples/ts/aztecjs_authwit/index.ts typescript +#include_code private_authwit /docs/lines/v6/examples/ts/aztecjs_authwit/index.ts typescript :::tip The nonce prevents replay attacks. When `from` and `msg_sender` are the same (self-transfer), set the nonce to `0`. @@ -60,13 +60,13 @@ The nonce prevents replay attacks. When `from` and `msg_sender` are the same (se Public authwits require a transaction to store the authorization in the `AuthRegistry` contract before the authorized action can be executed: -#include_code public_authwit /docs/examples/ts/aztecjs_authwit/index.ts typescript +#include_code public_authwit /docs/lines/v6/examples/ts/aztecjs_authwit/index.ts typescript ## Create arbitrary message authwits Use this when authorizing arbitrary data rather than a specific contract function call: -#include_code arbitrary_authwit /docs/examples/ts/aztecjs_authwit/index.ts typescript +#include_code arbitrary_authwit /docs/lines/v6/examples/ts/aztecjs_authwit/index.ts typescript The `consumer` is the contract address that will verify this authwit. @@ -74,7 +74,7 @@ The `consumer` is the contract address that will verify this authwit. Public authwits can be revoked by setting `authorized` to `false`: -#include_code revoke_authwit /docs/examples/ts/aztecjs_authwit/index.ts typescript +#include_code revoke_authwit /docs/lines/v6/examples/ts/aztecjs_authwit/index.ts typescript ## Next steps diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_structure.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_structure.md index 5b118285d56..4c9c5984dc0 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_structure.md +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/contract_structure.md @@ -3,7 +3,7 @@ title: Contract Structure sidebar_position: 1 tags: [contracts] description: Learn the fundamental structure of Aztec smart contracts including the contract keyword, directory layout, and how contracts manage state and functions. -references: ["docs/examples/contracts/counter_contract/src/main.nr"] +references: ["docs/lines/v6/examples/contracts/counter_contract/src/main.nr"] --- High-level structure of how Aztec smart contracts including the different components. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/custom_notes.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/custom_notes.md index e321df53e55..43b3b6b1176 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/custom_notes.md +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/custom_notes.md @@ -52,7 +52,7 @@ address_note = { git="https://github.com/aztec-labs-eng/aztec-nr", tag="#include Define your custom note with the `#[note]` macro: -#include_code nft_note_struct /docs/examples/contracts/nft/src/nft.nr rust +#include_code nft_note_struct /docs/lines/v6/examples/contracts/nft/src/nft.nr rust The `#[note]` macro generates the following for your struct: @@ -99,13 +99,13 @@ struct Storage { ### Inserting notes -#include_code mint /docs/examples/contracts/nft/src/main.nr rust +#include_code mint /docs/lines/v6/examples/contracts/nft/src/main.nr rust ### Reading and removing notes Use `pop_notes` to read and nullify notes atomically. This is the recommended pattern for most use cases: -#include_code burn /docs/examples/contracts/nft/src/main.nr rust +#include_code burn /docs/lines/v6/examples/contracts/nft/src/main.nr rust :::warning There's also a `get_notes` function that reads without nullifying, but use it with caution - the returned notes may have already been spent in another transaction. diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/data_packing.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/data_packing.md index b1866036d5d..b2aca60fe49 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/data_packing.md +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/data_packing.md @@ -3,7 +3,7 @@ title: Data Packing and Serialization sidebar_position: 7 tags: [contracts, storage, gas, optimization] description: Understand Serialize, Deserialize, and Packable traits, when each is used, how to write custom packing, and the cost implications. -references: ["docs/examples/contracts/packing_example/src/types.nr"] +references: ["docs/lines/v6/examples/contracts/packing_example/src/types.nr"] --- Aztec contracts use two separate encoding schemes to convert structs into `Field` arrays. This page tells you which one to reach for and when. @@ -48,7 +48,7 @@ There are two cases. When a note's members are already `Field`-sized, deriving is enough: -#include_code field_like_note /docs/examples/contracts/packing_example/src/types.nr rust +#include_code field_like_note /docs/lines/v6/examples/contracts/packing_example/src/types.nr rust `Eq` is a Noir standard trait for equality comparisons (see [Noir's `Eq` trait](https://noir-lang.org/docs/language/traits)). `#[note]` does not require it, but deriving it is idiomatic because it enables `assert_eq` in tests and note-equality checks. `Serialize` and `Deserialize` are similarly optional here, and useful when a note type crosses a function boundary. @@ -56,7 +56,7 @@ This is how the built-in note types work too: [`AddressNote`](pathname:///aztec- When a note has members smaller than a `Field` (`bool`, `u8`, `u32`, `u64`), you can skip the `Packable` derive and write a custom implementation that packs multiple values into a single `Field`: -#include_code card_note /docs/examples/contracts/packing_example/src/types.nr rust +#include_code card_note /docs/lines/v6/examples/contracts/packing_example/src/types.nr rust Derived `Packable` would give `CardNote` an `N = 2`; the custom impl halves that to `N = 1`. Smaller `N` means fewer inputs to the note hash, which directly reduces the gate count of private functions. @@ -72,7 +72,7 @@ Primitive types (`bool`, `u8` through `u128`, `Field`, `AztecAddress`) already i When all members are already `Field`-sized (`Field`, `AztecAddress`, and types built from them), deriving is sufficient: -#include_code derived_packable /docs/examples/contracts/packing_example/src/types.nr rust +#include_code derived_packable /docs/lines/v6/examples/contracts/packing_example/src/types.nr rust For [`PublicMutable`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.PublicMutable), each element of the packed `[Field; N]` array maps directly to one `SLOAD` on read and one `SSTORE` on write, so a smaller `N` is a direct gas saving. [`PublicImmutable`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.PublicImmutable) and [`DelayedPublicMutable`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/state_vars/struct.DelayedPublicMutable) have additional overhead on top (see [Cost impact](#cost-impact) below), but also benefit from a smaller `N`. @@ -94,7 +94,7 @@ struct GameState { } ``` -#include_code game_state_manual_packable /docs/examples/contracts/packing_example/src/types.nr rust +#include_code game_state_manual_packable /docs/lines/v6/examples/contracts/packing_example/src/types.nr rust `N = 1` means every storage read or write uses one `SLOAD` / `SSTORE` instead of three. The rest of this section walks through how the implementation was built. @@ -125,7 +125,7 @@ This prime is ~253.58 bits (slightly less than 2^254), so not every 254-bit valu `2.pow_32(k)` computes `2^k`, which is equivalent to a left shift by `k` bits for integer-valued `Field`s. Add the shifted values together to concatenate them inside a single `Field`: -#include_code game_state_pack /docs/examples/contracts/packing_example/src/types.nr rust +#include_code game_state_pack /docs/lines/v6/examples/contracts/packing_example/src/types.nr rust The bit layout you choose is arbitrary; the only requirement is that `pack` and `unpack` agree. The example above places `score` (a `u32`) at the lowest bits because a truncating cast (`packed[0] as u32`) then extracts it for free, with no subtraction or division. When you have a member whose width matches a standard `uN` type, putting it in the lowest position makes `unpack` cleaner. The remaining members can be placed in any order above it. @@ -133,25 +133,25 @@ The bit layout you choose is arbitrary; the only requirement is that `pack` and Extract values from lowest bits first, to match how we packed them, subtracting each extracted value before extracting the next: -#include_code game_state_unpack /docs/examples/contracts/packing_example/src/types.nr rust +#include_code game_state_unpack /docs/lines/v6/examples/contracts/packing_example/src/types.nr rust ### Step 4: Write roundtrip tests Always test that `unpack(pack(x)) == x` for boundary values: -#include_code game_state_tests /docs/examples/contracts/packing_example/src/types.nr rust +#include_code game_state_tests /docs/lines/v6/examples/contracts/packing_example/src/types.nr rust ## More custom Packable examples ### Packing two u32 values into one Field -#include_code card_custom_packable /docs/examples/contracts/packing_example/src/types.nr rust +#include_code card_custom_packable /docs/lines/v6/examples/contracts/packing_example/src/types.nr rust With derived `Packable`, `Card` would have `N = 2`. The manual implementation achieves `N = 1`, halving the storage cost. ### Packing mixed-width integers -#include_code mixed_width_packable /docs/examples/contracts/packing_example/src/types.nr rust +#include_code mixed_width_packable /docs/lines/v6/examples/contracts/packing_example/src/types.nr rust A `u128` and a `u64` pack into a single `Field` (128 + 64 = 192 bits, well within the 253-bit safe limit), while the `AztecAddress` occupies a full `Field` on its own. This reduces `N` from 3 to 2. @@ -159,7 +159,7 @@ A `u128` and a `u64` pack into a single `Field` (128 + 64 = 192 bits, well withi Always test custom `Packable` implementations at boundary values: -#include_code pack_unpack_tests /docs/examples/contracts/packing_example/src/types.nr rust +#include_code pack_unpack_tests /docs/lines/v6/examples/contracts/packing_example/src/types.nr rust ## Cost impact diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/ethereum_aztec_messaging.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/ethereum_aztec_messaging.md index 9d7a7c56115..fa1ae45c418 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/ethereum_aztec_messaging.md +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/ethereum_aztec_messaging.md @@ -3,7 +3,7 @@ title: Ethereum<>Aztec Messaging tags: [contracts, portals] sidebar_position: 12 description: Send messages and data between L1 and L2 contracts using portal contracts and cross-chain messaging. -references: ["docs/examples/solidity/example_swap/ExampleTokenPortal.sol", "labs/noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr"] +references: ["docs/lines/v6/examples/solidity/example_swap/ExampleTokenPortal.sol", "labs/noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr"] --- This guide covers cross-chain communication between Ethereum (L1) and Aztec (L2) using portal contracts. @@ -28,7 +28,7 @@ Use the `Inbox` contract's `sendL2Message` function: | `_content` | `bytes32` | Hash of message content (use `Hash.sha256ToField`) | | `_secretHash` | `bytes32` | Hash of secret for message consumption | -#include_code deposit_to_aztec_public docs/examples/solidity/example_swap/ExampleTokenPortal.sol solidity +#include_code deposit_to_aztec_public docs/lines/v6/examples/solidity/example_swap/ExampleTokenPortal.sol solidity :::note Message availability L1 to L2 messages are not available immediately. The proposer batches messages from the Inbox and includes them in the next L2 block. You must wait for this before consuming the message on L2. @@ -62,7 +62,7 @@ Use the `Outbox` contract to consume L2 messages. L2 to L1 messages are only available after the epoch proof is submitted to L1. Since multiple L2 blocks fit within an epoch, there may be a delay—especially if the message was sent near the start of an epoch. ::: -#include_code withdraw docs/examples/solidity/example_swap/ExampleTokenPortal.sol solidity +#include_code withdraw docs/lines/v6/examples/solidity/example_swap/ExampleTokenPortal.sol solidity :::info Getting the membership witness diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md index da818aa1544..e0610cdb528 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md @@ -3,7 +3,7 @@ title: How to Define Functions sidebar_position: 1 tags: [functions, smart-contracts] description: Define different types of functions in your Aztec contracts for private, public, and utility execution. -references: ["docs/examples/contracts/bob_token_contract/src/main.nr", "docs/examples/contracts/counter_contract/src/main.nr"] +references: ["docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr", "docs/lines/v6/examples/contracts/counter_contract/src/main.nr"] --- ## Overview @@ -33,7 +33,7 @@ This guide shows you how to define different types of functions in your Aztec co Use `#[external("private")]` to create functions that execute privately on user devices. For example: -#include_code increment /docs/examples/contracts/counter_contract/src/main.nr rust +#include_code increment /docs/lines/v6/examples/contracts/counter_contract/src/main.nr rust Private functions run in a private context, can access private state, and can read certain public values through storage types like [`DelayedPublicMutable`](../state_variables.md#delayedpublicmutable). @@ -41,7 +41,7 @@ Private functions run in a private context, can access private state, and can re Use `#[external("public")]` to create functions that execute on the sequencer: -#include_code mint_public /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code mint_public /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust Public functions operate on public state, similar to EVM contracts. They can write to private storage, but any data written from a public function is publicly visible. @@ -51,7 +51,7 @@ Create offchain query functions using the `#[external("utility")]` annotation wi Utility functions are unconstrained functions that applications call to perform auxiliary tasks, like querying contract state or processing offchain messages. Their execution is never proven, so no guarantees are made on the correctness of their results. They can also be called from other utility functions and from private functions. See [utility calls](../calling_contracts.md#utility-calls). Public functions cannot call them. Example: -#include_code get_counter /docs/examples/contracts/counter_contract/src/main.nr rust +#include_code get_counter /docs/lines/v6/examples/contracts/counter_contract/src/main.nr rust Use `aztec.js` `simulate` to execute utility functions and read their return values. For details, see [Call Types](../../../foundational-topics/call_types.md#simulate). @@ -74,7 +74,7 @@ View functions cannot modify contract state. They're akin to Ethereum's `view` f Create contract-only functions using the `#[only_self]` annotation: -#include_code _assert_is_owner /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code _assert_is_owner /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust Only-self functions are only callable by the same contract, which is useful when a private function enqueues a public call that should only be callable internally. @@ -82,7 +82,7 @@ Only-self functions are only callable by the same contract, which is useful when Create constructor-like functions using the `#[initializer]` annotation: -#include_code constructor /docs/examples/contracts/counter_contract/src/main.nr rust +#include_code constructor /docs/lines/v6/examples/contracts/counter_contract/src/main.nr rust ### Use multiple initializers diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/index.md b/docs/lines/v6/docs-developers/docs/aztec-nr/index.md index 10f13e0e04d..9e2034e9fb7 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-nr/index.md +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/index.md @@ -3,7 +3,7 @@ title: Overview sidebar_position: 0 tags: [aztec.nr, smart contracts] description: Comprehensive guide to writing smart contracts for the Aztec network using Noir. -references: ["docs/examples/contracts/counter_contract/src/main.nr"] +references: ["docs/lines/v6/examples/contracts/counter_contract/src/main.nr"] --- import DocCardList from "@theme/DocCardList"; @@ -52,11 +52,11 @@ aztec = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include_aztec Update your `my_project_contract/src/main.nr` contract file to use the Aztec.nr macros for writing contracts. -#include_code setup /docs/examples/contracts/counter_contract/src/main.nr rust +#include_code setup /docs/lines/v6/examples/contracts/counter_contract/src/main.nr rust and import dependencies from the Aztec.nr library. -#include_code imports /docs/examples/contracts/counter_contract/src/main.nr rust +#include_code imports /docs/lines/v6/examples/contracts/counter_contract/src/main.nr rust :::info diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/logging.md b/docs/lines/v6/docs-developers/docs/aztec-nr/logging.md index d291a079829..57a01e3c2ce 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-nr/logging.md +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/logging.md @@ -3,7 +3,7 @@ title: Logging from Contracts sidebar_position: 4 tags: [contracts, logging, debugging, aztec.nr] description: Add log statements to your Aztec contracts and control log verbosity in tests and local networks. -references: ["docs/examples/contracts/logging_example/src/main.nr", "labs/noir-projects/aztec-nr/aztec/src/logging.nr"] +references: ["docs/lines/v6/examples/contracts/logging_example/src/main.nr", "labs/noir-projects/aztec-nr/aztec/src/logging.nr"] --- Aztec contracts can emit log messages at seven severity levels. Private function logs appear immediately during local simulation in the Private eXecution Environment (PXE), while public function logs are collected and displayed in test mode. @@ -17,7 +17,7 @@ Aztec contracts can emit log messages at seven severity levels. Private function Logging functions live under `aztec::oracle::logging`. Import the specific functions you need: -#include_code logging_imports /docs/examples/contracts/logging_example/src/main.nr rust +#include_code logging_imports /docs/lines/v6/examples/contracts/logging_example/src/main.nr rust Or import only what you need: @@ -47,24 +47,24 @@ When you set `LOG_LEVEL=info`, you see `fatal`, `error`, `warn`, and `info` mess Here is an example using all seven levels: -#include_code log_all_levels /docs/examples/contracts/logging_example/src/main.nr rust +#include_code log_all_levels /docs/lines/v6/examples/contracts/logging_example/src/main.nr rust ## Simple log messages Each level has a function that accepts a plain string with no format arguments: -#include_code log_simple /docs/examples/contracts/logging_example/src/main.nr rust +#include_code log_simple /docs/lines/v6/examples/contracts/logging_example/src/main.nr rust ## Log messages with format arguments Each level also has a `_format` variant that accepts a format string and an array of `Field` values. Use `{0}`, `{1}`, etc. to insert individual arguments by index, or `{}` to print the entire array: -#include_code log_format_patterns /docs/examples/contracts/logging_example/src/main.nr rust +#include_code log_format_patterns /docs/lines/v6/examples/contracts/logging_example/src/main.nr rust :::note Format arguments must be `Field` values. Use `.to_field()` to convert addresses and other types: -#include_code log_address /docs/examples/contracts/logging_example/src/main.nr rust +#include_code log_address /docs/lines/v6/examples/contracts/logging_example/src/main.nr rust ::: ## Viewing logs @@ -162,7 +162,7 @@ Private and public functions handle logging differently: The logging API is the same in public functions: -#include_code log_public /docs/examples/contracts/logging_example/src/main.nr rust +#include_code log_public /docs/lines/v6/examples/contracts/logging_example/src/main.nr rust ### Accessing logs programmatically diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/call_types.md b/docs/lines/v6/docs-developers/docs/foundational-topics/call_types.md index a8bda3875c4..ae2fd451d15 100644 --- a/docs/lines/v6/docs-developers/docs/foundational-topics/call_types.md +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/call_types.md @@ -210,7 +210,7 @@ There are two main ways to execute an Aztec contract function using the `aztec.j This is used to get a result out of an execution, either private or public. It creates no transaction and spends no gas. The mental model is fairly close to that of [`eth_call`](#eth_call), in that it can be used to call any type of function, simulate its execution and get a result out of it. `simulate` is also the only way to run [utility functions](#utility). -#include_code simulate_function docs/examples/ts/aztecjs_connection/index.ts typescript +#include_code simulate_function docs/lines/v6/examples/ts/aztecjs_connection/index.ts typescript :::warning No correctness is guaranteed on the result of `simulate`! Correct execution is entirely optional and left up to the client that handles this request. diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/data_structures.md b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/data_structures.md index 16e6471c89a..505e7d0c309 100644 --- a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/data_structures.md +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/data_structures.md @@ -11,25 +11,25 @@ This page documents the Solidity structs used for L1-L2 message passing in the A An entity on L1, specifying the address and the chainId. Used when specifying a sender or recipient on L1. -#include_code l1_actor docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity +#include_code l1_actor docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity ## `L2Actor` An entity on L2, specifying the Aztec address and the protocol version. Used when specifying a sender or recipient on L2. -#include_code l2_actor docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity +#include_code l2_actor docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity ## `L1ToL2Msg` A message sent from L1 to L2. The `secretHash` field contains the hash of a secret pre-image that must be known to consume the message on L2. Use [`computeSecretHash`](https://github.com/aztec-labs-eng/aztec-node/blob/#include_aztec_version/yarn-project/stdlib/src/hash/hash.ts) to compute it from a secret. -#include_code l1_to_l2_msg docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity +#include_code l1_to_l2_msg docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity ## `L2ToL1Msg` A message sent from L2 to L1. -#include_code l2_to_l1_msg docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity +#include_code l2_to_l1_msg docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/core/libraries/DataStructures.sol solidity ## See also diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/inbox.md b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/inbox.md index 9654d4dee3b..17bc675a0fd 100644 --- a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/inbox.md +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/inbox.md @@ -12,7 +12,7 @@ The `Inbox` is a contract deployed on L1 that handles message passing from L1 to Sends a message from L1 to L2. -#include_code send_l1_to_l2_message docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol solidity +#include_code send_l1_to_l2_message docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol solidity | Name | Type | Description | | ----------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/outbox.md b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/outbox.md index 4cf579946ce..6dda991436f 100644 --- a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/outbox.md +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/outbox.md @@ -12,7 +12,7 @@ The `Outbox` is a contract deployed on L1 that handles message passing from L2 t Inserts the root of a merkle tree containing all of the L2 to L1 messages in an epoch, after a proof covering the first `_numCheckpointsInEpoch` checkpoints of that epoch lands. This function is only callable by the Rollup contract. -#include_code outbox_insert docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity +#include_code outbox_insert docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity | Name | Type | Description | | ------------------------ | --------- | ------------------------------------------------------------------------------------------------------------ | @@ -28,7 +28,7 @@ Inserts the root of a merkle tree containing all of the L2 to L1 messages in an Allows a recipient to consume a message from the `Outbox`. -#include_code outbox_consume docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity +#include_code outbox_consume docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity | Name | Type | Description | | ------------------------ | ----------- | ---------------------------------------------------------------------------------------------------- | @@ -54,7 +54,7 @@ Allows a recipient to consume a message from the `Outbox`. Checks if an L2 to L1 message in a specific epoch has been consumed. -#include_code outbox_has_message_been_consumed_at_epoch_and_index docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity +#include_code outbox_has_message_been_consumed_at_epoch_and_index docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity | Name | Type | Description | | --------- | --------- | ------------------------------------------------------------ | diff --git a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/registry.md b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/registry.md index 9203e29dc22..993425d4002 100644 --- a/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/registry.md +++ b/docs/lines/v6/docs-developers/docs/foundational-topics/ethereum-aztec-messaging/registry.md @@ -12,7 +12,7 @@ The Registry is a contract deployed on L1 that tracks canonical and historical r Retrieves the number of versions that have been deployed. -#include_code registry_number_of_versions docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/governance/interfaces/IRegistry.sol solidity +#include_code registry_number_of_versions docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/governance/interfaces/IRegistry.sol solidity | Name | Description | | ----------- | ---------------------------------------------- | @@ -22,7 +22,7 @@ Retrieves the number of versions that have been deployed. Retrieves the current rollup contract. -#include_code registry_get_canonical_rollup docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/governance/interfaces/IRegistry.sol solidity +#include_code registry_get_canonical_rollup docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/governance/interfaces/IRegistry.sol solidity | Name | Description | | ----------- | ------------------ | @@ -32,7 +32,7 @@ Retrieves the current rollup contract. Retrieves the rollup contract for a specific version. -#include_code registry_get_rollup docs/node_modules/@aztec-foundation/l1-artifacts/l1-contracts/src/governance/interfaces/IRegistry.sol solidity +#include_code registry_get_rollup docs/lines/v6/.aztec-node/l1-artifacts/l1-contracts/src/governance/interfaces/IRegistry.sol solidity | Name | Description | | ----------- | ------------------------------------ | diff --git a/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md index 8835665d878..5ad5b632911 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md @@ -2,7 +2,7 @@ title: Counter contract description: Code-along tutorial for creating a simple counter contract on Aztec. sidebar_position: 0 -references: ["docs/examples/contracts/counter_contract/src/main.nr"] +references: ["docs/lines/v6/examples/contracts/counter_contract/src/main.nr"] --- import Image from "@theme/IdealImage"; @@ -56,7 +56,7 @@ balance_set = { git="https://github.com/aztec-labs-eng/aztec-nr/", tag="#include Go to `counter_contract/src/main.nr`, and replace the boilerplate code with this contract initialization: ```rust -#include_code setup /docs/examples/contracts/counter_contract/src/main.nr raw +#include_code setup /docs/lines/v6/examples/contracts/counter_contract/src/main.nr raw } ``` @@ -82,7 +82,7 @@ pub contract Counter { } ``` -#include_code imports /docs/examples/contracts/counter_contract/src/main.nr rust +#include_code imports /docs/lines/v6/examples/contracts/counter_contract/src/main.nr rust - `macros::{functions::{external, initializer}, storage::storage}` Imports the macros needed to define function types (`external`, `initializer`) and the `storage` macro for declaring contract storage structures. @@ -106,7 +106,7 @@ pub contract Counter { Add this below the imports. It declares the storage variables for our contract. We use an `Owned` state variable wrapping a `BalanceSet` to manage private balances for each owner. -#include_code storage_struct /docs/examples/contracts/counter_contract/src/main.nr rust +#include_code storage_struct /docs/lines/v6/examples/contracts/counter_contract/src/main.nr rust ## Keep the counter private @@ -114,7 +114,7 @@ Now we’ve got a mechanism for storing our private state, we can start using it Let’s create a constructor method to run on deployment that assigns an initial count to a specified owner. We name it `constructor` here, but the name is arbitrary; it is the `#[initializer]` decorator that marks it to run once when the contract is deployed. Write this: -#include_code constructor /docs/examples/contracts/counter_contract/src/main.nr rust +#include_code constructor /docs/lines/v6/examples/contracts/counter_contract/src/main.nr rust This function accesses the counters from storage. It adds the `initial_value` to the `owner`'s counter using `at().add()`, then calls `.deliver(MessageDelivery::onchain_constrained())` to ensure the note is delivered onchain. @@ -124,7 +124,7 @@ We have annotated this and other functions with `#[external("private")]` which a Now let's implement an `increment` function to increase the counter. -#include_code increment /docs/examples/contracts/counter_contract/src/main.nr rust +#include_code increment /docs/lines/v6/examples/contracts/counter_contract/src/main.nr rust The `increment` function works similarly to the `constructor`. It logs a debug message, then adds 1 to the `owner`'s counter and delivers the note onchain. @@ -132,7 +132,7 @@ The `increment` function works similarly to the `constructor`. It logs a debug m The last thing we need to implement is a function to retrieve a counter value. -#include_code get_counter /docs/examples/contracts/counter_contract/src/main.nr rust +#include_code get_counter /docs/lines/v6/examples/contracts/counter_contract/src/main.nr rust This is a `utility` function used to obtain the counter value outside of a transaction. We access the `owner`'s balance from the `counters` storage variable using `at(owner)`, then call `balance_of()` to retrieve the current count. This yields a private counter that only the owner can decrypt. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md index 5ba204338e2..3517a2cc4fc 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md @@ -1,7 +1,7 @@ --- title: "Verify Noir Proofs in Aztec Contracts" description: "Learn to generate offchain ZK proofs with Noir and verify them onchain in Aztec private smart contracts" -references: ["docs/examples/circuits/hello_circuit/*", "docs/examples/contracts/recursive_verification_contract/*", "docs/examples/ts/recursive_verification/*"] +references: ["docs/lines/v6/examples/circuits/hello_circuit/*", "docs/lines/v6/examples/contracts/recursive_verification_contract/*", "docs/lines/v6/examples/ts/recursive_verification/*"] --- ## Overview @@ -126,7 +126,7 @@ circuit/ Replace the contents of `circuit/src/main.nr` with: -#include_code circuit /docs/examples/circuits/hello_circuit/src/main.nr rust +#include_code circuit /docs/lines/v6/examples/circuits/hello_circuit/src/main.nr rust This is intentionally minimal to focus on the verification pattern. In production, you would replace `assert(x != y)` with meaningful computations like: @@ -156,7 +156,7 @@ For example, you could create a zkpassport proof demonstrating that you are over Update `circuit/Nargo.toml` (see [Noir crates and packages](https://noir-lang.org/docs/project_structure/crates_and_packages) for more details): -#include_code circuit_nargo_toml /docs/examples/circuits/hello_circuit/Nargo.toml toml +#include_code circuit_nargo_toml /docs/lines/v6/examples/circuits/hello_circuit/Nargo.toml toml **Note**: This is a vanilla Noir circuit, not an Aztec contract. It has `type = "bin"` (binary) and no Aztec dependencies. The circuit is compiled with `nargo`, not `aztec compile`. This distinction is important—you can verify proofs from _any_ Noir circuit inside Aztec contracts. @@ -252,7 +252,7 @@ bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages Replace the contents of `ValueNotEqual/ValueNotEqual_contract/src/main.nr` with: -#include_code full_contract /docs/examples/contracts/recursive_verification_contract/src/main.nr rust +#include_code full_contract /docs/lines/v6/examples/contracts/recursive_verification_contract/src/main.nr rust :::note Clear the scaffold's placeholder test The scaffolded `ValueNotEqual/ValueNotEqual_test/src/lib.nr` imports the default contract name (`Main`) we just replaced above, so it now fails to compile. Tests aren't used in this tutorial — replace its contents with a single-line stub so `aztec compile` stays clean: @@ -471,7 +471,7 @@ Create `scripts/generate_data.ts`: ```js import circuitJson from "../circuit/target/hello_circuit.json" with { type: "json" }; -#include_code generate_data /docs/examples/ts/recursive_verification/scripts/generate_data.ts raw +#include_code generate_data /docs/lines/v6/examples/ts/recursive_verification/scripts/generate_data.ts raw ``` ### Understanding the Proof Generation Pipeline @@ -592,7 +592,7 @@ The deployment script connects to the Aztec network, creates an account, deploys Create `index.ts`: -#include_code run_recursion /docs/examples/ts/recursive_verification/index.ts typescript +#include_code run_recursion /docs/lines/v6/examples/ts/recursive_verification/index.ts typescript ### Understanding the Deployment Script @@ -635,7 +635,7 @@ This single line triggers a complex flow: Create `scripts/sponsored_fpc.ts`: -#include_code sponsored_fpc /docs/examples/ts/recursive_verification/scripts/sponsored_fpc.ts typescript +#include_code sponsored_fpc /docs/lines/v6/examples/ts/recursive_verification/scripts/sponsored_fpc.ts typescript This utility computes the address of the pre-deployed sponsored FPC contract. The salt ensures we get the same address every time. For more information about fee payment options, see [Paying Fees](../../aztec-js/how_to_pay_fees.md). diff --git a/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/token_contract.md b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/token_contract.md index 10083fa64ca..d87dcc4f080 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/token_contract.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/contract_tutorials/token_contract.md @@ -3,7 +3,7 @@ title: Private Token Contract sidebar_position: 1 tags: [privacy, tokens, intermediate] description: Build a privacy-preserving token for employee mental health benefits that keeps spending habits confidential. -references: ["docs/examples/contracts/bob_token_contract/src/main.nr", "docs/examples/ts/bob_token_contract/index.ts"] +references: ["docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr", "docs/lines/v6/examples/ts/bob_token_contract/index.ts"] --- ## The Privacy Challenge: Mental Health Benefits at Giggle @@ -52,7 +52,7 @@ yarn add @aztec-labs/aztec.js@#include_aztec_version @aztec-labs/accounts@#inclu The `aztec new` command created a workspace with two crates: a `bob_token_contract` crate for your smart contract code and a `bob_token_test` crate for Noir tests. In `bob_token_contract/src/main.nr` we have a proto-contract. Let's replace it with a simple starting point: ```rust -#include_code start /docs/examples/contracts/bob_token_contract/src/main.nr raw +#include_code start /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr raw // We'll build the mental health token here } ``` @@ -81,7 +81,7 @@ aztec = { git = "https://github.com/aztec-labs-eng/aztec-nr/", tag = "#include_a Since we're here, let's import more specific stuff from this library: ```rust -#include_code imports /docs/examples/contracts/bob_token_contract/src/main.nr raw +#include_code imports /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr raw } ``` @@ -118,7 +118,7 @@ Let's start with the public components that Giggle will use to mint and track in First, define the storage for our BOB tokens: ```rust -#include_code public_storage /docs/examples/contracts/bob_token_contract/src/main.nr raw +#include_code public_storage /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr raw } ``` @@ -140,7 +140,7 @@ While employees want privacy when spending, having public balances during mintin When deploying the contract, we need to set Giggle as the owner: -#include_code setup /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code setup /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust The `#[initializer]` decorator ensures this runs once during deployment. Only Giggle's address will have the power to mint new BOB tokens for employees. @@ -148,7 +148,7 @@ The `#[initializer]` decorator ensures this runs once during deployment. Only Gi Giggle needs a way to allocate mental health tokens to employees: -#include_code mint_public /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code mint_public /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust This public minting function: @@ -164,7 +164,7 @@ Imagine Giggle allocating 100 BOB tokens to each employee at the start of the ye While most transfers will be private, we'll add public transfers for cases where transparency is desired: -#include_code transfer_public /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code transfer_public /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust This might be used when: @@ -176,7 +176,7 @@ This might be used when: In case Giggle's mental health program administration changes: -#include_code transfer_ownership /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code transfer_ownership /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust ## Your First Deployment - Let's See It Work @@ -207,9 +207,9 @@ aztec start --local-network Then we will use the `giggleWallet` to deploy our contract, mint 100 BOB to Alice, then transfer 10 of those to Bob's Clinic publicly... for now. Let's go: ```typescript -#include_code imports /docs/examples/ts/bob_token_contract/index.ts raw +#include_code imports /docs/lines/v6/examples/ts/bob_token_contract/index.ts raw -#include_code checkpoint_1 /docs/examples/ts/bob_token_contract/index.ts raw +#include_code checkpoint_1 /docs/lines/v6/examples/ts/bob_token_contract/index.ts raw } main().catch(console.error); @@ -300,7 +300,7 @@ pub contract BobToken { We need to update the contract storage to have private balances as well: -#include_code storage /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code storage /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust The `private_balances` use `BalanceSet` which manages encrypted notes automatically. @@ -308,11 +308,11 @@ The `private_balances` use `BalanceSet` which manages encrypted notes automatica Great, now our contract knows about private balances. Let's implement a method to allow users to move their publicly minted tokens there: -#include_code public_to_private /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code public_to_private /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust And the helper function: -#include_code _deduct_public_balance /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code _deduct_public_balance /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust By calling `public_to_private` we're telling the network "deduct this amount from my balance" while simultaneously creating a Note with that balance in privateland. @@ -320,7 +320,7 @@ By calling `public_to_private` we're telling the network "deduct this amount fro Now for the crucial privacy feature - transferring BOB tokens in privacy. This is actually pretty simple: -#include_code transfer_private /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code transfer_private /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust This function simply nullifies the sender's notes, while adding them to the recipient. @@ -338,7 +338,7 @@ When an employee uses 50 BOB tokens at Bob's clinic, this private transfer ensur Employees can check their BOB token balances without hitting the network by using utility unconstrained functions: -#include_code check_balances /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code check_balances /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust ## Part 3: Securing Private Minting @@ -367,11 +367,11 @@ We want Giggle to mint BOB tokens directly to employees' private balances (for m Let's use a clever pattern where private functions enqueue public validation checks. First we make a little helper function in public. Remember, public functions always run _after_ private functions, since private functions run client-side. -#include_code _assert_is_owner /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code _assert_is_owner /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust Now we can add a secure private minting function. It looks pretty easy, and it is, since the whole thing will revert if the public function fails: -#include_code mint_private /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code mint_private /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust This pattern ensures: @@ -384,7 +384,7 @@ This pattern ensures: For the sake of completeness, let's also have a function that brings the tokens back to publicland: -#include_code private_to_public /docs/examples/contracts/bob_token_contract/src/main.nr rust +#include_code private_to_public /docs/lines/v6/examples/contracts/bob_token_contract/src/main.nr rust Now you've made changes to your contract, you need to recompile your contract. @@ -413,7 +413,7 @@ Let's stop being lazy and add a nice little "log" function that just spits out e ```typescript // at the top of your file -#include_code get_balances /docs/examples/ts/bob_token_contract/index.ts raw +#include_code get_balances /docs/lines/v6/examples/ts/bob_token_contract/index.ts raw ``` Looks ugly but it does what it says: prints Alice's and Bob's balances. This will make it easier to see our contract working. @@ -423,7 +423,7 @@ Now let's add some more stuff to our `index.ts`: ```typescript async function main() { // ...etc -#include_code checkpoint_2 /docs/examples/ts/bob_token_contract/index.ts raw +#include_code checkpoint_2 /docs/lines/v6/examples/ts/bob_token_contract/index.ts raw ``` The flow is something like: diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md index ed673785e36..13a0aabd17c 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md @@ -3,7 +3,7 @@ title: "Deposit to Aave from Aztec" sidebar_position: 2 description: "Build a cross-chain DeFi integration that deposits tokens into Aave from Aztec L2 and claims yield back." tags: [defi, cross-chain, messaging, portals, advanced] -references: ["docs/examples/contracts/aave_bridge/src/main.nr", "docs/examples/solidity/aave_bridge/AavePortal.sol", "docs/examples/ts/aave_bridge/index.ts"] +references: ["docs/lines/v6/examples/contracts/aave_bridge/src/main.nr", "docs/lines/v6/examples/solidity/aave_bridge/AavePortal.sol", "docs/lines/v6/examples/ts/aave_bridge/index.ts"] --- ## Why DeFi from Aztec? @@ -217,14 +217,14 @@ token = { git="https://github.com/aztec-labs-eng/aztec-node", tag = "#include_az The bridge stores two things: the L2 token address and the L1 portal address. First, create the config module at `contracts/aztec/aave_bridge/aave_bridge_contract/src/config.nr`: -#include_code config /docs/examples/contracts/aave_bridge/src/config.nr rust +#include_code config /docs/lines/v6/examples/contracts/aave_bridge/src/config.nr rust Then replace `contracts/aztec/aave_bridge/aave_bridge_contract/src/main.nr`: ```rust -#include_code bridge_setup /docs/examples/contracts/aave_bridge/src/main.nr raw +#include_code bridge_setup /docs/lines/v6/examples/contracts/aave_bridge/src/main.nr raw } ``` @@ -236,9 +236,9 @@ The code above shows the contract opening — imports, storage, constructor, and Add the following functions inside the `AaveBridge` contract body (before the closing `}`). `claim_public` consumes an L1→L2 message and mints tokens. `exit_to_l1_public` burns tokens and sends an L2→L1 message: -#include_code claim_public /docs/examples/contracts/aave_bridge/src/main.nr rust +#include_code claim_public /docs/lines/v6/examples/contracts/aave_bridge/src/main.nr rust -#include_code exit_to_l1_public /docs/examples/contracts/aave_bridge/src/main.nr rust +#include_code exit_to_l1_public /docs/lines/v6/examples/contracts/aave_bridge/src/main.nr rust The `authwit_nonce` parameter supports [authentication witnesses](../../aztec-js/how_to_use_authwit.md). When the caller is the token owner (`msg.sender`), pass `0` — no authorization witness is needed. If a third party calls this function on behalf of the owner, they must provide a valid nonce from an authwit the owner previously created. @@ -246,9 +246,9 @@ The `authwit_nonce` parameter supports [authentication witnesses](../../aztec-js Still inside the contract body, add the private variants. They work the same way but use private token operations. The recipient's address is hidden when claiming privately: -#include_code claim_private /docs/examples/contracts/aave_bridge/src/main.nr rust +#include_code claim_private /docs/lines/v6/examples/contracts/aave_bridge/src/main.nr rust -#include_code exit_to_l1_private /docs/examples/contracts/aave_bridge/src/main.nr rust +#include_code exit_to_l1_private /docs/lines/v6/examples/contracts/aave_bridge/src/main.nr rust :::info Content Hash Matching @@ -291,7 +291,7 @@ Create the following mock contracts in `contracts/`. ```solidity import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -#include_code mock_erc20 /docs/examples/solidity/aave_bridge/MockERC20.sol raw +#include_code mock_erc20 /docs/lines/v6/examples/solidity/aave_bridge/MockERC20.sol raw ``` `contracts/MockAToken.sol` — Aave's yield-bearing token mock: @@ -299,7 +299,7 @@ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; ```solidity import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -#include_code mock_atoken /docs/examples/solidity/aave_bridge/MockAToken.sol raw +#include_code mock_atoken /docs/lines/v6/examples/solidity/aave_bridge/MockAToken.sol raw ``` `contracts/MockAavePool.sol` — simplified Aave lending pool that returns a configurable yield: @@ -310,7 +310,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {MockERC20} from "./MockERC20.sol"; import {MockAToken} from "./MockAToken.sol"; -#include_code mock_aave_pool /docs/examples/solidity/aave_bridge/MockAavePool.sol raw +#include_code mock_aave_pool /docs/lines/v6/examples/solidity/aave_bridge/MockAavePool.sol raw ``` ### AavePortal Contract @@ -331,7 +331,7 @@ import {DataStructures} from "@aztec-foundation/l1-artifacts/l1-contracts/src/co import {Hash} from "@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/crypto/Hash.sol"; import {Epoch} from "@aztec-foundation/l1-artifacts/l1-contracts/src/core/libraries/TimeLib.sol"; -#include_code portal_setup /docs/examples/solidity/aave_bridge/AavePortal.sol raw +#include_code portal_setup /docs/lines/v6/examples/solidity/aave_bridge/AavePortal.sol raw } ``` @@ -341,15 +341,15 @@ Like the L2 contract, the code above shows the contract opening — imports, sta The portal has three key functions. First, `depositToAave` consumes an L2→L1 message (proving the user burned tokens on L2) and deposits the underlying tokens into Aave: -#include_code portal_deposit_to_aave /docs/examples/solidity/aave_bridge/AavePortal.sol solidity +#include_code portal_deposit_to_aave /docs/lines/v6/examples/solidity/aave_bridge/AavePortal.sol solidity Then, `claimFromAavePublic` withdraws from Aave (including any yield earned) and sends an L1→L2 message so the user can mint tokens on L2: -#include_code portal_claim_public /docs/examples/solidity/aave_bridge/AavePortal.sol solidity +#include_code portal_claim_public /docs/lines/v6/examples/solidity/aave_bridge/AavePortal.sol solidity There's also a private variant that lets the user claim without revealing their L2 address: -#include_code portal_claim_private /docs/examples/solidity/aave_bridge/AavePortal.sol solidity +#include_code portal_claim_private /docs/lines/v6/examples/solidity/aave_bridge/AavePortal.sol solidity ### Compile @@ -390,7 +390,7 @@ import MockAavePool from "../artifacts/contracts/MockAavePool.sol/MockAavePool.j import { TokenContract } from "@aztec-labs/noir-contracts.js/Token"; import { AaveBridgeContract } from "../contracts/aztec/artifacts/AaveBridge.js"; -#include_code setup /docs/examples/ts/aave_bridge/index.ts raw +#include_code setup /docs/lines/v6/examples/ts/aave_bridge/index.ts raw ``` :::note About EmbeddedWallet @@ -403,15 +403,15 @@ import { AaveBridgeContract } from "../contracts/aztec/artifacts/AaveBridge.js"; The deployment snippet below comes from the example project, which builds its Solidity with Foundry. Hardhat artifacts store the bytecode as a plain string, so with `npx hardhat compile` use `MockERC20.bytecode` (and likewise for `MockAToken`, `MockAavePool` and `AavePortal`) instead of `.bytecode.object`. ::: -#include_code deploy_l1 /docs/examples/ts/aave_bridge/index.ts typescript +#include_code deploy_l1 /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript ### Deploy L2 Contracts -#include_code deploy_l2 /docs/examples/ts/aave_bridge/index.ts typescript +#include_code deploy_l2 /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript ### Initialize -#include_code initialize /docs/examples/ts/aave_bridge/index.ts typescript +#include_code initialize /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript ### Fund the User @@ -422,7 +422,7 @@ For this tutorial, you need tokens in two places: For simplicity, mint directly to both: -#include_code fund_user /docs/examples/ts/aave_bridge/index.ts typescript +#include_code fund_user /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript ### Deposit to Aave (L2 → L1) @@ -432,15 +432,15 @@ Now for the main flow. Burn tokens on L2 and send a message to L1. The `recipient` in `exit_to_l1_public` is the L1 address that receives the withdrawal message. Since the AavePortal contract needs to deposit the tokens into Aave, the portal itself is the recipient. Setting `caller_on_l1` to `EthAddress.ZERO` means anyone can relay the message on L1 — there's no access restriction on who calls `depositToAave`. ::: -#include_code deposit_to_aave /docs/examples/ts/aave_bridge/index.ts typescript +#include_code deposit_to_aave /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript Compute the membership witness to prove the message on L1: -#include_code get_deposit_witness /docs/examples/ts/aave_bridge/index.ts typescript +#include_code get_deposit_witness /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript Execute the deposit on L1: -#include_code execute_deposit_l1 /docs/examples/ts/aave_bridge/index.ts typescript +#include_code execute_deposit_l1 /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript ### Claim from Aave with Yield (L1 → L2) @@ -448,23 +448,23 @@ Before withdrawing from Aave, generate a random secret and compute its hash. The Withdraw from Aave on L1 and send the message to L2. The mock pool returns 10% yield: -#include_code claim_from_aave_l1 /docs/examples/ts/aave_bridge/index.ts typescript +#include_code claim_from_aave_l1 /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript Extract the message leaf index: -#include_code get_claim_leaf_index /docs/examples/ts/aave_bridge/index.ts typescript +#include_code get_claim_leaf_index /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript On the local network, L2 blocks are only produced when transactions are submitted. An L1-to-L2 message can only be consumed once an L2 block includes it, and the network waits until the message is at least 12 seconds old before including it. This utility deploys two dummy contracts (with random salts for unique addresses) to force block production. On devnet or testnet, blocks are produced continuously and this step is unnecessary: -#include_code mine_blocks /docs/examples/ts/aave_bridge/index.ts typescript +#include_code mine_blocks /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript Claim the tokens (with yield) on L2: -#include_code claim_on_l2 /docs/examples/ts/aave_bridge/index.ts typescript +#include_code claim_on_l2 /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript ### Verify -#include_code verify /docs/examples/ts/aave_bridge/index.ts typescript +#include_code verify /docs/lines/v6/examples/ts/aave_bridge/index.ts typescript Run the full flow: diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aztecjs-getting-started.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aztecjs-getting-started.md index 4ca94044426..7d9c06d4739 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aztecjs-getting-started.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aztecjs-getting-started.md @@ -2,7 +2,7 @@ title: Deploying a Token Contract sidebar_position: 0 description: A tutorial going through how to deploy a token contract to the local network using typescript. -references: ["docs/examples/ts/aztecjs_getting_started/index.ts"] +references: ["docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts"] --- import Image from "@theme/IdealImage"; @@ -68,7 +68,7 @@ Keep this terminal running throughout the tutorial. Create an `index.ts` file in the root of your project with the following code. This connects to the local network and imports test accounts (Alice and Bob): -#include_code setup /docs/examples/ts/aztecjs_getting_started/index.ts typescript +#include_code setup /docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts typescript **Step 3: Verify the script runs** @@ -86,23 +86,23 @@ Now that we have our accounts loaded, let's deploy a pre-compiled token contract Add the following to `index.ts` to import the contract and deploy it with Alice as the admin: -#include_code deploy /docs/examples/ts/aztecjs_getting_started/index.ts typescript +#include_code deploy /docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts typescript ## Mint and transfer Let's go ahead and have Alice mint herself some tokens, in private: -#include_code mint /docs/examples/ts/aztecjs_getting_started/index.ts typescript +#include_code mint /docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts typescript Let's check both Alice's and Bob's balances now: -#include_code check_balances /docs/examples/ts/aztecjs_getting_started/index.ts typescript +#include_code check_balances /docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts typescript Alice should have 100 tokens, while Bob has none yet. Great! Let's have Alice transfer some tokens to Bob, also in private: -#include_code transfer /docs/examples/ts/aztecjs_getting_started/index.ts typescript +#include_code transfer /docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts typescript Bob should now see 10 tokens in his balance. @@ -110,11 +110,11 @@ Bob should now see 10 tokens in his balance. Say that Alice is nice and wants to set Bob as a minter. Even though it's a public function, it can be called in a similar way: -#include_code set_minter /docs/examples/ts/aztecjs_getting_started/index.ts typescript +#include_code set_minter /docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts typescript Bob is now the minter, so he can mint some tokens to himself: -#include_code bob_mints /docs/examples/ts/aztecjs_getting_started/index.ts typescript +#include_code bob_mints /docs/lines/v6/examples/ts/aztecjs_getting_started/index.ts typescript :::info diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md index 543158c3830..6785a52fa01 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md @@ -2,7 +2,7 @@ title: "Bridge Your NFT to Aztec" sidebar_position: 1 description: "Build a private NFT bridge between Ethereum and Aztec using custom notes, PrivateSet, and cross-chain messaging portals." -references: ["docs/examples/contracts/nft/*", "docs/examples/contracts/nft_bridge/*", "docs/examples/solidity/nft_bridge/*", "docs/examples/ts/token_bridge/*"] +references: ["docs/lines/v6/examples/contracts/nft/*", "docs/lines/v6/examples/contracts/nft_bridge/*", "docs/lines/v6/examples/solidity/nft_bridge/*", "docs/lines/v6/examples/ts/token_bridge/*"] --- ## Why Bridge an NFT? @@ -143,7 +143,7 @@ touch nft_contract/src/nft.nr In this file, you're going to create a **private note** that represents NFT ownership. This is a struct with macros that indicate it is a note that can be compared and packed: -#include_code nft_note_struct /docs/examples/contracts/nft/src/nft.nr rust +#include_code nft_note_struct /docs/lines/v6/examples/contracts/nft/src/nft.nr rust You now have a note that represents the owner of a particular NFT. Next, move on to the contract itself. @@ -169,7 +169,7 @@ Write the storage struct and a simple [initializer](../../foundational-topics/co ```rust -#include_code contract_setup /docs/examples/contracts/nft/src/main.nr raw +#include_code contract_setup /docs/lines/v6/examples/contracts/nft/src/main.nr raw } ``` @@ -177,28 +177,28 @@ Write the storage struct and a simple [initializer](../../foundational-topics/co Add an internal function to handle the `DelayedPublicMutable` value change. Mark the function as public and `#[only_self]` so only the contract can call it: -#include_code mark_nft_exists /docs/examples/contracts/nft/src/main.nr rust +#include_code mark_nft_exists /docs/lines/v6/examples/contracts/nft/src/main.nr rust This function is marked with `#[only_self]`, meaning only the contract itself can call it. It uses `schedule_value_change` to update the `nfts` storage, preventing the same NFT from being minted twice or burned when it doesn't exist. You'll call this public function from a private function later using `enqueue_self`. Another useful function checks how many notes a caller has. You can use this later to verify the claim and exit from L2: -#include_code notes_of /docs/examples/contracts/nft/src/main.nr rust +#include_code notes_of /docs/lines/v6/examples/contracts/nft/src/main.nr rust ### Add Minting and Burning Before anything else, you need to set the minter. This will be the bridge contract, so only the bridge contract can mint NFTs. This value doesn't need to change after initialization. Here's how to initialize the `PublicImmutable`: -#include_code set_minter /docs/examples/contracts/nft/src/main.nr rust +#include_code set_minter /docs/lines/v6/examples/contracts/nft/src/main.nr rust Now for the magic - minting NFTs **privately**. The bridge will call this to mint to a user, deliver the note using [constrained message delivery](../../aztec-nr/framework-description/events_and_logs.md) (best practice when "sending someone a note") and then [enqueue a public call](../../aztec-nr/framework-description/calling_contracts.md) to the `_mark_nft_exists` function: -#include_code mint /docs/examples/contracts/nft/src/main.nr rust +#include_code mint /docs/lines/v6/examples/contracts/nft/src/main.nr rust The bridge will also need to burn NFTs when users withdraw back to L1: -#include_code burn /docs/examples/contracts/nft/src/main.nr rust +#include_code burn /docs/lines/v6/examples/contracts/nft/src/main.nr rust :::note Clear the scaffold's placeholder test The scaffolded `nft_test/src/lib.nr` imports the default contract name (`Main`), which no longer exists, so it fails to compile with "Could not resolve 'Main'". Tests aren't used in this tutorial, so replace its contents with a single-line stub to keep `aztec compile` clean: @@ -284,7 +284,7 @@ Clean up `nft_bridge_contract/src/main.nr` which is just a placeholder, and let' ```rust -#include_code bridge_setup /docs/examples/contracts/nft_bridge/src/main.nr raw +#include_code bridge_setup /docs/lines/v6/examples/contracts/nft_bridge/src/main.nr raw } ``` @@ -298,7 +298,7 @@ You need to define how to encode messages. Here's a simple approach: when an NFT Build the `claim` function, which consumes the message and mints the NFT on the L2 side: -#include_code claim /docs/examples/contracts/nft_bridge/src/main.nr rust +#include_code claim /docs/lines/v6/examples/contracts/nft_bridge/src/main.nr rust :::tip Secret @@ -308,7 +308,7 @@ The secret prevents front-running. Certainly you don't want anyone to claim your Similarly, exiting to L1 means burning the NFT on the L2 side and pushing a message through the protocol. To ensure only the L1 recipient can claim it, hash the `token_id` together with the `recipient`: -#include_code exit /docs/examples/contracts/nft_bridge/src/main.nr rust +#include_code exit /docs/lines/v6/examples/contracts/nft_bridge/src/main.nr rust Cross-chain messaging on Aztec is powerful because it doesn't conform to any specific format—you can structure messages however you want. @@ -370,7 +370,7 @@ touch contracts/SimpleNFT.sol Create a minimal NFT contract sufficient for demonstrating bridging: -#include_code simple_nft /docs/examples/solidity/nft_bridge/SimpleNFT.sol solidity +#include_code simple_nft /docs/lines/v6/examples/solidity/nft_bridge/SimpleNFT.sol solidity ### Create the NFT Portal @@ -383,7 +383,7 @@ touch contracts/NFTPortal.sol Initialize it with Aztec's registry, which holds the canonical contracts for Aztec-related contracts, including the Inbox and Outbox. These are the message-passing contracts—Aztec sequencers read any messages on these contracts. ```solidity -#include_code portal_setup /docs/examples/solidity/nft_bridge/NFTPortal.sol raw +#include_code portal_setup /docs/lines/v6/examples/solidity/nft_bridge/NFTPortal.sol raw } ``` @@ -391,7 +391,7 @@ The core logic is similar to the L2 logic. `depositToAztec` calls the `Inbox` ca Add these two functions with explanatory comments: -#include_code portal_deposit_and_withdraw /docs/examples/solidity/nft_bridge/NFTPortal.sol solidity +#include_code portal_deposit_and_withdraw /docs/lines/v6/examples/solidity/nft_bridge/NFTPortal.sol solidity The portal handles two flows: @@ -434,7 +434,7 @@ This section assumes you're working locally using the local network. For the tes First, initialize the clients: `aztec.js` for Aztec and `viem` for Ethereum: -#include_code setup /docs/examples/ts/token_bridge/index.ts typescript +#include_code setup /docs/lines/v6/examples/ts/token_bridge/index.ts typescript :::warning Adjust the artifact imports for this project's layout The snippet above comes from the monorepo's runnable example, and its artifact imports point at that repo's layout. In the Hardhat project used in this tutorial, replace the four artifact imports with: @@ -451,15 +451,15 @@ import { NFTPunkContract } from "../contracts/aztec/artifacts/NFTPunk.js"; You now have wallets for both chains, correctly connected to their respective chains. Next, deploy the L1 contracts: -#include_code deploy_l1_contracts /docs/examples/ts/token_bridge/index.ts typescript +#include_code deploy_l1_contracts /docs/lines/v6/examples/ts/token_bridge/index.ts typescript Now deploy the L2 contracts. Thanks to the TypeScript bindings generated with `aztec codegen`, deployment is straightforward: -#include_code deploy_l2_contracts /docs/examples/ts/token_bridge/index.ts typescript +#include_code deploy_l2_contracts /docs/lines/v6/examples/ts/token_bridge/index.ts typescript Now that you have the L2 bridge's contract address, initialize the L1 bridge: -#include_code initialize_portal /docs/examples/ts/token_bridge/index.ts typescript +#include_code initialize_portal /docs/lines/v6/examples/ts/token_bridge/index.ts typescript The L2 contracts were already initialized when you deployed them, but you still need to: @@ -468,7 +468,7 @@ The L2 contracts were already initialized when you deployed them, but you still Complete these initialization steps: -#include_code initialize_l2_bridge /docs/examples/ts/token_bridge/index.ts typescript +#include_code initialize_l2_bridge /docs/lines/v6/examples/ts/token_bridge/index.ts typescript This completes the setup. It's a lot of configuration, but you're dealing with four contracts across two chains. @@ -476,41 +476,41 @@ This completes the setup. It's a lot of configuration, but you're dealing with f Now for the main flow. Mint a CryptoPunk on L1, deposit it to Aztec, and claim it on Aztec. Put everything in the same script. To mint, call the L1 contract with `mint`, which will mint `tokenId = 0`: -#include_code mint_nft_l1 /docs/examples/ts/token_bridge/index.ts typescript +#include_code mint_nft_l1 /docs/lines/v6/examples/ts/token_bridge/index.ts typescript To bridge, first approve the portal address to transfer the NFT, then transfer it by calling `depositToAztec`: -#include_code deposit_to_aztec /docs/examples/ts/token_bridge/index.ts typescript +#include_code deposit_to_aztec /docs/lines/v6/examples/ts/token_bridge/index.ts typescript The `Inbox` contract will emit an important log: `MessageSent(hash, inboxRollingHash, bucketSeq, message);`. The `message.index` field is the **leaf index** of the message in the [L1-L2 Message Tree](../../foundational-topics/ethereum-aztec-messaging/index.md)—the location of the message in the tree that will appear on L2. You need this index, plus the secret, to correctly claim and decrypt the message. Use viem to extract this information: -#include_code get_message_leaf_index /docs/examples/ts/token_bridge/index.ts typescript +#include_code get_message_leaf_index /docs/lines/v6/examples/ts/token_bridge/index.ts typescript This extracts the logs from the deposit and retrieves the leaf index. You can now claim it on L2. However, a message can only be claimed once an L2 block includes it, and the network waits until the message is at least 12 seconds old before including it, so expect 12 to 30 seconds of latency. If you called `claim` on the L2 contract immediately, it would return "no message available". On a local network blocks are only produced when transactions are submitted, so add a utility function that forces a couple of blocks (it deploys a contract with a random salt): -#include_code mine_blocks /docs/examples/ts/token_bridge/index.ts typescript +#include_code mine_blocks /docs/lines/v6/examples/ts/token_bridge/index.ts typescript Now claim the message on L2: -#include_code claim_on_l2 /docs/examples/ts/token_bridge/index.ts typescript +#include_code claim_on_l2 /docs/lines/v6/examples/ts/token_bridge/index.ts typescript ### L2 → L1 Flow Great! You can expand the L2 contract to add features like NFT transfers. For now, exit the NFT on L2 and redeem it on L1. Mine two blocks because of `DelayedMutable`: -#include_code exit_from_l2 /docs/examples/ts/token_bridge/index.ts typescript +#include_code exit_from_l2 /docs/lines/v6/examples/ts/token_bridge/index.ts typescript Just like in the L1 → L2 flow, you need to know what to claim on L1. Where in the message tree is the message you want to claim? Use `node.getL2ToL1MembershipWitness`, which provides the leaf and the sibling path of the message: -#include_code get_withdrawal_witness /docs/examples/ts/token_bridge/index.ts typescript +#include_code get_withdrawal_witness /docs/lines/v6/examples/ts/token_bridge/index.ts typescript With this information, call the L1 contract and use the index and the sibling path to claim the L1 NFT: -#include_code withdraw_on_l1 /docs/examples/ts/token_bridge/index.ts typescript +#include_code withdraw_on_l1 /docs/lines/v6/examples/ts/token_bridge/index.ts typescript You can now try the whole flow with: diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/uniswap_swap.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/uniswap_swap.md index 8d7dacfa9fe..79df25ad2d4 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/uniswap_swap.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/uniswap_swap.md @@ -2,7 +2,7 @@ title: "Cross-Chain Token Swap (L1 <> L2)" sidebar_position: 2 description: "Build a cross-chain token swap that exits L2, swaps on L1, and deposits the output back to L2 using Aztec's messaging protocol." -references: ["docs/examples/contracts/example_uniswap/*", "docs/examples/solidity/example_swap/*", "docs/examples/ts/example_swap/*"] +references: ["docs/lines/v6/examples/contracts/example_uniswap/*", "docs/lines/v6/examples/solidity/example_swap/*", "docs/lines/v6/examples/ts/example_swap/*"] --- ## Why Build a Cross-Chain Swap? @@ -142,7 +142,7 @@ Neither message alone is sufficient. If only the token exit existed, anyone obse The token portal handles depositing tokens from L1 to L2 and withdrawing from L2 to L1. This is a simplified version for tutorial purposes -- for a deeper look at how portals work, see the [token bridge tutorial](./token_bridge.md). -#include_code example_token_portal /docs/examples/solidity/example_swap/ExampleTokenPortal.sol solidity +#include_code example_token_portal /docs/lines/v6/examples/solidity/example_swap/ExampleTokenPortal.sol solidity Key functions: @@ -154,9 +154,9 @@ The [registry](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) Each cross-chain message includes a **content hash** -- a `sha256` digest of the function selector and its parameters that uniquely identifies the message. The content hash is computed with `Hash.sha256ToField(abi.encodeWithSignature(...))`, where `abi.encodeWithSignature` prepends a 4-byte function selector (keccak256 of the function signature, per Solidity convention). This makes each message type unique, preventing a deposit message from being confused with a withdrawal message. -#include_code deposit_to_aztec_public /docs/examples/solidity/example_swap/ExampleTokenPortal.sol solidity +#include_code deposit_to_aztec_public /docs/lines/v6/examples/solidity/example_swap/ExampleTokenPortal.sol solidity -#include_code withdraw /docs/examples/solidity/example_swap/ExampleTokenPortal.sol solidity +#include_code withdraw /docs/lines/v6/examples/solidity/example_swap/ExampleTokenPortal.sol solidity ## Part 2: Uniswap Portal (Solidity) @@ -166,15 +166,15 @@ The uniswap portal orchestrates the swap on L1. It consumes two L2→L1 messages This tutorial uses a mock 1:1 swap instead of a real Uniswap V3 router. The portal must be pre-funded with output tokens. The important part is the **message-passing pattern**, not the swap itself. ::: -#include_code example_uniswap_portal /docs/examples/solidity/example_swap/ExampleUniswapPortal.sol solidity +#include_code example_uniswap_portal /docs/lines/v6/examples/solidity/example_swap/ExampleUniswapPortal.sol solidity The public swap function consumes two messages and deposits the output: -#include_code swap_public /docs/examples/solidity/example_swap/ExampleUniswapPortal.sol solidity +#include_code swap_public /docs/lines/v6/examples/solidity/example_swap/ExampleUniswapPortal.sol solidity The private swap follows the same pattern but deposits output tokens privately: -#include_code swap_private /docs/examples/solidity/example_swap/ExampleUniswapPortal.sol solidity +#include_code swap_private /docs/lines/v6/examples/solidity/example_swap/ExampleUniswapPortal.sol solidity ### Compile Solidity Contracts @@ -195,7 +195,7 @@ The L2 contract handles the user-facing logic: transferring input tokens, callin The contract stores the portal address and imports the `Token` and `TokenBridge` contracts: -#include_code example_uniswap_setup /docs/examples/contracts/example_uniswap/src/main.nr rust +#include_code example_uniswap_setup /docs/lines/v6/examples/contracts/example_uniswap/src/main.nr rust ### Public Swap @@ -205,13 +205,13 @@ The public swap transfers tokens from the sender to the contract, exits them to Aztec uses [**authorization witnesses** (authwit)](../../aztec-nr/framework-description/authentication_witnesses.md) instead of the ERC20 approve/transferFrom pattern. The contract computes the hash of the exact action it wants to perform, sets that hash as authorized, then immediately performs the action. This gives fine-grained control - the authorization is for a specific action, not a blanket approval. Since we authorize and spend in the same transaction, replay attacks are impossible. ::: -#include_code swap_public /docs/examples/contracts/example_uniswap/src/main.nr rust +#include_code swap_public /docs/lines/v6/examples/contracts/example_uniswap/src/main.nr rust ### Private Swap The private swap is similar but uses `transfer_to_public` (private to public transfer) and [`enqueue_self`](../../aztec-nr/framework-description/calling_contracts.md) instead of `call_self`. Because `swap_private` is a private function and `_approve_bridge_and_exit_input_asset_to_L1` is public, it cannot be called synchronously -- private functions execute before public functions in a transaction. `enqueue_self` schedules the public call to run in the public phase of the same transaction: -#include_code swap_private /docs/examples/contracts/example_uniswap/src/main.nr rust +#include_code swap_private /docs/lines/v6/examples/contracts/example_uniswap/src/main.nr rust :::note Why no recipient parameter? In `swap_private`, the recipient is the person that provides the secret used to generate the hash for the L1 to L2 message. This preserves privacy: revealing a recipient address to L1 would compromise the caller's identity. The output tokens are deposited privately to L2, where only the secret holder can claim them. @@ -221,7 +221,7 @@ In `swap_private`, the recipient is the person that provides the secret used to Both flows share this internal function that approves the bridge to burn tokens and exits them to L1: -#include_code approve_bridge_and_exit /docs/examples/contracts/example_uniswap/src/main.nr rust +#include_code approve_bridge_and_exit /docs/lines/v6/examples/contracts/example_uniswap/src/main.nr rust :::note Portal Address Validation The portal address checks are a **safety mechanism**. If either portal is zero (not configured), the funds would be permanently lost. Always validate external addresses before sending irreversible messages. @@ -237,9 +237,9 @@ These content hashes form the **cross-chain contract interface**. The L2 contrac This is how L1 verifies that L2 actually authorized the swap - not by trusting a signature, but by independently computing what the message should contain. The hashes must match exactly between L2 (Noir) and L1 (Solidity): -#include_code swap_public_content_hash /docs/examples/contracts/example_uniswap/src/util.nr rust +#include_code swap_public_content_hash /docs/lines/v6/examples/contracts/example_uniswap/src/util.nr rust -#include_code swap_private_content_hash /docs/examples/contracts/example_uniswap/src/util.nr rust +#include_code swap_private_content_hash /docs/lines/v6/examples/contracts/example_uniswap/src/util.nr rust ### Compile and Generate Bindings @@ -281,35 +281,35 @@ npm install \ Now you can tie everything together in a TypeScript script. Start by setting up clients and deploying all contracts: -#include_code setup /docs/examples/ts/example_swap/index.ts typescript +#include_code setup /docs/lines/v6/examples/ts/example_swap/index.ts typescript ### Deploy L1 Contracts Deploy two ERC20 tokens, two token portals, and the uniswap portal: -#include_code deploy_l1 /docs/examples/ts/example_swap/index.ts typescript +#include_code deploy_l1 /docs/lines/v6/examples/ts/example_swap/index.ts typescript ### Deploy L2 Contracts Deploy L2 tokens (using `TokenContract` from `@aztec-labs/noir-contracts.js`), bridges, and the uniswap contract: -#include_code deploy_l2 /docs/examples/ts/example_swap/index.ts typescript +#include_code deploy_l2 /docs/lines/v6/examples/ts/example_swap/index.ts typescript ### Initialize and Fund Initialize all portals and mint tokens: -#include_code initialize /docs/examples/ts/example_swap/index.ts typescript +#include_code initialize /docs/lines/v6/examples/ts/example_swap/index.ts typescript Fund the user with input tokens and pre-fund the uniswap portal with output tokens: -#include_code fund /docs/examples/ts/example_swap/index.ts typescript +#include_code fund /docs/lines/v6/examples/ts/example_swap/index.ts typescript ### Deposit to L2 Bridge WETH from L1 to L2: -#include_code deposit_to_l2 /docs/examples/ts/example_swap/index.ts typescript +#include_code deposit_to_l2 /docs/lines/v6/examples/ts/example_swap/index.ts typescript :::tip Why use a secret hash? When depositing from L1 to L2, we use a secret/secret-hash pattern: generate a random secret on the client, send only the hash to L1 (in the deposit transaction), then later reveal the secret on L2 to claim the tokens. This prevents **front-running attacks**: a malicious sequencer (the node that orders and processes L2 transactions) cannot observe the L1 deposit and claim the tokens themselves because they don't know the secret. Only someone who knows the preimage can claim. @@ -317,41 +317,41 @@ When depositing from L1 to L2, we use a secret/secret-hash pattern: generate a r Before claiming, we need to mine a couple of L2 blocks. An L1-to-L2 message is not available the moment it is sent -- the rollup only includes it in an L2 block once it is at least 12 seconds old, and it becomes consumable as soon as that block lands. We use a helper that deploys throwaway contracts to force these blocks: -#include_code mine_blocks /docs/examples/ts/example_swap/index.ts typescript +#include_code mine_blocks /docs/lines/v6/examples/ts/example_swap/index.ts typescript Claim the deposited tokens on L2: -#include_code claim_on_l2 /docs/examples/ts/example_swap/index.ts typescript +#include_code claim_on_l2 /docs/lines/v6/examples/ts/example_swap/index.ts typescript ### Execute the Swap Initiate the swap on L2: -#include_code public_swap /docs/examples/ts/example_swap/index.ts typescript +#include_code public_swap /docs/lines/v6/examples/ts/example_swap/index.ts typescript ### Waiting for Block Proofs L2→L1 messages can only be consumed on L1 after the L2 block containing them has been **proven**. Aztec batches blocks into [**epochs**](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) and generates ZK proofs for each epoch. The proof confirms that the L2 state transition (including our swap messages) actually happened according to the protocol rules. Until the proof is submitted to L1, the messages exist but cannot be trusted. -#include_code wait_for_proof /docs/examples/ts/example_swap/index.ts typescript +#include_code wait_for_proof /docs/lines/v6/examples/ts/example_swap/index.ts typescript The [outbox](../../aztec-nr/framework-description/ethereum_aztec_messaging.md) stores L2→L1 messages in a Merkle tree. To consume a message, you must provide the **epoch** (which proof batch contains the message), the **leaf index** (position in the message tree), and the **sibling path** (Merkle proof showing the message is in the tree). These parameters are computed offchain by observing L2 blocks. First, read the rollup version from the portal and compute the content hash and message leaf for the token bridge exit message: -#include_code consume_l1_messages_setup /docs/examples/ts/example_swap/index.ts typescript +#include_code consume_l1_messages_setup /docs/lines/v6/examples/ts/example_swap/index.ts typescript Next, compute Merkle membership witnesses for both L2→L1 messages -- the sibling path (proof of inclusion) for each. We also compute the swap intent message leaf using the same encoding as the Solidity portal: -#include_code consume_l1_messages_witnesses /docs/examples/ts/example_swap/index.ts typescript +#include_code consume_l1_messages_witnesses /docs/lines/v6/examples/ts/example_swap/index.ts typescript Next, call `swapPublic` on the L1 uniswap portal, passing both message proofs. The portal verifies both messages against the outbox, performs the mock swap, and deposits the output tokens back to L2: -#include_code consume_l1_messages_execute /docs/examples/ts/example_swap/index.ts typescript +#include_code consume_l1_messages_execute /docs/lines/v6/examples/ts/example_swap/index.ts typescript Finally, claim the output DAI on L2: -#include_code claim_output /docs/examples/ts/example_swap/index.ts typescript +#include_code claim_output /docs/lines/v6/examples/ts/example_swap/index.ts typescript ### Run It diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/01-architecture.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/01-architecture.md index bb5bf874ac2..f47b777c82b 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/01-architecture.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/01-architecture.md @@ -2,7 +2,7 @@ title: "1. Extension Architecture" description: Understanding Chrome extension architecture for Aztec wallets - service workers, offscreen documents, and message passing sidebar_position: 1 -references: ["docs/examples/webapp-tutorial/test-extension/src/content-script.ts", "docs/examples/webapp-tutorial/test-extension/src/background.ts", "docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts", "docs/examples/webapp-tutorial/test-extension/src/config.ts"] +references: ["docs/lines/v6/examples/webapp-tutorial/test-extension/src/content-script.ts", "docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts", "docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts", "docs/lines/v6/examples/webapp-tutorial/test-extension/src/config.ts"] --- # Extension Architecture @@ -49,7 +49,7 @@ Offscreen documents: The content script runs in the context of every web page. Its only job is to relay messages: -#include_code content-script docs/examples/webapp-tutorial/test-extension/src/content-script.ts typescript +#include_code content-script docs/lines/v6/examples/webapp-tutorial/test-extension/src/content-script.ts typescript It uses the `ContentScriptConnectionHandler` from the wallet SDK, which: - Listens for messages from the page (dApp) @@ -60,7 +60,7 @@ It uses the `ContentScriptConnectionHandler` from the wallet SDK, which: The service worker handles the wallet SDK protocol and coordinates between components: -#include_code offscreen-management docs/examples/webapp-tutorial/test-extension/src/background.ts typescript +#include_code offscreen-management docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts typescript Key responsibilities: - **Protocol handling** - Discovery, key exchange, session management @@ -72,7 +72,7 @@ Key responsibilities: The offscreen document is where the heavy lifting happens: -#include_code pxe-instance docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript +#include_code pxe-instance docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript It hosts: - **PXE instance** - The Private eXecution Environment @@ -162,7 +162,7 @@ Key points: Constants are centralized in `config.ts`: -#include_code wallet-config docs/examples/webapp-tutorial/test-extension/src/config.ts typescript +#include_code wallet-config docs/lines/v6/examples/webapp-tutorial/test-extension/src/config.ts typescript This keeps configuration in one place and provides typed message constants. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/02-wallet-protocol.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/02-wallet-protocol.md index 5de276c4e27..5c1925c1852 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/02-wallet-protocol.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/02-wallet-protocol.md @@ -2,7 +2,7 @@ title: "2. Wallet Protocol" description: Implementing the Aztec wallet SDK protocol - discovery, ECDH key exchange, and secure messaging sidebar_position: 2 -references: ["docs/examples/webapp-tutorial/test-extension/src/background.ts", "labs/yarn-project/wallet-sdk/src/*"] +references: ["docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts", "labs/yarn-project/wallet-sdk/src/*"] --- # Wallet Protocol @@ -46,7 +46,7 @@ chrome.runtime.sendMessage({ The background service worker handles it: -#include_code callbacks docs/examples/webapp-tutorial/test-extension/src/background.ts typescript +#include_code callbacks docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts typescript Key points: - Each discovery becomes a "pending discovery" awaiting user approval @@ -57,7 +57,7 @@ Key points: When the user clicks "Connect" in the popup: -#include_code popup-messages docs/examples/webapp-tutorial/test-extension/src/background.ts typescript +#include_code popup-messages docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts typescript The `handler.approveDiscovery()` call: 1. Marks the discovery as approved @@ -170,7 +170,7 @@ const needsApproval = For methods that don't need approval, the extension forwards directly to offscreen via a persistent port. The port uses `messageId`-based request/response correlation with a 5-minute timeout and automatic retry if the offscreen document is torn down by Chrome: -#include_code send-to-offscreen docs/examples/webapp-tutorial/test-extension/src/background.ts typescript +#include_code send-to-offscreen docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts typescript For methods that need approval, the extension stores them as pending and waits for user action: @@ -193,7 +193,7 @@ updateBadge(); The transport bridges Chrome's messaging APIs: -#include_code transport docs/examples/webapp-tutorial/test-extension/src/background.ts typescript +#include_code transport docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts typescript Key points: - `sendToTab` uses `chrome.tabs.sendMessage` to reach content scripts diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/03-pxe-integration.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/03-pxe-integration.md index 5a53dfd734b..73a79c3b392 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/03-pxe-integration.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/03-pxe-integration.md @@ -2,7 +2,7 @@ title: "3. PXE Integration" description: Running the Private eXecution Environment in a browser extension and extending BaseWallet sidebar_position: 3 -references: ["docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts"] +references: ["docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts"] --- # PXE Integration @@ -24,7 +24,7 @@ The PXE is stateful and long-running, which is why the extension uses an offscre The offscreen document initializes PXE lazily on first use with deduplication to prevent multiple initializations: -#include_code pxe-instance docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript +#include_code pxe-instance docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript Key configuration: - `rollupAddress` - The L1 rollup contract address, read from the node, that identifies the rollup instance the PXE tracks @@ -42,7 +42,7 @@ The extension has two wallet-related classes with distinct responsibilities: The `OffscreenWallet` extends `BaseWallet` from the wallet SDK: -#include_code wallet-instance docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript +#include_code wallet-instance docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript By extending `BaseWallet`, you inherit: - `sendTx()` - Transaction submission with proof generation @@ -62,7 +62,7 @@ You implement: The key override is `completeFeeOptions()`, which lazily registers the SponsoredFPC contract on first use: -#include_code complete-fee-options docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript +#include_code complete-fee-options docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript This ensures that by default: 1. If the payload already has a `feePayer` (e.g., during account deployment), the wallet respects it @@ -77,7 +77,7 @@ The `SponsoredFeePaymentMethod` creates an execution payload that: The offscreen document receives messages from the background via a persistent port. When the background connects with `chrome.runtime.connect({ name: 'offscreen' })`, the offscreen stores the port and listens for messages. Each message includes a `messageId` for request/response correlation: -#include_code message-handler docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript +#include_code message-handler docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript Each message type maps to a handler: - `INIT_PXE` - Ensures PXE is ready @@ -93,7 +93,7 @@ Each message type maps to a handler: For wallet methods from dApps, the handler dispatches based on method name. The handler uses `WalletSchema` to parse incoming JSON arguments back into proper Aztec types (`AztecAddress`, `Fr`, `ExecutionPayload`, etc.), and `jsonStringify` to serialize results before returning through Chrome messaging: -#include_code wallet-method-handler docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript +#include_code wallet-method-handler docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript This generic dispatch means any method on the `BaseWallet` interface (e.g., `getAccounts`, `sendTx`, `simulateTx`, `createAuthWit`, `getChainInfo`) is automatically available to dApps through the wallet SDK protocol. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md index c10b0e4bbf5..e808ace9702 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md @@ -2,7 +2,7 @@ title: "4. Account Management" description: Key derivation, encrypted storage, and SchnorrAccountContract for Aztec wallet accounts sidebar_position: 4 -references: ["docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts", "docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts", "docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts"] +references: ["docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts", "docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/storage.ts", "docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts"] --- # Account Management @@ -70,7 +70,7 @@ The "lazy" import defers loading the Noir artifact until needed. Account creation is handled by the `ExtensionWalletManager` utility class: -#include_code create-new-account docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts typescript +#include_code create-new-account docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts typescript Breaking this down: @@ -85,7 +85,7 @@ Note that PXE (Private eXecution Environment) registration happens separately - Keys are encrypted using PBKDF2 + AES-GCM with a non-extractable `CryptoKey`: -#include_code derive-master-key docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript +#include_code derive-master-key docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript Key derivation parameters: - **600,000 iterations** - OWASP 2023 recommendation for SHA-256, slows brute-force attacks @@ -95,7 +95,7 @@ Key derivation parameters: Encryption and decryption use the derived `CryptoKey` directly - the raw password string is never stored or passed around: -#include_code encrypt-decrypt docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript +#include_code encrypt-decrypt docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript Each account gets a unique random IV for AES-GCM. The stored data includes: - `encrypted` - The encrypted secret (base64) @@ -105,7 +105,7 @@ Each account gets a unique random IV for AES-GCM. The stored data includes: The offscreen document cannot access `chrome.storage` directly, so all storage operations are proxied through the background script via Chrome messaging: -#include_code account-operations docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript +#include_code account-operations docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript The stored account structure: @@ -124,7 +124,7 @@ interface StoredAccount { Instead of storing a password hash (vulnerable to rainbow tables), the wallet uses an encrypt-then-verify approach: derive a `CryptoKey` via PBKDF2 with a random salt, encrypt a known plaintext, and verify by attempting to decrypt it: -#include_code password-management docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript +#include_code password-management docs/lines/v6/examples/webapp-tutorial/test-extension/src/wallet/storage.ts typescript This provides: - `setupPassword()` - Derives a master key, encrypts a known plaintext for future verification, returns the `CryptoKey` @@ -137,7 +137,7 @@ The caller caches the `CryptoKey` in memory and discards the password string imm When the user unlocks the wallet, the extension verifies the password, derives the master `CryptoKey`, initializes PXE, and registers all stored accounts: -#include_code load-accounts docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript +#include_code load-accounts docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript Each account: 1. Has its secret decrypted using the master `CryptoKey` @@ -149,7 +149,7 @@ Each account: Accounts must be deployed before they can receive notes or initiate transactions: -#include_code deploy-account docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript +#include_code deploy-account docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript The deployment: 1. Gets the cached master `CryptoKey` (wallet must be unlocked) diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/05-transactions.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/05-transactions.md index 19fbd258790..d06c7492cb9 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/05-transactions.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/05-transactions.md @@ -2,7 +2,7 @@ title: "5. Transaction Handling" description: The sendTx flow, proof generation, and SponsoredFPC fee payment in Aztec wallet extensions sidebar_position: 5 -references: ["docs/examples/webapp-tutorial/test-extension/src/background.ts", "docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts"] +references: ["docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts", "docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts"] --- # Transaction Handling diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/06-approval-ui.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/06-approval-ui.md index 037e10705ee..9902774bd89 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/06-approval-ui.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/06-approval-ui.md @@ -2,7 +2,7 @@ title: "6. Approval UI" description: Building React popups for connection and transaction approval in Aztec wallet extensions sidebar_position: 6 -references: ["docs/examples/webapp-tutorial/test-extension/src/popup/*"] +references: ["docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/*"] --- # Approval UI @@ -50,7 +50,7 @@ The HTML loads the compiled JavaScript: The popup is split into focused components. The top-level `popup.tsx` acts as an orchestrator with a state machine that routes between views: setup, lock, main, create-account, approvals, session-verification, and settings. -#include_code main-app docs/examples/webapp-tutorial/test-extension/src/popup/popup.tsx typescript +#include_code main-app docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/popup.tsx typescript Key features: - **Persistent port** to background for real-time push updates (no polling) @@ -62,7 +62,7 @@ Key features: The popup sends messages to the background script: -#include_code send-message docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts typescript +#include_code send-message docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/helpers.ts typescript The popup targets the background explicitly with `target: MessageTarget.BACKGROUND` to distinguish from content script messages. @@ -70,7 +70,7 @@ The popup targets the background explicitly with `target: MessageTarget.BACKGROU The main page shows the active account and provides key actions: -#include_code main-page docs/examples/webapp-tutorial/test-extension/src/popup/MainScreen.tsx typescript +#include_code main-page docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/MainScreen.tsx typescript Features: - Active account with alias and truncated address @@ -82,7 +82,7 @@ Features: When a dApp requests connection, the wallet shows the approval UI: -#include_code connection-approval docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx typescript +#include_code connection-approval docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx typescript The approval shows: - Origin URL (the dApp's domain) @@ -95,7 +95,7 @@ The wallet also includes emoji verification for secure channel confirmation — Transaction approvals show more detail: -#include_code transaction-approval docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx typescript +#include_code transaction-approval docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx typescript The popup displays: - Origin and method type (sendTx, simulateTx, etc.) @@ -107,7 +107,7 @@ The popup displays: Utility for address truncation: -#include_code helpers docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts typescript +#include_code helpers docs/lines/v6/examples/webapp-tutorial/test-extension/src/popup/helpers.ts typescript ## Styling diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/07-testing.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/07-testing.md index 9f0b790adc6..72b2471305c 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/07-testing.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/07-testing.md @@ -2,7 +2,7 @@ title: "7. Testing" description: Loading the wallet extension in Chrome and testing with the Pod Racing dApp sidebar_position: 7 -references: ["docs/examples/webapp-tutorial/esbuild.extension.mjs", "docs/examples/webapp-tutorial/test-extension/*"] +references: ["docs/lines/v6/examples/webapp-tutorial/esbuild.extension.mjs", "docs/lines/v6/examples/webapp-tutorial/test-extension/*"] --- # Testing the Wallet Extension diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/index.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/index.md index f620673f228..f3dc9aead9f 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/index.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/wallet-extension/index.md @@ -2,7 +2,7 @@ title: Building a Wallet Extension description: Learn how to build a Chrome extension wallet for Aztec with encrypted key storage, SponsoredFPC fee payment, and transaction approval flows sidebar_position: 3 -references: ["docs/examples/webapp-tutorial/test-extension/*"] +references: ["docs/lines/v6/examples/webapp-tutorial/test-extension/*"] --- # Building a Wallet Extension for Aztec diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/01-the-contract.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/01-the-contract.md index b65daa0c058..d76d2d0a2f7 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/01-the-contract.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/01-the-contract.md @@ -2,7 +2,7 @@ title: "1. The Contract" sidebar_position: 1 description: "Walk through the Pod Racing smart contract, compile it, and deploy and interact with it via a TypeScript script" -references: ["docs/examples/webapp-tutorial/contracts/src/*", "docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts"] +references: ["docs/lines/v6/examples/webapp-tutorial/contracts/src/*", "docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts"] --- # The Contract @@ -63,7 +63,7 @@ Replace the tag with your Aztec version. Open `contracts/src/game_round_note.nr`. This is a private note that stores a player's point allocation for one round: -#include_code game-round-note /docs/examples/webapp-tutorial/contracts/src/game_round_note.nr rust +#include_code game-round-note /docs/lines/v6/examples/webapp-tutorial/contracts/src/game_round_note.nr rust The fields `track1` through `track5` store the points allocated to each track for that round. `round` identifies which round (1, 2, or 3) the note belongs to. `owner` is the player's address — only the owner's PXE (Private eXecution Environment) can decrypt and read this note. @@ -86,7 +86,7 @@ This means: Open `contracts/src/race.nr`. This is the public game state: -#include_code race /docs/examples/webapp-tutorial/contracts/src/race.nr rust +#include_code race /docs/lines/v6/examples/webapp-tutorial/contracts/src/race.nr rust The `Race` struct holds both player addresses, per-player round counters, the final aggregated scores for each track (filled in when a player calls `finish_game`), and a block deadline. The winner calculation compares track-by-track totals: whoever wins more of the 5 tracks wins the game. @@ -96,33 +96,33 @@ Open `contracts/src/main.nr`. This is the main contract file that defines the ga ### Storage -#include_code storage /docs/examples/webapp-tutorial/contracts/src/main.nr rust +#include_code storage /docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr rust The storage maps directly to the overview above: `admin` for the deployer, `races` for game state, `progress` for private round notes, and `win_history` for tracking wins. ### Creating and joining a game -#include_code create-game /docs/examples/webapp-tutorial/contracts/src/main.nr rust +#include_code create-game /docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr rust -#include_code join-game /docs/examples/webapp-tutorial/contracts/src/main.nr rust +#include_code join-game /docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr rust Both are public functions. `create_game` initializes a new `Race` with the caller as player 1 and sets a block deadline. `join_game` fills in player 2. ### Playing a round (private) -#include_code play-round /docs/examples/webapp-tutorial/contracts/src/main.nr rust +#include_code play-round /docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr rust This is a **private function** — the point allocation remains hidden from the opponent. It creates a `GameRoundNote`, then enqueues a public call to `validate_and_play_round` to increment the round counter without revealing points. ### Finishing the game (reveal) -#include_code finish-game /docs/examples/webapp-tutorial/contracts/src/main.nr rust +#include_code finish-game /docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr rust Another private function. It reads all your `GameRoundNote`s, sums up totals per track, and publishes the aggregated scores to public state. Your per-round choices stay hidden, but your final totals become visible. ### Finalizing the game -#include_code finalize-game /docs/examples/webapp-tutorial/contracts/src/main.nr rust +#include_code finalize-game /docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr rust A public function that compares both players' track totals, declares the winner (best of 5 tracks), and updates the win history. Can only be called after the game's block deadline has passed. @@ -145,27 +145,27 @@ Open `scripts/deploy-and-interact.ts`. It follows the same pattern as the [aztec ### Setup -#include_code script-setup /docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript +#include_code script-setup /docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript This uses `EmbeddedWallet` from `@aztec-labs/wallets/embedded` — a ready-made embedded wallet that handles PXE creation and account management. `getInitialTestAccountsData` provides the local network's genesis-funded test accounts, created here as initializerless Schnorr accounts. The `{ ephemeral: true }` option means PXE state is not persisted between runs. ### Deploy the contract -#include_code script-deploy /docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript +#include_code script-deploy /docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript ### Create and join a game -#include_code script-create-join /docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript +#include_code script-create-join /docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript ### Play rounds -#include_code script-play-rounds /docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript +#include_code script-play-rounds /docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript Each `play_round` call is a private transaction — the point allocations are encrypted as notes. Alice and Bob each play 3 rounds. ### Finish and finalize -#include_code script-finish-finalize /docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript +#include_code script-finish-finalize /docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts typescript Both players call `finish_game` to reveal their aggregated scores, then either player calls `finalize_game` to determine the winner. diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/02-project-setup.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/02-project-setup.md index ba7b75c3969..e7d26b35626 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/02-project-setup.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/02-project-setup.md @@ -2,7 +2,7 @@ title: "2. Project Setup" sidebar_position: 2 description: "Understand the webapp project structure, Vite configuration, and environment setup" -references: ["docs/examples/webapp-tutorial/vite.config.ts", "docs/examples/webapp-tutorial/package.json"] +references: ["docs/lines/v6/examples/webapp-tutorial/vite.config.ts", "docs/lines/v6/examples/webapp-tutorial/package.json"] --- # Project Setup @@ -45,7 +45,7 @@ webapp-tutorial/ Aztec uses WASM modules that require `SharedArrayBuffer`, which needs specific HTTP headers. Open `vite.config.ts`: -#include_code vite-config /docs/examples/webapp-tutorial/vite.config.ts typescript +#include_code vite-config /docs/lines/v6/examples/webapp-tutorial/vite.config.ts typescript Why each piece is needed: - **COOP/COEP headers**: Enable `SharedArrayBuffer` for multithreaded WASM (barretenberg proving) diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/03-network-and-wallet.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/03-network-and-wallet.md index 13f0852fad6..63697861228 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/03-network-and-wallet.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/03-network-and-wallet.md @@ -2,7 +2,7 @@ title: "3. Network & Wallet" sidebar_position: 3 description: "Connect to Aztec using an embedded wallet for local dev or the wallet SDK for browser extensions" -references: ["docs/examples/webapp-tutorial/src/config.ts", "docs/examples/webapp-tutorial/src/embedded-wallet.ts", "docs/examples/webapp-tutorial/src/wallet-connection.ts", "docs/examples/webapp-tutorial/src/fees.ts", "docs/examples/webapp-tutorial/src/components/*"] +references: ["docs/lines/v6/examples/webapp-tutorial/src/config.ts", "docs/lines/v6/examples/webapp-tutorial/src/embedded-wallet.ts", "docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts", "docs/lines/v6/examples/webapp-tutorial/src/fees.ts", "docs/lines/v6/examples/webapp-tutorial/src/components/*"] --- # Network & Wallet @@ -25,7 +25,7 @@ The relationship is straightforward: PXE handles everything private (notes, keys Open `src/config.ts`. This determines which Aztec node to connect to and provides a helper for creating an in-browser PXE: -#include_code config /docs/examples/webapp-tutorial/src/config.ts typescript +#include_code config /docs/lines/v6/examples/webapp-tutorial/src/config.ts typescript `createLocalPXE` sets up PXE in three steps: @@ -50,11 +50,11 @@ Open `src/embedded-wallet.ts`: ### Imports -#include_code embedded-wallet-imports /docs/examples/webapp-tutorial/src/embedded-wallet.ts typescript +#include_code embedded-wallet-imports /docs/lines/v6/examples/webapp-tutorial/src/embedded-wallet.ts typescript ### Initialization -#include_code initialize /docs/examples/webapp-tutorial/src/embedded-wallet.ts typescript +#include_code initialize /docs/lines/v6/examples/webapp-tutorial/src/embedded-wallet.ts typescript The `initialize` factory calls the inherited `create()` method, which sets up an in-browser PXE and account storage. It then registers the SponsoredFPC contract with PXE so that fee payment works out of the box. @@ -62,7 +62,7 @@ The `initialize` factory calls the inherited `create()` method, which sets up an The local network ships with test accounts that are funded at genesis. They are initializerless Schnorr accounts, so you can use them immediately without deploying an account contract. The inherited `createSchnorrInitializerlessAccount` registers the account with PXE and persists it in the wallet database. You select one by index (0, 1, 2, etc.). -#include_code connect-test-account /docs/examples/webapp-tutorial/src/embedded-wallet.ts typescript +#include_code connect-test-account /docs/lines/v6/examples/webapp-tutorial/src/embedded-wallet.ts typescript ### Fee payment @@ -72,13 +72,13 @@ Every Aztec transaction must pay a fee (similar to gas on Ethereum). Rather than SponsoredFPC only works on the local network. Production Aztec networks deployed to Ethereum mainnet require an alternative fee payment strategy. ::: -#include_code fee-options /docs/examples/webapp-tutorial/src/embedded-wallet.ts typescript +#include_code fee-options /docs/lines/v6/examples/webapp-tutorial/src/embedded-wallet.ts typescript ### Full class The complete `EmbeddedWallet` — most of the heavy lifting (simulation, proving, gas estimation) is inherited from the official wallet: -#include_code embedded-wallet-class /docs/examples/webapp-tutorial/src/embedded-wallet.ts typescript +#include_code embedded-wallet-class /docs/lines/v6/examples/webapp-tutorial/src/embedded-wallet.ts typescript ## Wallet SDK (browser extension) @@ -126,7 +126,7 @@ Your app needs to find which wallet extensions the user has installed. The SDK d The `discoverWallets` function starts this process and calls your `onUpdate` callback each time a new wallet extension responds. You use the resulting list to show users a "pick your wallet" UI. -#include_code discover-wallets /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript +#include_code discover-wallets /docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts typescript ### Step 2: Connect and verify @@ -138,7 +138,7 @@ To guard against man-in-the-middle attacks (where a malicious script intercepts After the user confirms the emojis match, calling `confirm()` completes the handshake and returns a `Wallet` instance connected to the extension. -#include_code connect-wallet /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript +#include_code connect-wallet /docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts typescript ## Fee payment helpers @@ -146,11 +146,11 @@ Both wallet modes need access to the SponsoredFPC contract. **SponsoredFPC** (Fe Open `src/fees.ts`: -#include_code get-sponsored-fpc /docs/examples/webapp-tutorial/src/fees.ts typescript +#include_code get-sponsored-fpc /docs/lines/v6/examples/webapp-tutorial/src/fees.ts typescript PXE needs the SponsoredFPC artifact registered so it can include fee payment logic when constructing transaction proofs. Without this registration, PXE wouldn't know how to interact with the fee contract: -#include_code register-fpc /docs/examples/webapp-tutorial/src/fees.ts typescript +#include_code register-fpc /docs/lines/v6/examples/webapp-tutorial/src/fees.ts typescript ## Components @@ -160,7 +160,7 @@ With the wallet logic in place, three UI components wire it together. Lets the user choose between "Local" (embedded wallet) and "Browser Wallet" (wallet extension). Calls `onNetworkChange` with the selected `NetworkType`, which determines which wallet mode the app uses. -#include_code network-picker /docs/examples/webapp-tutorial/src/components/NetworkPicker.tsx typescript +#include_code network-picker /docs/lines/v6/examples/webapp-tutorial/src/components/NetworkPicker.tsx typescript ### Wallet connect (`src/components/WalletConnect.tsx`) @@ -168,15 +168,15 @@ Handles both wallet modes in a single component. For local networks it shows the Open `src/components/WalletConnect.tsx`: -#include_code wallet-connect-imports /docs/examples/webapp-tutorial/src/components/WalletConnect.tsx typescript +#include_code wallet-connect-imports /docs/lines/v6/examples/webapp-tutorial/src/components/WalletConnect.tsx typescript -#include_code wallet-connect-component /docs/examples/webapp-tutorial/src/components/WalletConnect.tsx typescript +#include_code wallet-connect-component /docs/lines/v6/examples/webapp-tutorial/src/components/WalletConnect.tsx typescript ### Account info (`src/components/AccountInfo.tsx`) Displays the connected account's address. Takes the wallet as a prop. -#include_code account-info /docs/examples/webapp-tutorial/src/components/AccountInfo.tsx typescript +#include_code account-info /docs/lines/v6/examples/webapp-tutorial/src/components/AccountInfo.tsx typescript ## Next steps diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/04-contract-interaction.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/04-contract-interaction.md index 313cfec1b4c..8d563e71e89 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/04-contract-interaction.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/04-contract-interaction.md @@ -2,7 +2,7 @@ title: "4. Contract Interaction & Gameplay" sidebar_position: 4 description: "Deploy and call the Pod Racing contract from the webapp, handle game lobby and private gameplay" -references: ["docs/examples/webapp-tutorial/src/components/GameLobby.tsx", "docs/examples/webapp-tutorial/src/components/GameBoard.tsx", "docs/examples/webapp-tutorial/src/components/GameStatus.tsx"] +references: ["docs/lines/v6/examples/webapp-tutorial/src/components/GameLobby.tsx", "docs/lines/v6/examples/webapp-tutorial/src/components/GameBoard.tsx", "docs/lines/v6/examples/webapp-tutorial/src/components/GameStatus.tsx"] --- # Contract Interaction & Gameplay @@ -39,17 +39,17 @@ The lobby handles two-player coordination. Player 1 deploys a new contract and c Open `src/components/GameLobby.tsx`: -#include_code game-lobby-imports /docs/examples/webapp-tutorial/src/components/GameLobby.tsx typescript +#include_code game-lobby-imports /docs/lines/v6/examples/webapp-tutorial/src/components/GameLobby.tsx typescript ### Creating a game -#include_code handle-create /docs/examples/webapp-tutorial/src/components/GameLobby.tsx typescript +#include_code handle-create /docs/lines/v6/examples/webapp-tutorial/src/components/GameLobby.tsx typescript This deploys a fresh contract and creates a game in a single flow. The contract address is displayed so the creator can copy and share it with an opponent. ### Joining a game -#include_code handle-join /docs/examples/webapp-tutorial/src/components/GameLobby.tsx typescript +#include_code handle-join /docs/lines/v6/examples/webapp-tutorial/src/components/GameLobby.tsx typescript The opponent pastes the contract address and game ID to join. Under the hood, this calls `attachToContract` to register the contract with the joiner's PXE, then sends a `join_game` transaction. @@ -59,7 +59,7 @@ Open [`src/components/GameBoard.tsx`](https://github.com/aztec-labs-eng/aztec-no ### Submitting a round (private transaction) -#include_code submit-round /docs/examples/webapp-tutorial/src/components/GameBoard.tsx typescript +#include_code submit-round /docs/lines/v6/examples/webapp-tutorial/src/components/GameBoard.tsx typescript When the `playRound` helper sends the transaction (via `.send()`), the following happens under the hood: 1. PXE executes the private function locally, creating a `GameRoundNote` @@ -71,7 +71,7 @@ When the `playRound` helper sends the transaction (via `.send()`), the following After all 3 rounds, the game has two more phases: -#include_code finish-and-finalize /docs/examples/webapp-tutorial/src/components/GameBoard.tsx typescript +#include_code finish-and-finalize /docs/lines/v6/examples/webapp-tutorial/src/components/GameBoard.tsx typescript - **`finish_game`**: A private function that reads all your `GameRoundNote`s, sums up totals per track, and publishes the aggregated scores. This is the "reveal" — your per-round choices stay hidden, but your final totals become public. - **`finalize_game`**: A public function that compares both players' totals track by track and declares the winner (best of 5 tracks). Can only be called after the game's block deadline. @@ -82,7 +82,7 @@ Why two separate phases? Both players must call `finish_game` before anyone can Open `src/components/GameStatus.tsx`: -#include_code game-status-component /docs/examples/webapp-tutorial/src/components/GameStatus.tsx typescript +#include_code game-status-component /docs/lines/v6/examples/webapp-tutorial/src/components/GameStatus.tsx typescript ## Why opponents can't see your allocations diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/05-transactions-and-fees.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/05-transactions-and-fees.md index 3c0a0ff7c3c..d69267bfd44 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/05-transactions-and-fees.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/05-transactions-and-fees.md @@ -2,7 +2,7 @@ title: "5. Transactions & Fees" sidebar_position: 6 description: "Learn about the Aztec transaction lifecycle and fee payment with SponsoredFPC" -references: ["docs/examples/webapp-tutorial/src/components/TxStatus.tsx", "docs/examples/webapp-tutorial/src/fees.ts"] +references: ["docs/lines/v6/examples/webapp-tutorial/src/components/TxStatus.tsx", "docs/lines/v6/examples/webapp-tutorial/src/fees.ts"] --- # Transactions & Fees @@ -27,7 +27,7 @@ When you call `.send()` on a contract method, it handles the entire lifecycle au Open `src/components/TxStatus.tsx`: -#include_code tx-status-component /docs/examples/webapp-tutorial/src/components/TxStatus.tsx typescript +#include_code tx-status-component /docs/lines/v6/examples/webapp-tutorial/src/components/TxStatus.tsx typescript Integrate with contract calls: @@ -53,7 +53,7 @@ Every Aztec transaction requires fee payment, similar to gas on Ethereum. Withou ### How it works -#include_code get-sponsored-fpc /docs/examples/webapp-tutorial/src/fees.ts typescript +#include_code get-sponsored-fpc /docs/lines/v6/examples/webapp-tutorial/src/fees.ts typescript `SPONSORED_FPC_SALT` is a fixed constant so that the SponsoredFPC contract is deployed at a deterministic, well-known address across all networks (local network and beyond). Your app can always compute where it lives without querying a registry. @@ -61,13 +61,13 @@ Every Aztec transaction requires fee payment, similar to gas on Ethereum. Withou PXE needs the SponsoredFPC contract artifact registered so it can include fee payment logic when constructing transaction proofs. Without registration, PXE wouldn't know how to interact with the fee contract. -#include_code register-fpc /docs/examples/webapp-tutorial/src/fees.ts typescript +#include_code register-fpc /docs/lines/v6/examples/webapp-tutorial/src/fees.ts typescript ### Manual fee payment For explicit control: -#include_code create-fee-payment /docs/examples/webapp-tutorial/src/fees.ts typescript +#include_code create-fee-payment /docs/lines/v6/examples/webapp-tutorial/src/fees.ts typescript ```typescript const paymentMethod = await createSponsoredFeePayment(); diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/06-putting-it-together.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/06-putting-it-together.md index 0b18a392a37..617c381f5c1 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/06-putting-it-together.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/06-putting-it-together.md @@ -2,7 +2,7 @@ title: "6. Putting It Together" sidebar_position: 7 description: "Wire all components into a complete Pod Racing webapp and run it" -references: ["docs/examples/webapp-tutorial/src/main.tsx", "docs/examples/webapp-tutorial/src/App.tsx"] +references: ["docs/lines/v6/examples/webapp-tutorial/src/main.tsx", "docs/lines/v6/examples/webapp-tutorial/src/App.tsx"] --- # Putting It Together @@ -13,7 +13,7 @@ In this final section, you wire all the components together into a working app a Open `src/main.tsx`: -#include_code main /docs/examples/webapp-tutorial/src/main.tsx typescript +#include_code main /docs/lines/v6/examples/webapp-tutorial/src/main.tsx typescript ## App component @@ -25,11 +25,11 @@ Open `src/App.tsx`. The app is structured as a simple state machine with three p ### Imports -#include_code app-imports /docs/examples/webapp-tutorial/src/App.tsx typescript +#include_code app-imports /docs/lines/v6/examples/webapp-tutorial/src/App.tsx typescript ### State -#include_code app-state /docs/examples/webapp-tutorial/src/App.tsx typescript +#include_code app-state /docs/lines/v6/examples/webapp-tutorial/src/App.tsx typescript ### Event handlers @@ -38,13 +38,13 @@ Each handler transitions the app to the next phase: - `handleWalletConnected` — receives the wallet instance from the `WalletConnect` component. For embedded wallets it reads the connected account directly; for extension wallets it fetches the account list. Then it transitions from `connect` to `lobby`. - `handleGameJoined` — receives the typed contract instance and game ID from the `GameLobby` component, and transitions from `lobby` to `playing`. -#include_code app-handlers /docs/examples/webapp-tutorial/src/App.tsx typescript +#include_code app-handlers /docs/lines/v6/examples/webapp-tutorial/src/App.tsx typescript ### Render The render function conditionally displays components based on the current phase. Props flow downward: `network` goes to `WalletConnect`, `wallet` and `account` go to `GameLobby`, `contract`, `gameId`, and `currentRound` go to `GameBoard`. Each child component calls back to the parent via the event handlers above when its phase is complete. -#include_code app-render /docs/examples/webapp-tutorial/src/App.tsx typescript +#include_code app-render /docs/lines/v6/examples/webapp-tutorial/src/App.tsx typescript ## Running the app diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/index.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/index.md index c5c9f20eeec..bfc71b0f3f1 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/index.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/index.md @@ -2,7 +2,7 @@ title: "Building a Webapp on Aztec" sidebar_position: 0 description: "Build a Pod Racing game webapp with Vite, React, and Aztec — featuring private state, wallet connections, and zero-knowledge proofs." -references: ["docs/examples/webapp-tutorial/*"] +references: ["docs/lines/v6/examples/webapp-tutorial/*"] --- # Building a Webapp on Aztec diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/01-dapp-integration.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/01-dapp-integration.md index d9b5b4d483c..fbf158521e9 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/01-dapp-integration.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/01-dapp-integration.md @@ -2,7 +2,7 @@ title: "1. dApp Integration" sidebar_position: 1 description: "Connect your dApp to Aztec wallet extensions: discovery, secure channels, capabilities, and wallet usage" -references: ["docs/examples/webapp-tutorial/src/wallet-connection.ts", "docs/examples/webapp-tutorial/src/components/WalletConnect.tsx", "labs/yarn-project/wallet-sdk/src/*"] +references: ["docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts", "docs/lines/v6/examples/webapp-tutorial/src/components/WalletConnect.tsx", "labs/yarn-project/wallet-sdk/src/*"] --- # dApp integration @@ -30,7 +30,7 @@ import type { Wallet, AppCapabilities, GrantedAccountsCapability } from '@aztec- Wallet discovery broadcasts a request via `window.postMessage`. Any installed Aztec wallet extension that the user has approved responds with its info. -#include_code discover-wallets /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript +#include_code discover-wallets /docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts typescript `WalletManager.configure()` accepts options to filter extensions: @@ -63,7 +63,7 @@ Discovery requires user action in the wallet extension. The wallet won't reveal Once the user picks a wallet, establish an encrypted channel using ECDH key exchange: -#include_code connect-wallet /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript +#include_code connect-wallet /docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts typescript `establishSecureChannel()` performs the ECDH P-256 key exchange and returns a `PendingConnection` with: - `verificationHash`: hex string that both sides compute independently @@ -107,7 +107,7 @@ After connecting, your dApp should declare the permissions it needs using `reque The `AppCapabilities` manifest describes your dApp's identity and the permissions it requires. Each capability type maps to a set of wallet operations. For example, `accounts` lets you read account addresses, `transaction` lets you send transactions, and `simulation` lets you simulate without sending. The wallet may grant all, some, or none of the requested capabilities. -#include_code app-capabilities /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript +#include_code app-capabilities /docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts typescript ### Capability types @@ -212,13 +212,13 @@ const unsubscribe = provider.onDisconnect(handleUnexpectedDisconnect); Here's the full connection flow in a React component: -#include_code remote-connect /docs/examples/webapp-tutorial/src/components/WalletConnect.tsx typescript +#include_code remote-connect /docs/lines/v6/examples/webapp-tutorial/src/components/WalletConnect.tsx typescript ## State types For tracking wallet discovery state in your app: -#include_code wallet-sdk-types /docs/examples/webapp-tutorial/src/wallet-connection.ts typescript +#include_code wallet-sdk-types /docs/lines/v6/examples/webapp-tutorial/src/wallet-connection.ts typescript ## Next steps diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/02-wallet-integration.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/02-wallet-integration.md index d66e6b8e580..81dfc5e22b7 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/02-wallet-integration.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/webapp/wallet-sdk/02-wallet-integration.md @@ -2,7 +2,7 @@ title: "2. Wallet Extension Integration" sidebar_position: 2 description: "Build an Aztec wallet extension: handle discovery, manage sessions, route messages, and extend BaseWallet" -references: ["docs/examples/webapp-tutorial/test-extension/src/background.ts", "docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts", "labs/yarn-project/wallet-sdk/src/*"] +references: ["docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts", "docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts", "labs/yarn-project/wallet-sdk/src/*"] --- # Wallet extension integration @@ -60,7 +60,7 @@ The SDK provides four optional callbacks at different protocol stages. The tutor Called when a dApp broadcasts a discovery request. You decide whether to show an approval UI or auto-approve: -#include_code callbacks /docs/examples/webapp-tutorial/test-extension/src/background.ts typescript +#include_code callbacks /docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts typescript Key responsibilities: - **Stale session cleanup**: terminate sessions from the same tab (handles page refresh) @@ -81,13 +81,13 @@ Called after ECDH key exchange completes. The session has a `verificationHash` f Called when a dApp sends an encrypted wallet method call. Messages arriving before emoji verification are queued and flushed after the user confirms: -#include_code on-wallet-message /docs/examples/webapp-tutorial/test-extension/src/background.ts typescript +#include_code on-wallet-message /docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts typescript ## Message routing The core routing logic decides which methods need user approval: -#include_code approval-check /docs/examples/webapp-tutorial/test-extension/src/background.ts typescript +#include_code approval-check /docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts typescript The approval matrix: @@ -124,13 +124,13 @@ await handler.sendResponse(session.sessionId, { For auto-executing methods, forward to the offscreen document and return the result: -#include_code send-to-offscreen /docs/examples/webapp-tutorial/test-extension/src/background.ts typescript +#include_code send-to-offscreen /docs/lines/v6/examples/webapp-tutorial/test-extension/src/background.ts typescript ## Extending BaseWallet The offscreen document hosts your wallet implementation. Extend `BaseWallet` to get `sendTx`, `simulateTx`, `batch`, and other methods for free: -#include_code wallet-instance /docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript +#include_code wallet-instance /docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript ### What you must implement @@ -163,7 +163,7 @@ The offscreen document hosts your wallet implementation. Extend `BaseWallet` to The offscreen document handles all wallet methods dynamically using `WalletSchema` for type-safe argument parsing: -#include_code wallet-method-handler /docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript +#include_code wallet-method-handler /docs/lines/v6/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts typescript ## Session lifecycle From ce4975974b835dd0f89f14793e2dff8e2dddef3c Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 16:54:12 +0000 Subject: [PATCH 15/32] feat(docs): serve the v6 line in previews Previews serve docs/lines/v6 at /developers-v6 and /operate-v6 as two noindex docs instances; production builds neither. Line pages keep their links on the line, hide the version dropdown and link the API reference of the network that runs v6. Search, llms.txt and the sitemap leave them out. bootstrap.sh checks each line's references and builds its examples. --- docs/bootstrap.sh | 23 ++++++++- docs/docs-developers/docs/aztec-nr/api.mdx | 10 +++- docs/docusaurus.config.js | 21 ++++++++ .../v6/docs-developers/docs/aztec-nr/api.mdx | 10 +++- docs/package.json | 1 + docs/scripts/aztec_node.mts | 5 ++ docs/scripts/netlify-build.sh | 2 + docs/src/__tests__/network_docs.test.js | 20 ++++++++ docs/src/__tests__/pagefind_pages.test.js | 3 ++ docs/src/components/PopularQuestions/index.js | 16 +++--- docs/src/components/RolePicker/index.js | 12 +++-- docs/src/network_docs.js | 49 +++++++++++++++++++ docs/src/plugins/pagefind/pages.js | 2 + docs/src/preprocess/include_version.js | 13 +---- .../UnifiedVersionDropdown/index.js | 8 ++- docs/src/utils/aztecNrApi.mjs | 10 +++- docs/src/utils/docsLine.mjs | 8 +++ docs/src/utils/useDocsBase.js | 14 ++++++ 18 files changed, 195 insertions(+), 32 deletions(-) create mode 100644 docs/src/utils/docsLine.mjs create mode 100644 docs/src/utils/useDocsBase.js diff --git a/docs/bootstrap.sh b/docs/bootstrap.sh index 8754e7b7268..84fba747683 100755 --- a/docs/bootstrap.sh +++ b/docs/bootstrap.sh @@ -9,7 +9,7 @@ repo_root=$(git rev-parse --show-toplevel) hash=$( cache_content_hash \ .rebuild_patterns \ - $(find docs docs-developers docs-operate docs-participate developer_versioned_docs network_versioned_docs -type f -name "*.md*" -exec grep '^#include_code' {} \; | \ + $(find docs docs-developers docs-operate docs-participate developer_versioned_docs network_versioned_docs $([ -d lines ] && echo lines) -type f -name "*.md*" -exec grep '^#include_code' {} \; | \ awk '{ gsub("^/", "", $3); if ($3 !~ /^labs\//) print "^" $3 }' | sort -u) ) # A release build renders its own versions and links this repository's files at DOCS_COMMIT, so it @@ -20,10 +20,15 @@ hash=$(hash_str "$hash" "$(git -C "$repo_root" rev-parse HEAD:docs)" \ # Every function that reads aztec-node code fetches and checks the pinned checkout first, on a cache # hit too, since later steps read it (scripts/aztec_node.mts; a no-op without network when current). +# The fetch covers every dev line (docs/lines//), each with its own pin. function fetch_aztec_node { node scripts/aztec_node.mts fetch } +function lines { + node scripts/aztec_node.mts lines +} + function build_docs { if [ "${CI:-0}" -eq 1 ] && [ $(arch) == arm64 ]; then echo "Not building docs for arm64 in CI." @@ -48,6 +53,9 @@ function check_generated_refs { echo_header "check generated aztec.js reference" fetch_aztec_node ./scripts/aztecjs_reference_generation/update_docs.sh --check + for line in $(lines); do + ./scripts/aztecjs_reference_generation/update_docs.sh --line "$line" --check + done } function test_cmds { @@ -137,6 +145,9 @@ function check { ./scripts/check_warnings.sh --self-test ./scripts/check_cli_defaults.sh ./scripts/check_doc_references.sh docs + for line in $(lines); do + ./scripts/check_doc_references.sh "docs/lines/$line" + done } function build_examples { @@ -144,9 +155,18 @@ function build_examples { (cd examples && ./bootstrap.sh "$@") } +# A dev line's examples compile and type-check here; they run against a network only at the line's cut. +function build_line_examples { + for line in $(lines); do + echo_header "Building $line examples" + (cd examples && ./bootstrap.sh --line "$line") + done +} + case "$cmd" in "ci") build_examples + build_line_examples build_docs check_generated_refs test @@ -155,6 +175,7 @@ case "$cmd" in ;; "") build_examples + build_line_examples build_docs check_generated_refs check_orphaned_urls diff --git a/docs/docs-developers/docs/aztec-nr/api.mdx b/docs/docs-developers/docs/aztec-nr/api.mdx index d31bd9e0b0f..ca4da211c0a 100644 --- a/docs/docs-developers/docs/aztec-nr/api.mdx +++ b/docs/docs-developers/docs/aztec-nr/api.mdx @@ -6,13 +6,19 @@ aztec_nr_api_version: next --- import Link from "@docusaurus/Link"; -import { useActiveVersion } from "@docusaurus/plugin-content-docs/client"; +import { useActivePluginAndVersion } from "@docusaurus/plugin-content-docs/client"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import { aztecNrApiFolder } from "@site/src/utils/aztecNrApi.mjs"; export const useApiVersion = () => { const { siteConfig } = useDocusaurusContext(); - return aztecNrApiFolder(useActiveVersion("developer"), siteConfig.customFields.aztecNrApiFolders); + const { activePlugin, activeVersion } = useActivePluginAndVersion() ?? {}; + return aztecNrApiFolder( + activeVersion, + siteConfig.customFields.aztecNrApiFolders, + activePlugin?.pluginId, + siteConfig.customFields.lineApiFolders, + ); }; export const ModuleLink = ({ path, children }) => { diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 7b235941f8a..fd54df988c0 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -22,9 +22,12 @@ const { aztecNrApiFolders, docsInstances, historicalVersions, + lineApiFolders, + lineInstances, llmsRoutes, syncVersionsFile, } = require("./src/network_docs.js"); +const { lineNames } = require("./src/preprocess/line.js"); const { versionLabel } = require("./src/release_names"); // Version config files are the source of truth for type→version mappings. @@ -42,6 +45,8 @@ const [developerDocs, operatorDocs] = docsInstances( process.env.CONTEXT === "production" ); const developerApiFolders = aztecNrApiFolders(developerDocs); +// The dev lines (docs/lines//) serve in previews only, each beside the dev docs. +const lines = lineInstances(lineNames(path.join(__dirname, "..")), process.env.CONTEXT === "production"); const NETWORK_LABELS = { mainnet: "Alpha", testnet: "Testnet" }; // Docs versions track release lines, so each label carries the line name and @@ -374,6 +379,20 @@ const config = { rehypePlugins, }, ], + ...lines.map(({ line, id, source, routeBasePath, sidebar }) => [ + "@docusaurus/plugin-content-docs", + { + id, + path: `processed-docs/${source}`, + routeBasePath, + sidebarPath: `./${sidebar}`, + editUrl: (params) => + `https://github.com/AztecProtocol/aztec-packages/edit/next/docs/${source}/` + params.docPath, + versions: { current: { label: `${line} dev`, noIndex: true, banner: "unreleased" } }, + remarkPlugins, + rehypePlugins, + }, + ]), // Participate docs instance - NOT versioned (educational content) [ "@docusaurus/plugin-content-docs", @@ -456,6 +475,7 @@ const config = { "/aztec-nr-api/**", "/typescript-api/**", ...llmsInstances.flatMap((l) => l.excludeRoutes), + ...lines.flatMap(({ routeBasePath }) => [`/${routeBasePath}`, `/${routeBasePath}/**`]), ], }, }, @@ -484,6 +504,7 @@ const config = { websiteId: WEBSITE_ID, // Read by the Aztec.nr API page (src/utils/aztecNrApi.mjs); labels carry names, not networks. aztecNrApiFolders: developerApiFolders, + lineApiFolders: lineApiFolders(developerVersionConfig, [...new Set(lines.map(({ line }) => line))]), }, themeConfig: /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ diff --git a/docs/lines/v6/docs-developers/docs/aztec-nr/api.mdx b/docs/lines/v6/docs-developers/docs/aztec-nr/api.mdx index d31bd9e0b0f..ca4da211c0a 100644 --- a/docs/lines/v6/docs-developers/docs/aztec-nr/api.mdx +++ b/docs/lines/v6/docs-developers/docs/aztec-nr/api.mdx @@ -6,13 +6,19 @@ aztec_nr_api_version: next --- import Link from "@docusaurus/Link"; -import { useActiveVersion } from "@docusaurus/plugin-content-docs/client"; +import { useActivePluginAndVersion } from "@docusaurus/plugin-content-docs/client"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import { aztecNrApiFolder } from "@site/src/utils/aztecNrApi.mjs"; export const useApiVersion = () => { const { siteConfig } = useDocusaurusContext(); - return aztecNrApiFolder(useActiveVersion("developer"), siteConfig.customFields.aztecNrApiFolders); + const { activePlugin, activeVersion } = useActivePluginAndVersion() ?? {}; + return aztecNrApiFolder( + activeVersion, + siteConfig.customFields.aztecNrApiFolders, + activePlugin?.pluginId, + siteConfig.customFields.lineApiFolders, + ); }; export const ModuleLink = ({ path, children }) => { diff --git a/docs/package.json b/docs/package.json index e05ed6b913f..29019b4e5f4 100644 --- a/docs/package.json +++ b/docs/package.json @@ -82,6 +82,7 @@ "./docs-developers/**/{*.md,*.md?}", "./docs-operate/**/{*.md,*.md?}", "./docs-participate/**/{*.md,*.md?}", + "./lines/*/docs-*/**/{*.md,*.md?}", "./developer_versioned_docs/**/{*.md,*.md?}", "./network_versioned_docs/**/{*.md,*.md?}", "./internal_notes/**/{*.md,*.md?}", diff --git a/docs/scripts/aztec_node.mts b/docs/scripts/aztec_node.mts index 38423afe70b..7e7af3cc2e8 100644 --- a/docs/scripts/aztec_node.mts +++ b/docs/scripts/aztec_node.mts @@ -8,6 +8,7 @@ // node scripts/aztec_node.mts [--line ] pin move the pin to an aztec-node release or nightly tag (needs gh) // node scripts/aztec_node.mts [--line ] provision ... install the pinned published toolchain (l1-artifacts: a line only) // node scripts/aztec_node.mts [--line ] env print the pin as shell exports, for eval +// node scripts/aztec_node.mts lines print the line names, one per line // // .mts because docs/package.json declares no module type: a .ts file with ESM syntax makes every run // print MODULE_TYPELESS_PACKAGE_JSON. @@ -566,6 +567,10 @@ async function main(argv: string[]): Promise { return found.length ? 1 : 0; }; switch (cmd) { + case "lines": + if (o.line || args.length) break; + for (const line of lineNames(docs)) console.log(line); + return 0; case "fetch": return report(await fetchPinned(o, log)); case "check": diff --git a/docs/scripts/netlify-build.sh b/docs/scripts/netlify-build.sh index a25a8524306..61d5b09a9df 100755 --- a/docs/scripts/netlify-build.sh +++ b/docs/scripts/netlify-build.sh @@ -3,6 +3,8 @@ # docs as CI builds them. Previews also get the aztec-nr API reference generated with the nargo # release the pinned aztec-node uses; production does not, as it serves only the committed released # references (static/aztec-nr-api/) and drops the dev docs that link to the generated one. +# Previews also serve each dev line (docs/lines//) at /developers- and /operate-, +# which link a network's released reference; production fetches and builds no line. set -euo pipefail docs=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) diff --git a/docs/src/__tests__/network_docs.test.js b/docs/src/__tests__/network_docs.test.js index 7a39494c24b..738f34d7092 100644 --- a/docs/src/__tests__/network_docs.test.js +++ b/docs/src/__tests__/network_docs.test.js @@ -8,6 +8,7 @@ const { docsInstances, historicalVersions, lineApiFolder, + lineInstances, llmsRoutes, networkSection, startHere, @@ -106,6 +107,25 @@ describe("Aztec.nr API folders", () => { }); }); +describe("dev lines", () => { + it("serve each line's developer and operator pages under their own base outside production only", () => { + assert.deepStrictEqual(lineInstances(["v6"], true), []); + assert.deepStrictEqual(lineInstances(["v6"], false), [ + { line: "v6", id: "developer-v6", source: "lines/v6/docs-developers", routeBasePath: "developers-v6", sidebar: "lines/v6/sidebars-developer.js" }, + { line: "v6", id: "network-v6", source: "lines/v6/docs-operate", routeBasePath: "operate-v6", sidebar: "lines/v6/sidebars-operate.js" }, + ]); + }); + + it("send a line's API page to its line's folder, and every other page where it went before", () => { + const folders = { "v6.0.0": "mainnet" }; + const lines = { v6: "mainnet" }; + assert.strictEqual(aztecNrApiFolder({ name: "current" }, folders, "developer-v6", lines), "mainnet"); + assert.strictEqual(aztecNrApiFolder({ name: "current" }, folders, "developer", lines), "next"); + assert.strictEqual(aztecNrApiFolder({ name: "v6.0.0" }, folders, "developer", lines), "mainnet"); + assert.strictEqual(aztecNrApiFolder({ name: "v6.0.0" }, folders), "mainnet"); + }); +}); + describe("Start here", () => { const urls = (production, siteUrl) => Object.fromEntries(startHereLinks(docsInstances(shared, shared, production), siteUrl).map((l) => [l.title, l.url])); diff --git a/docs/src/__tests__/pagefind_pages.test.js b/docs/src/__tests__/pagefind_pages.test.js index fb424aab550..bc3c6aade80 100644 --- a/docs/src/__tests__/pagefind_pages.test.js +++ b/docs/src/__tests__/pagefind_pages.test.js @@ -42,6 +42,9 @@ describe("classifyPage", () => { assert.strictEqual(classifyPage("developers/tags.html", tagged, API_DIRS), null); assert.strictEqual(classifyPage("developers/v5.2.0/tags/aztec-js.html", tagged, API_DIRS), null); assert.strictEqual(classifyPage("404.html", docsPage("docs-root-current"), API_DIRS), null); + // Dev line previews (/developers-v6, /operate-v6) duplicate the dev docs. + assert.strictEqual(classifyPage("developers-v6/docs/aztec-js.html", docsPage("docs-developer-v6-current"), API_DIRS), null); + assert.strictEqual(classifyPage("operate-v6.html", docsPage("docs-network-v6-current"), API_DIRS), null); assert.strictEqual(classifyPage("search.html", docsPage("default"), API_DIRS), null); assert.strictEqual(classifyPage("google0123.html", "google-site-verification: google0123.html", API_DIRS), null); }); diff --git a/docs/src/components/PopularQuestions/index.js b/docs/src/components/PopularQuestions/index.js index 21b515fc35f..cbdbe9e7647 100644 --- a/docs/src/components/PopularQuestions/index.js +++ b/docs/src/components/PopularQuestions/index.js @@ -1,5 +1,6 @@ import React from "react"; import Link from "@docusaurus/Link"; +import useDocsBase from "@site/src/utils/useDocsBase"; import styles from "./styles.module.css"; const QUESTIONS = [ @@ -7,47 +8,48 @@ const QUESTIONS = [ id: "identity-model", title: "Identity model", subText: "Which keys earn rewards, which are slashable, which can be rotated", - href: "/operate/operators/concepts/identity-model", + href: "operators/concepts/identity-model", }, { id: "l1-rpc", title: "L1 RPC requirements", subText: "Supernode flags, hosted vs self-hosted, polling tuning", - href: "/operate/operators/concepts/l1-rpc", + href: "operators/concepts/l1-rpc", }, { id: "hardware", title: "Hardware spec", subText: "Per-role minimums plus the operator-community gotchas", - href: "/operate/operators/concepts/hardware", + href: "operators/concepts/hardware", }, { id: "claiming-rewards", title: "Claiming rewards", subText: "Where rewards live, when they show up, the multi-rollup case", - href: "/operate/operators/concepts/claiming-rewards", + href: "operators/concepts/claiming-rewards", }, { id: "slashing", title: "Slashing", subText: "What gets you slashed, thresholds, the veto council, recovery", - href: "/operate/operators/concepts/slashing", + href: "operators/concepts/slashing", }, { id: "monitoring", title: "Monitoring and metrics", subText: "The five metrics that matter and what to alert on", - href: "/operate/operators/concepts/monitoring", + href: "operators/concepts/monitoring", }, ]; export default function PopularQuestions() { + const base = useDocsBase("operate"); return (
{QUESTIONS.map((q) => ( diff --git a/docs/src/components/RolePicker/index.js b/docs/src/components/RolePicker/index.js index 8a2637d637b..73453e867c3 100644 --- a/docs/src/components/RolePicker/index.js +++ b/docs/src/components/RolePicker/index.js @@ -1,5 +1,6 @@ import React from "react"; import Link from "@docusaurus/Link"; +import useDocsBase from "@site/src/utils/useDocsBase"; import styles from "./styles.module.css"; const svgProps = { @@ -73,7 +74,7 @@ const ROLES = [ "No on-chain registration required", ], cta: "Read setup guide", - href: "/operate/operators/full-node/overview", + href: "operators/full-node/overview", }, { id: "solo-sequencer", @@ -94,7 +95,7 @@ const ROLES = [ "Personalised commands: paste keys / RPCs once, every step auto-fills", ], cta: "Start guided setup", - href: "/operate/operators/solo-sequencer/", + href: "operators/solo-sequencer/", }, { id: "provider", @@ -115,7 +116,7 @@ const ROLES = [ "Adds key-management and commission-tooling complexity", ], cta: "Start guided setup", - href: "/operate/operators/provider/", + href: "operators/provider/", }, { id: "prover", @@ -136,7 +137,7 @@ const ROLES = [ "Earnings depend on epoch activity, gas, and prover-pool competition", ], cta: "Read setup guide", - href: "/operate/operators/prover/overview", + href: "operators/prover/overview", }, ]; @@ -145,12 +146,13 @@ function badgeClass(kind) { } export default function RolePicker() { + const base = useDocsBase("operate"); return (
{ROLES.map((role) => ( diff --git a/docs/src/network_docs.js b/docs/src/network_docs.js index 37d673b92b7..f54342c0a95 100644 --- a/docs/src/network_docs.js +++ b/docs/src/network_docs.js @@ -78,6 +78,32 @@ const aztecNrApiFolders = (instance) => .map((v) => [v.version, v.networks[0]]) ); +/** + * The docs instances of the dev lines (docs/lines//), built outside production only: each + * line's developer and operator pages at /developers- and /operate-, as the docs plugins + * `developer-` and `network-` (src/utils/docsLine.mjs reads that id form). + * + * @param {string[]} lines the line names, like `v6` + * @param {boolean} production + * @returns {{line: string, id: string, source: string, routeBasePath: string, sidebar: string}[]} + * `source` and `sidebar` are paths under docs/ + */ +function lineInstances(lines, production) { + if (production) return []; + return lines.flatMap((line) => + [ + ["developer", "docs-developers", "developers", "sidebars-developer.js"], + ["network", "docs-operate", "operate", "sidebars-operate.js"], + ].map(([plugin, source, base, sidebar]) => ({ + line, + id: `${plugin}-${line}`, + source: `lines/${line}/${source}`, + routeBasePath: `${base}-${line}`, + sidebar: `lines/${line}/${sidebar}`, + })), + ); +} + /** * The Aztec.nr API reference folder a dev line's pages link: the folder of the docs version a * network reads with the line's major. @@ -92,6 +118,27 @@ function lineApiFolder(developerConfig, major) { return version && folders[version]; } +/** + * Each line's Aztec.nr API reference folder (`lineApiFolder`). Throws for a line no network runs: + * another major's reference documents a different API. + * + * @param {object} developerConfig developer_version_config.json + * @param {string[]} lines the line names, like `v6` + * @returns {Record} line → folder + */ +function lineApiFolders(developerConfig, lines) { + return Object.fromEntries( + lines.map((line) => { + const folder = lineApiFolder(developerConfig, Number(line.slice(1))); + if (!folder) + throw new Error( + `no network runs ${line}, so line ${line} has no published API reference: retire the line (git rm -r docs/lines/${line}) or keep a reference for it`, + ); + return [line, folder]; + }), + ); +} + /** * The API reference folders of each docs version a network reads, one per kind, in the folder * aztecNrApiFolders names (the TypeScript reference is published per network the same way). @@ -286,6 +333,8 @@ module.exports = { docsInstances, historicalVersions, lineApiFolder, + lineApiFolders, + lineInstances, llmsRoutes, networkSection, startHere, diff --git a/docs/src/plugins/pagefind/pages.js b/docs/src/plugins/pagefind/pages.js index 8c3d60e46d0..a79bb284798 100644 --- a/docs/src/plugins/pagefind/pages.js +++ b/docs/src/plugins/pagefind/pages.js @@ -43,6 +43,8 @@ function classifyPage(rel, html, apiDirs) { return { tag: "default", body: "main", weight: API_WEIGHT }; } if (rel === "404.html" || /(^|\/)tags(\/|\.html$)/.test(rel)) return null; + // A dev line's preview pages (/developers-v6, /operate-v6) repeat the dev docs. + if (/^(developers|operate)-v\d+(\/|\.html$)/.test(rel)) return null; const tag = metaContent(html, "docusaurus_tag"); return tag?.startsWith("docs-") ? { tag, body: "article" } : null; } diff --git a/docs/src/preprocess/include_version.js b/docs/src/preprocess/include_version.js index 2a4680be7ef..109df9568de 100644 --- a/docs/src/preprocess/include_version.js +++ b/docs/src/preprocess/include_version.js @@ -2,7 +2,7 @@ const fs = require("fs"); const path = require("path"); const { processConditionalBlocks } = require("./conditional_content"); const { releaseNameFor, versionLabel } = require("../release_names"); -const { lineApiFolder } = require("../network_docs"); +const { lineApiFolders } = require("../network_docs"); const { lineOf, readPin } = require("./line"); // Load version defaults from config file (source of truth for version→type mapping) @@ -79,16 +79,7 @@ function foundationVersion() { return `v${JSON.parse(fs.readFileSync(pkg, "utf8")).version}`; } -// A line's API links read the reference a network on its major publishes; another major's would -// document a different API. -function lineApiRefVersion(line) { - const folder = lineApiFolder(developerVersionConfig, line.major); - if (!folder) - throw new Error( - `no network runs v${line.major}, so line ${line.name} has no published API reference: retire the line (git rm -r docs/lines/${line.name}) or keep a reference for it`, - ); - return folder; -} +const lineApiRefVersion = (line) => lineApiFolders(developerVersionConfig, [line.name])[line.name]; function pinnedAztecNodeRef() { return JSON.parse(fs.readFileSync(path.join(__dirname, "../../aztec-node.json"), "utf8")).ref; diff --git a/docs/src/theme/NavbarItem/UnifiedVersionDropdown/index.js b/docs/src/theme/NavbarItem/UnifiedVersionDropdown/index.js index 3c40e8dbe17..019f94f5754 100644 --- a/docs/src/theme/NavbarItem/UnifiedVersionDropdown/index.js +++ b/docs/src/theme/NavbarItem/UnifiedVersionDropdown/index.js @@ -1,6 +1,7 @@ import React from 'react'; import { useLocation } from '@docusaurus/router'; import { + useActivePlugin, useVersions, useActiveDocContext, useDocsPreferredVersion, @@ -9,6 +10,7 @@ import { import { useHistorySelector } from '@docusaurus/theme-common'; import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'; import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; +import { lineOfPlugin } from '@site/src/utils/docsLine.mjs'; // Plugin IDs for our two docs instances const DEVELOPER_PLUGIN_ID = 'developer'; @@ -88,9 +90,11 @@ function createSectionHeader(label) { export default function UnifiedVersionDropdown({ mobile, ...props }) { const { pathname } = useLocation(); + const line = lineOfPlugin(useActivePlugin()?.pluginId); - // Hide dropdown on landing page, networks, and participate section (participate is not versioned) - if (pathname === '/' || pathname === '/networks' || pathname.startsWith('/participate')) { + // Hide dropdown on landing page, networks, participate section (participate is not versioned) and + // a dev line's pages, which have one version and would otherwise get the menus of both sections + if (line || pathname === '/' || pathname === '/networks' || pathname.startsWith('/participate')) { return null; } diff --git a/docs/src/utils/aztecNrApi.mjs b/docs/src/utils/aztecNrApi.mjs index 98757ff447c..f8643a620b3 100644 --- a/docs/src/utils/aztecNrApi.mjs +++ b/docs/src/utils/aztecNrApi.mjs @@ -1,12 +1,18 @@ +import { lineOfPlugin } from "./docsLine.mjs"; + /** * The `static/aztec-nr-api/` folder a developer docs version's API links point into. * - * @param {{name: string} | undefined} version the active docs version (`useActiveVersion("developer")`) + * @param {{name: string} | undefined} version the active docs version * @param {Record} folders `siteConfig.customFields.aztecNrApiFolders`: version → folder * for every version a network reads. Versions no network reads keep their own name. + * @param {string} [pluginId] the active docs plugin; a dev line's reads `lineFolders` + * @param {Record} [lineFolders] `siteConfig.customFields.lineApiFolders`: line → folder * @returns {string} */ -export function aztecNrApiFolder(version, folders) { +export function aztecNrApiFolder(version, folders, pluginId, lineFolders = {}) { + const line = lineOfPlugin(pluginId); + if (line) return lineFolders[line]; if (!version || version.name === "current") return "next"; return folders[version.name] ?? version.name; } diff --git a/docs/src/utils/docsLine.mjs b/docs/src/utils/docsLine.mjs new file mode 100644 index 00000000000..8bdd0b6f732 --- /dev/null +++ b/docs/src/utils/docsLine.mjs @@ -0,0 +1,8 @@ +/** + * The dev line a docs plugin serves, from its id (`developer-` or `network-`, as + * `lineInstances` in src/network_docs.js names them). + * + * @param {string | undefined} pluginId + * @returns {string | undefined} the line, like `v6`; undefined for every other plugin + */ +export const lineOfPlugin = (pluginId) => /^(?:developer|network)-(v\d+)$/.exec(pluginId ?? "")?.[1]; diff --git a/docs/src/utils/useDocsBase.js b/docs/src/utils/useDocsBase.js new file mode 100644 index 00000000000..286ea4382e4 --- /dev/null +++ b/docs/src/utils/useDocsBase.js @@ -0,0 +1,14 @@ +import { useActivePlugin } from "@docusaurus/plugin-content-docs/client"; +import { lineOfPlugin } from "./docsLine.mjs"; + +/** + * The route base of a docs section as the current page should link it: `/-` on a dev + * line's page, so its links stay on that line, else `/`. + * + * @param {"developers" | "operate"} base + * @returns {string} + */ +export default function useDocsBase(base) { + const line = lineOfPlugin(useActivePlugin()?.pluginId); + return line ? `/${base}-${line}` : `/${base}`; +} From 5c76f3da81abb5815951058962abcfc141995234 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 17:35:12 +0000 Subject: [PATCH 16/32] fix(docs): keep a line's navbar on the line, and lines out of production checks On a line's pages the navbar's Build, Run a Node and Resources links now lead to the line's instances instead of the dev docs. Production no longer spellchecks line pages, and `aztec_node.mts lines` lists none there, so bootstrap's line steps skip in production like fetch does. --- docs/package.json | 1 - docs/scripts/aztec_node.mts | 10 ++++++---- docs/src/__tests__/network_docs.test.js | 8 ++++++++ docs/src/theme/NavbarItem/ComponentTypes.js | 21 +++++++++++++++++++++ docs/src/utils/docsLine.mjs | 10 ++++++++++ docs/src/utils/useDocsBase.js | 4 ++-- 6 files changed, 47 insertions(+), 7 deletions(-) diff --git a/docs/package.json b/docs/package.json index 29019b4e5f4..e05ed6b913f 100644 --- a/docs/package.json +++ b/docs/package.json @@ -82,7 +82,6 @@ "./docs-developers/**/{*.md,*.md?}", "./docs-operate/**/{*.md,*.md?}", "./docs-participate/**/{*.md,*.md?}", - "./lines/*/docs-*/**/{*.md,*.md?}", "./developer_versioned_docs/**/{*.md,*.md?}", "./network_versioned_docs/**/{*.md,*.md?}", "./internal_notes/**/{*.md,*.md?}", diff --git a/docs/scripts/aztec_node.mts b/docs/scripts/aztec_node.mts index 7e7af3cc2e8..a3ccccdfd3a 100644 --- a/docs/scripts/aztec_node.mts +++ b/docs/scripts/aztec_node.mts @@ -8,7 +8,7 @@ // node scripts/aztec_node.mts [--line ] pin move the pin to an aztec-node release or nightly tag (needs gh) // node scripts/aztec_node.mts [--line ] provision ... install the pinned published toolchain (l1-artifacts: a line only) // node scripts/aztec_node.mts [--line ] env print the pin as shell exports, for eval -// node scripts/aztec_node.mts lines print the line names, one per line +// node scripts/aztec_node.mts lines print the line names, one per line; none in production // // .mts because docs/package.json declares no module type: a .ts file with ESM syntax makes every run // print MODULE_TYPELESS_PACKAGE_JSON. @@ -81,6 +81,9 @@ export function lineNames(o: Options): string[] { return fs.readdirSync(dir).filter((l) => LINE.test(l) && fs.existsSync(path.join(dir, l, "aztec-node.json"))).sort(); } +/** The lines this build serves: none in production, which never reads a line. */ +export const builtLines = (o: Options): string[] => (process.env.CONTEXT === "production" ? [] : lineNames(o)); + export const lineOptions = (o: Options, line: string): Options => ({ ...o, docsDir: path.join(packageDirOf(o), "lines", line), @@ -535,8 +538,7 @@ export async function fetchPinned(o: Options, log: (s: string) => void = () => { fetchTree(o, pin, log); if (o.line) await provision(o, pin, ["l1-artifacts"], log); const found = problems(o, pin); - if (!o.line && process.env.CONTEXT !== "production") - for (const line of lineNames(o)) found.push(...(await fetchPinned(lineOptions(o, line), log))); + if (!o.line) for (const line of builtLines(o)) found.push(...(await fetchPinned(lineOptions(o, line), log))); return found; } @@ -569,7 +571,7 @@ async function main(argv: string[]): Promise { switch (cmd) { case "lines": if (o.line || args.length) break; - for (const line of lineNames(docs)) console.log(line); + for (const line of builtLines(docs)) console.log(line); return 0; case "fetch": return report(await fetchPinned(o, log)); diff --git a/docs/src/__tests__/network_docs.test.js b/docs/src/__tests__/network_docs.test.js index 738f34d7092..b6ca5983f03 100644 --- a/docs/src/__tests__/network_docs.test.js +++ b/docs/src/__tests__/network_docs.test.js @@ -18,6 +18,7 @@ const { const { llmsSiteUrl } = require("../site_url"); const { versionLabel } = require("../release_names"); const { aztecNrApiFolder } = require("../utils/aztecNrApi.mjs"); +const { lineDocsPath } = require("../utils/docsLine.mjs"); const split = { mainnet: "v5.2.0", testnet: "v6.0.0-rc.3" }; const shared = { mainnet: "v6.0.0", testnet: "v6.0.0" }; @@ -124,6 +125,13 @@ describe("dev lines", () => { assert.strictEqual(aztecNrApiFolder({ name: "v6.0.0" }, folders, "developer", lines), "mainnet"); assert.strictEqual(aztecNrApiFolder({ name: "v6.0.0" }, folders), "mainnet"); }); + + it("move a line's navigation into the dev docs onto the line, and nothing else", () => { + assert.strictEqual(lineDocsPath("/developers/docs/aztec-js", "v6"), "/developers-v6/docs/aztec-js"); + assert.strictEqual(lineDocsPath("/operate", "v6"), "/operate-v6"); + for (const p of ["/developers-v6/overview", "/developersx", "/networks", "/participate/x", "https://example.com/developers/x"]) + assert.strictEqual(lineDocsPath(p, "v6"), p); + }); }); describe("Start here", () => { diff --git a/docs/src/theme/NavbarItem/ComponentTypes.js b/docs/src/theme/NavbarItem/ComponentTypes.js index 1ea0e048307..b6d548ba911 100644 --- a/docs/src/theme/NavbarItem/ComponentTypes.js +++ b/docs/src/theme/NavbarItem/ComponentTypes.js @@ -1,7 +1,28 @@ +import React from 'react'; import ComponentTypes from '@theme-original/NavbarItem/ComponentTypes'; +import { useActivePlugin } from '@docusaurus/plugin-content-docs/client'; import UnifiedVersionDropdown from './UnifiedVersionDropdown'; +import { lineDocsPath, lineOfPlugin } from '@site/src/utils/docsLine.mjs'; + +// On a dev line's page the shared navbar (desktop and mobile, dropdown children included) leads to +// the line's own instances, not the dev docs. Elsewhere every item renders exactly as configured. +function onLine(Item, rewrite) { + return function LineNavbarItem(props) { + const line = lineOfPlugin(useActivePlugin()?.pluginId); + return ; + }; +} + +const linePlugin = (props, line) => + props.docsPluginId === 'developer' || props.docsPluginId === 'network' + ? { ...props, docsPluginId: `${props.docsPluginId}-${line}` } + : props; +const lineLink = (props, line) => (typeof props.to === 'string' ? { ...props, to: lineDocsPath(props.to, line) } : props); export default { ...ComponentTypes, + default: onLine(ComponentTypes.default, lineLink), + doc: onLine(ComponentTypes.doc, linePlugin), + docSidebar: onLine(ComponentTypes.docSidebar, linePlugin), 'custom-unifiedVersionDropdown': UnifiedVersionDropdown, }; diff --git a/docs/src/utils/docsLine.mjs b/docs/src/utils/docsLine.mjs index 8bdd0b6f732..30ef328c59d 100644 --- a/docs/src/utils/docsLine.mjs +++ b/docs/src/utils/docsLine.mjs @@ -6,3 +6,13 @@ * @returns {string | undefined} the line, like `v6`; undefined for every other plugin */ export const lineOfPlugin = (pluginId) => /^(?:developer|network)-(v\d+)$/.exec(pluginId ?? "")?.[1]; + +/** + * `path` moved onto `line`'s instances when it points into the dev docs (`/developers`, `/operate`); + * any other path unchanged. + * + * @param {string} path a site path, like `/developers/docs/aztec-js` + * @param {string} line + * @returns {string} + */ +export const lineDocsPath = (path, line) => path.replace(/^\/(developers|operate)(?=[/?#]|$)/, `/$1-${line}`); diff --git a/docs/src/utils/useDocsBase.js b/docs/src/utils/useDocsBase.js index 286ea4382e4..059084d882c 100644 --- a/docs/src/utils/useDocsBase.js +++ b/docs/src/utils/useDocsBase.js @@ -1,5 +1,5 @@ import { useActivePlugin } from "@docusaurus/plugin-content-docs/client"; -import { lineOfPlugin } from "./docsLine.mjs"; +import { lineDocsPath, lineOfPlugin } from "./docsLine.mjs"; /** * The route base of a docs section as the current page should link it: `/-` on a dev @@ -10,5 +10,5 @@ import { lineOfPlugin } from "./docsLine.mjs"; */ export default function useDocsBase(base) { const line = lineOfPlugin(useActivePlugin()?.pluginId); - return line ? `/${base}-${line}` : `/${base}`; + return line ? lineDocsPath(`/${base}`, line) : `/${base}`; } From f99452ad71b98cf128376b5e459e824593c97b07 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 17:43:40 +0000 Subject: [PATCH 17/32] fix(docs): rewrite a line's dropdown links before the dropdown matches them On a line's page the Resources dropdown now sees its children's line URLs, so the mobile menu opens on the page it contains. The navbar rewrite is one tested function in docsLine.mjs. --- docs/src/__tests__/network_docs.test.js | 25 ++++++++++++++++++++- docs/src/theme/NavbarItem/ComponentTypes.js | 23 ++++++++----------- docs/src/utils/docsLine.mjs | 18 +++++++++++++++ 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/docs/src/__tests__/network_docs.test.js b/docs/src/__tests__/network_docs.test.js index b6ca5983f03..0e3bf38f24b 100644 --- a/docs/src/__tests__/network_docs.test.js +++ b/docs/src/__tests__/network_docs.test.js @@ -18,7 +18,7 @@ const { const { llmsSiteUrl } = require("../site_url"); const { versionLabel } = require("../release_names"); const { aztecNrApiFolder } = require("../utils/aztecNrApi.mjs"); -const { lineDocsPath } = require("../utils/docsLine.mjs"); +const { lineDocsPath, lineNavbarProps } = require("../utils/docsLine.mjs"); const split = { mainnet: "v5.2.0", testnet: "v6.0.0-rc.3" }; const shared = { mainnet: "v6.0.0", testnet: "v6.0.0" }; @@ -132,6 +132,29 @@ describe("dev lines", () => { for (const p of ["/developers-v6/overview", "/developersx", "/networks", "/participate/x", "https://example.com/developers/x"]) assert.strictEqual(lineDocsPath(p, "v6"), p); }); + + it("point a line's navbar items at the line, a dropdown's children before the dropdown sees them", () => { + assert.deepStrictEqual(lineNavbarProps({ type: "docSidebar", docsPluginId: "developer", sidebarId: "sidebar" }, "v6"), { + type: "docSidebar", + docsPluginId: "developer-v6", + sidebarId: "sidebar", + }); + const participate = { type: "doc", docsPluginId: "participate", docId: "index" }; + assert.deepStrictEqual(lineNavbarProps(participate, "v6"), participate); + const resources = { + type: "dropdown", + items: [ + { type: "html", value: "Other Docs" }, + { to: "/developers/ai_tooling", label: "AI Tooling" }, + { to: "https://github.com/AztecProtocol/aztec-starter", label: "Aztec Starter repo" }, + ], + }; + assert.deepStrictEqual(lineNavbarProps(resources, "v6").items, [ + resources.items[0], + { to: "/developers-v6/ai_tooling", label: "AI Tooling" }, + resources.items[2], + ]); + }); }); describe("Start here", () => { diff --git a/docs/src/theme/NavbarItem/ComponentTypes.js b/docs/src/theme/NavbarItem/ComponentTypes.js index b6d548ba911..210521c674e 100644 --- a/docs/src/theme/NavbarItem/ComponentTypes.js +++ b/docs/src/theme/NavbarItem/ComponentTypes.js @@ -2,27 +2,22 @@ import React from 'react'; import ComponentTypes from '@theme-original/NavbarItem/ComponentTypes'; import { useActivePlugin } from '@docusaurus/plugin-content-docs/client'; import UnifiedVersionDropdown from './UnifiedVersionDropdown'; -import { lineDocsPath, lineOfPlugin } from '@site/src/utils/docsLine.mjs'; +import { lineNavbarProps, lineOfPlugin } from '@site/src/utils/docsLine.mjs'; -// On a dev line's page the shared navbar (desktop and mobile, dropdown children included) leads to -// the line's own instances, not the dev docs. Elsewhere every item renders exactly as configured. -function onLine(Item, rewrite) { +// On a dev line's page the shared navbar (desktop and mobile) leads to the line's own instances, not +// the dev docs. Elsewhere every item renders exactly as configured. +function onLine(Item) { return function LineNavbarItem(props) { const line = lineOfPlugin(useActivePlugin()?.pluginId); - return ; + return ; }; } -const linePlugin = (props, line) => - props.docsPluginId === 'developer' || props.docsPluginId === 'network' - ? { ...props, docsPluginId: `${props.docsPluginId}-${line}` } - : props; -const lineLink = (props, line) => (typeof props.to === 'string' ? { ...props, to: lineDocsPath(props.to, line) } : props); - export default { ...ComponentTypes, - default: onLine(ComponentTypes.default, lineLink), - doc: onLine(ComponentTypes.doc, linePlugin), - docSidebar: onLine(ComponentTypes.docSidebar, linePlugin), + default: onLine(ComponentTypes.default), + doc: onLine(ComponentTypes.doc), + docSidebar: onLine(ComponentTypes.docSidebar), + dropdown: onLine(ComponentTypes.dropdown), 'custom-unifiedVersionDropdown': UnifiedVersionDropdown, }; diff --git a/docs/src/utils/docsLine.mjs b/docs/src/utils/docsLine.mjs index 30ef328c59d..e3850e6998b 100644 --- a/docs/src/utils/docsLine.mjs +++ b/docs/src/utils/docsLine.mjs @@ -16,3 +16,21 @@ export const lineOfPlugin = (pluginId) => /^(?:developer|network)-(v\d+)$/.exec( * @returns {string} */ export const lineDocsPath = (path, line) => path.replace(/^\/(developers|operate)(?=[/?#]|$)/, `/$1-${line}`); + +/** + * A navbar item's props as a dev line's page renders them: docs items of the dev docs read the + * line's plugin, links into the dev docs move onto the line, and so do a dropdown's children, before + * the dropdown matches them against the current page. + * + * @param {object} props the item's config props + * @param {string} line + * @returns {object} + */ +export function lineNavbarProps(props, line) { + let out = props; + if (props.docsPluginId === "developer" || props.docsPluginId === "network") + out = { ...out, docsPluginId: `${props.docsPluginId}-${line}` }; + if (typeof props.to === "string") out = { ...out, to: lineDocsPath(props.to, line) }; + if (Array.isArray(props.items)) out = { ...out, items: props.items.map((item) => lineNavbarProps(item, line)) }; + return out; +} From ff25e1ac95d78eb5a32ec6fb9bd0f78012468ad5 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 17:49:36 +0000 Subject: [PATCH 18/32] feat(docs): promote a dev line into a release cut's main paths scripts/promote_line.sh pins a line to the release tag and serves its pages and pin from docs-developers, docs-operate and the main pin, so a v6.x cut renders the v6 line. It refuses outside the cut worktree, on a dirty docs/, a tag of another major or off the line's branch, and a failed pin, before touching the main paths. Promoted pages keep quoting the line's examples. include_code allows a page to quote another line's files while both pins name one commit. --- docs/scripts/__tests__/promote_line.test.mts | 107 ++++++++++++++++++ docs/scripts/promote_line.sh | 47 ++++++++ .../preprocess/__tests__/include_code.test.js | 11 ++ docs/src/preprocess/include_code.js | 3 + 4 files changed, 168 insertions(+) create mode 100644 docs/scripts/__tests__/promote_line.test.mts create mode 100755 docs/scripts/promote_line.sh diff --git a/docs/scripts/__tests__/promote_line.test.mts b/docs/scripts/__tests__/promote_line.test.mts new file mode 100644 index 00000000000..ec6d95f0174 --- /dev/null +++ b/docs/scripts/__tests__/promote_line.test.mts @@ -0,0 +1,107 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { after, before, beforeEach, test } from "node:test"; + +const git = (cwd: string, ...args: string[]) => + execFileSync("git", ["-C", cwd, "-c", "commit.gpgsign=false", "-c", "user.name=t", "-c", "user.email=t@t", ...args], { encoding: "utf8" }).trim(); + +function write(file: string, text: string) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, text); +} + +const MAIN_PATHS = ["docs-developers", "docs-operate", "sidebars-developer.js", "sidebars-operate.js", "aztec-node.json"]; +const pinFile = (ref: string, commit: string) => + JSON.stringify({ repository: "https://github.com/aztec-labs-eng/aztec-node", ref, commit }, null, 2) + "\n"; + +// A cut worktree: main's paths and a v6 line holding different pages. The pin tool is a stand-in +// (the real one is tested in aztec_node.test.mts) that pins the line like `pin` does, or writes the +// pin and fails like a failed check; `gh` answers the compare for v6...v6.0.1 only. +let root: string, docs: string, bin: string; +before(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), "promote-line-")); + docs = path.join(root, "docs"); + bin = path.join(root, "bin"); + write(path.join(docs, "scripts/promote_line.sh"), fs.readFileSync(path.resolve(import.meta.dirname, "../promote_line.sh"), "utf8")); + write( + path.join(docs, "scripts/aztec_node.mts"), + `import fs from "node:fs"; +const [flag, line, cmd, tag] = process.argv.slice(2); +if (flag !== "--line" || cmd !== "pin") process.exit(2); +fs.writeFileSync(new URL(\`../lines/\${line}/aztec-node.json\`, import.meta.url), ${JSON.stringify(pinFile("TAG", "b".repeat(40)))}.replace("TAG", tag)); +process.exit(process.env.PIN_FAIL ? 1 : 0); +`, + ); + write( + path.join(bin, "gh"), + `#!/usr/bin/env bash\n[ "$2" = repos/aztec-labs-eng/aztec-node/compare/v6...v6.0.1 ] && echo "\${COMPARE_STATUS:-behind}" || exit 1\n`, + ); + fs.chmodSync(path.join(bin, "gh"), 0o755); + for (const [dir, who] of [[docs, "main"], [path.join(docs, "lines/v6"), "line"]]) { + write(path.join(dir, "docs-developers/docs/a.md"), `${who} a\n`); + write(path.join(dir, "docs-operate/operators/b.md"), `${who} b\n`); + write(path.join(dir, "sidebars-developer.js"), `// ${who}\n`); + write(path.join(dir, "sidebars-operate.js"), `// ${who}\n`); + } + write(path.join(docs, "docs-developers/docs/main-only.md"), "v7 only\n"); + write(path.join(docs, "aztec-node.json"), pinFile("v7.0.0-nightly.1", "a".repeat(40))); + write(path.join(docs, "lines/v6/aztec-node.json"), pinFile("v6.1.0-nightly.1", "c".repeat(40))); + git(root, "init", "-q"); + git(root, "add", "-A"); + git(root, "commit", "-q", "-m", "cut"); +}); +after(() => fs.rmSync(root, { recursive: true, force: true })); +beforeEach(() => { + git(root, "reset", "-q", "--hard"); + git(root, "clean", "-qfd"); +}); + +const promote = (tag: string, env: Record = {}) => + spawnSync("bash", [path.join(docs, "scripts/promote_line.sh"), "v6", tag], { + encoding: "utf8", + env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, CUT: root, COMPARE_STATUS: undefined, PIN_FAIL: undefined, ...env }, + }); + +const snapshot = (dir: string) => + Object.fromEntries( + MAIN_PATHS.flatMap((p) => { + const full = path.join(dir, p); + if (!fs.existsSync(full)) return [[p, null]]; + if (fs.statSync(full).isFile()) return [[p, fs.readFileSync(full, "utf8")]]; + return (fs.readdirSync(full, { recursive: true }) as string[]) + .filter((f) => fs.statSync(path.join(full, f)).isFile()) + .sort() + .map((f) => [`${p}/${f}`, fs.readFileSync(path.join(full, f), "utf8")]); + }), + ); + +test("a line cut serves the line's pages and its tag's pin from the main paths", () => { + const r = promote("v6.0.1"); + assert.equal(r.status, 0, r.stderr); + assert.deepEqual(snapshot(docs), snapshot(path.join(docs, "lines/v6"))); + assert.equal(JSON.parse(fs.readFileSync(path.join(docs, "aztec-node.json"), "utf8")).ref, "v6.0.1"); +}); + +test("a wrong worktree, a dirty docs/, another major, a tag off the branch and a failed pin leave the main paths untouched", () => { + const before = snapshot(docs); + const cases: [string, () => ReturnType, RegExp][] = [ + ["no cut", () => promote("v6.0.1", { CUT: undefined }), /not the cut worktree/], + ["another worktree", () => promote("v6.0.1", { CUT: os.tmpdir() }), /not the cut worktree/], + ["dirty docs/", () => (write(path.join(docs, "docs-developers/docs/wip.md"), "wip\n"), promote("v6.0.1")), /uncommitted changes/], + ["another major", () => promote("v7.0.0"), /v7\.0\.0 is not a v6 release/], + ["off the branch", () => promote("v6.0.1", { COMPARE_STATUS: "diverged" }), /not on aztec-labs-eng\/aztec-node's v6 branch .*diverged/], + ["failed pin", () => promote("v6.0.1", { PIN_FAIL: "1" }), /pinning line v6 to v6\.0\.1 failed/], + ]; + for (const [name, run, message] of cases) { + git(root, "reset", "-q", "--hard"); + git(root, "clean", "-qfd"); + const r = run(); + assert.notEqual(r.status, 0, name); + assert.match(r.stderr, message, name); + fs.rmSync(path.join(docs, "docs-developers/docs/wip.md"), { force: true }); + assert.deepEqual(snapshot(docs), before, name); + } +}); diff --git a/docs/scripts/promote_line.sh b/docs/scripts/promote_line.sh new file mode 100755 index 00000000000..c8496874d54 --- /dev/null +++ b/docs/scripts/promote_line.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Promotes a dev line (docs/lines//) into the main paths of a release cut, at the cut's Step 2 +# (release-docs, release-network-docs): it pins the line to the release tag, then replaces +# docs-developers/, docs-operate/ and both sidebars with the line's, and docs/aztec-node.json with the +# line's pin. Every later step reads the line's pages through the main paths. Their snippets keep +# quoting docs/lines//, which include_code allows while both pins name one commit. +# +# CUT= docs/scripts/promote_line.sh +# +# It refuses, before writing anything, unless it runs in the worktree $CUT names (the cut's env.sh), +# docs/ is clean, the tag has the line's major, and the tag is on aztec-node's branch. A failed +# pin leaves the main paths untouched. +set -euo pipefail + +fail() { + echo "promote_line: $*" >&2 + exit 1 +} +[ $# -eq 2 ] || { echo "usage: CUT= $0 " >&2; exit 2; } +line=$1 tag=$2 +docs=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P) +root=$(dirname "$docs") + +[[ "$line" =~ ^v([0-9]+)$ ]] || fail "$line is not a line name (v)" +major=${BASH_REMATCH[1]} +line_root=$docs/lines/$line +[ -f "$line_root/aztec-node.json" ] || fail "no line $line: docs/lines/$line/aztec-node.json is missing" +[ -n "${CUT:-}" ] && [ "$(cd "$CUT" 2>/dev/null && pwd -P)" = "$root" ] || + fail "this is $root, not the cut worktree: run it from the cut, with the cut's env.sh sourced (\$CUT)" +[ -z "$(git -C "$root" status --porcelain -- docs)" ] || fail "docs/ has uncommitted changes: commit or discard them first" +[[ "$tag" =~ ^v${major}\. ]] || fail "$tag is not a v$major release, and line $line takes only v$major tags" +repo=$(node -p 'new URL(require(process.argv[1]).repository).pathname.slice(1)' "$line_root/aztec-node.json") +status=$(gh api "repos/$repo/compare/$line...$tag" --jq .status) || fail "cannot compare $tag with $repo's $line branch" +case "$status" in + behind | identical) ;; + *) fail "$tag is not on $repo's $line branch (compare $line...$tag: $status)" ;; +esac + +node "$docs/scripts/aztec_node.mts" --line "$line" pin "$tag" || fail "pinning line $line to $tag failed; the main paths are untouched" + +for tree in docs-developers docs-operate; do + rm -rf "${docs:?}/$tree" + cp -R "$line_root/$tree" "$docs/$tree" +done +cp "$line_root/sidebars-developer.js" "$line_root/sidebars-operate.js" "$docs/" +cp "$line_root/aztec-node.json" "$docs/aztec-node.json" +echo "promoted line $line at $tag into docs/: commit it as the promotion commit" diff --git a/docs/src/preprocess/__tests__/include_code.test.js b/docs/src/preprocess/__tests__/include_code.test.js index eac35153b20..e57d416d4f6 100644 --- a/docs/src/preprocess/__tests__/include_code.test.js +++ b/docs/src/preprocess/__tests__/include_code.test.js @@ -140,6 +140,17 @@ describe("include_code", () => { await assert.rejects(preprocessIncludeCode(include("docs/lines/v1/examples/a.ts"), "page.md", root.rootDir), /is line v1's, but this page is main's/); }); + it("lets main's pages quote a line's files while both pin one commit, as a promoted line cut does", async () => { + const line = path.join(root.rootDir, "docs/lines/v1"); + write(path.join(line, "aztec-node.json"), fs.readFileSync(path.join(root.rootDir, "docs/aztec-node.json"))); + write(path.join(line, "examples/a.ts"), "// docs:start:x\nconst promoted = 1;\n// docs:end:x\n"); + process.env.DOCS_COMMIT = "def456"; + + const { content } = await preprocessIncludeCode("#include_code x /docs/lines/v1/examples/a.ts typescript\n", "page.md", root.rootDir); + assert.match(content, /const promoted = 1/); + assert.ok(content.includes("aztec-packages/blob/def456/docs/lines/v1/examples/a.ts#L1-L3")); + }); + it("checks a line's checkout even after main's check passed on the same directory", async () => { const line = path.join(root.rootDir, "docs/lines/v1"); const tree = path.join(line, ".aztec-node/tree"); diff --git a/docs/src/preprocess/include_code.js b/docs/src/preprocess/include_code.js index 0d03fb402cd..9d6ce88f569 100644 --- a/docs/src/preprocess/include_code.js +++ b/docs/src/preprocess/include_code.js @@ -74,6 +74,9 @@ function checkSnippetLine(relativeCodeFilePath, rootDir, line) { const owner = lineOf(relativeCodeFilePath, rootDir); if (!owner && !MAIN_ONLY.test(relativeCodeFilePath)) return; if (owner?.name === line?.name) return; + // Pins naming one commit quote the same code: a line cut (scripts/promote_line.sh) serves the + // line's pages from main's paths under the line's pin, and they keep quoting the line's files. + if (readPin(rootDir, owner).commit === readPin(rootDir, line).commit) return; const whose = (l) => (l ? `line ${l.name}'s` : "main's"); throw new Error(`${relativeCodeFilePath} is ${whose(owner)}, but this page is ${whose(line)}: quote the page's own copy`); } From 7845ee5359a32becb564724f57b8b13d97fec8d0 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 17:52:10 +0000 Subject: [PATCH 19/32] docs: document the two dev lines and their pins docs/CLAUDE.md opens with the rule to change both lines unless the code says a change is line-specific, how to port a page's paths, and how to check both. The pin section gives each line its own branch, adds the line cut and line retirement, and drops the stay-on-v6 rule. README.md carries the same for people. --- docs/CLAUDE.md | 28 ++++++++++++++++++++++++---- docs/README.md | 23 +++++++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index cd6fbac658a..3afebc37832 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -2,6 +2,23 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Two dev lines: change both + +The dev docs follow two aztec-node lines, and each developer and operator page has a copy per line. The root pages (`docs/docs/`), `docs-participate/`, the versioned snapshots and `static/` are shared. + +| Line | Follows | Pages and examples | Pin | +| --- | --- | --- | --- | +| main | aztec-node `main` | `docs-developers/`, `docs-operate/`, `sidebars-*.js`, `examples/` | `aztec-node.json` | +| v6 | aztec-node `v6` | `lines/v6/docs-developers/`, `lines/v6/docs-operate/`, `lines/v6/sidebars-*.js`, `lines/v6/examples/` | `lines/v6/aztec-node.json` | + +- **A change goes into both lines.** Change one line only when the change is specific to it. +- **Decide that from the code, not the PR.** Grep the symbol or behaviour you document in each line's checkout: `.aztec-node/tree` for main, `lines/v6/.aztec-node/tree` for v6. If it is the same in both, change both copies. If it differs or is missing in one, change only the line that has it, the way that line has it. + - A Labs PR's base branch (`main` or `v6`) only says which checkout to start with. A `main` PR can fix behaviour v6 shares, and a backport to `v6` still needs this check. + - A fix to wording, structure or a mistake that both copies share is never line-specific. +- **Port with the line's paths.** The counterpart of `docs-developers/` is `lines/v6/docs-developers/`. In a v6 page, `#include_code` and `references:` paths into `docs/examples/` start with `docs/lines/v6/examples/`, and paths into `docs/node_modules/@aztec-foundation/l1-artifacts/` start with `docs/lines/v6/.aztec-node/l1-artifacts/`. `labs/` paths are the same in both and read each line's own checkout. +- **Check both lines** with `yarn build` (a preview also serves v6 at `/developers-v6/` and `/operate-v6/`), `./bootstrap.sh check`, `examples/bootstrap.sh` and `examples/bootstrap.sh --line v6`. +- **Migration notes and the operator changelog exist in both lines.** A v6.x entry goes into both, since v7 readers upgrade from the latest v6. A v7 entry goes into main only. Released entries are history in either line: never edit them. + ## Overview You are working with the **Aztec Protocol Documentation** - a comprehensive documentation site built with Docusaurus 3 for Aztec, the decentralized Ethereum L2 for building unstoppable private apps. Your role includes both technical development tasks and ensuring all documentation meets quality standards for accuracy, clarity, and usability. @@ -63,8 +80,9 @@ The aztec-node (Labs) code the docs quote is the commit `aztec-node.json` pins, - Move the pin with `node scripts/aztec_node.mts pin ` (a release or nightly tag), never by hand; then set `@aztec-foundation/l1-artifacts` to the version `check` names, run `yarn install`, and commit `aztec-node.json` with `package.json` and `yarn.lock`. - `pin` needs `gh` logged in: it records the sha256 of each noir release archive in `noirArchives` once `gh attestation verify` proves noir's publish workflow built it, and `provision` refuses a nargo archive whose sha256 differs. Never edit `noirArchives` to get past a mismatch: it means the release archive changed after the pin. - Tools that need nargo, bb or the aztec CLI use the pinned published ones: `node scripts/aztec_node.mts provision nargo bb cli`, then `eval "$(node scripts/aztec_node.mts env)"` exports `NARGO`, `BB`, `AZTEC` and the versions. Never run nargo inside `docs/` or the checkout: it takes `docs/Nargo.toml` as its workspace. -- The dev docs stay on aztec-node's v6 line until they carry a separate v7 line: move their pin to the newest `v6.*` nightly, never to a v7 tag (`main` carries v7 from 2026-11-04). A v6.x release is cut from the dev docs' pages and examples, so it works only while they are v6 content. Then check that the recorded commit is on `v6`: `gh api "repos/aztec-labs-eng/aztec-node/compare/v6...$(node -p 'require("./aztec-node.json").commit')" --jq .status` prints `behind` or `identical`. -- A release cut moves the pin to the release tag in its own worktree only (`/release-docs`, `/release-network-docs`); v6.x releases are cut from their tags on aztec-node's `v6` branch. The dev docs keep their pin. +- Each line's pin follows its own aztec-node branch (see "Two dev lines: change both"): `aztec-node.json` follows `main` (v7 from 2026-11-04), and `lines//aztec-node.json` follows ``. Move a line's pin with `node scripts/aztec_node.mts --line pin `: it installs the line's l1-artifacts into `lines//.aztec-node/`, so `package.json` does not change. A line's `pin` and `check` refuse a tag of another major. After moving any pin, check that the recorded commit is on its branch: `gh api "repos/aztec-labs-eng/aztec-node/compare/..." --jq .status` prints `behind` or `identical`. +- A release cut moves the pins in its own worktree only (`/release-docs`, `/release-network-docs`); the dev docs keep theirs. A cut of a line's major (a v6.x release, from its tag on aztec-node's `v6` branch) first promotes the line into the main paths with `scripts/promote_line.sh`. +- Retire a line with `git rm -r docs/lines/` after its last release, or once no network runs its major: from then on, preprocessing fails on the line's API reference links. ### Preprocessing Environment Variables @@ -186,6 +204,8 @@ The `examples/` directory contains runnable code examples that are included in d **Validation (type-checking)**: `examples/bootstrap.sh` compiles Noir circuits, Noir contracts, Solidity, and type-checks TypeScript examples. This runs on every PR. +`examples/bootstrap.sh --line ` builds a line's examples the same way, in a copy staged outside the repository (nargo would take `docs/Nargo.toml` as the workspace). Run it whole: the stage is new every run, so a single step such as `validate-ts` finds no compiled contracts. A line's examples run against a network only at its release cut, never in CI. + The Solidity examples compile against the l1-contracts sources shipped in the `@aztec-foundation/l1-artifacts` devDependency (see `examples/solidity/foundry.toml`). Its version in `package.json` must equal `toolchain.l1Artifacts` in `aztec-node.json` (the pinned aztec-node's `BB_VERSION`); `node scripts/aztec_node.mts check` fails until both move together. The examples build with the pin's published toolchain, the same code the snippets quote: `examples/bootstrap.sh` provisions nargo, bb and the aztec CLI (`@aztec-labs/aztec`) at the pinned versions, the contracts' Nargo path dependencies point into the default checkout (`.aztec-node/tree`; an `AZTEC_NODE_DIR` override never applies to them), and the TS examples install `@aztec-labs/*` at `AZTEC_VERSION` and `@aztec-foundation/*` at `BB_VERSION` from npm. @@ -400,5 +420,5 @@ Approved external documentation sources: - Suggest improvements even if they go beyond pure editing - When making changes to documentation processes or tooling, remember to check and update READMEs, project documentation (like this file), and code comments -Last updated: 2026-11-02 -Version: 1.11 +Last updated: 2026-11-05 +Version: 1.12 diff --git a/docs/README.md b/docs/README.md index d4a25015371..392a0d8c373 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,23 @@ Documentation for the Aztec Network, built with Docusaurus You can view the latest successful build here: https://docs.aztec.network +## Two dev lines + +The dev docs follow two aztec-node branches at once: `main` (v7) and `v6`, which keeps shipping v6.x releases. So each developer and operator page exists twice. The landing pages (`docs/`), `docs-participate/`, the versioned snapshots and `static/` are shared. + +| Line | Follows | Pages and examples | Pin | +| --- | --- | --- | --- | +| main | aztec-node `main` | `docs-developers/`, `docs-operate/`, `sidebars-*.js`, `examples/` | `aztec-node.json` | +| v6 | aztec-node `v6` | `lines/v6/docs-developers/`, `lines/v6/docs-operate/`, `lines/v6/sidebars-*.js`, `lines/v6/examples/` | `lines/v6/aztec-node.json` | + +- Make each change in both lines, unless it is specific to one. Decide from the code: look the symbol or behaviour up in each line's checkout (`.aztec-node/tree` and `lines/v6/.aztec-node/tree`). If it is the same in both, change both pages; if not, change the line that has it. A fix to wording or to a mistake both copies share always goes into both. +- In a v6 page, snippet and `references:` paths name the line's own files: `docs/lines/v6/examples/...` and `docs/lines/v6/.aztec-node/l1-artifacts/...`. `labs/` paths are the same in both lines and read each line's checkout. While the two pins differ, preprocessing refuses a page that quotes the other line's files. +- Deploy previews serve the v6 line at `/developers-v6/` and `/operate-v6/`, marked noindex and left out of search, `llms.txt` and the sitemap. Production never builds it. +- `./bootstrap.sh` checks each line's references and builds its examples (`examples/bootstrap.sh --line v6`). A line's examples run against a network only at its release cut. +- Migration notes and the operator changelog exist in both lines. A v6.x entry goes into both, since v7 readers upgrade from the latest v6; a v7 entry goes into main only. +- A v6.x release cut promotes the v6 line into the main paths with `scripts/promote_line.sh` (see the release skills). +- Retire a line with `git rm -r docs/lines/` after its last release, or once no network runs its major. + ## Docusaurus This website is built using [Docusaurus 3](https://docusaurus.io/), a modern static website generator. @@ -221,14 +238,16 @@ node scripts/aztec_node.mts check # fail on any drift between the pin node scripts/aztec_node.mts pin # move the pin to an aztec-node release or nightly tag (needs gh) node scripts/aztec_node.mts provision nargo bb cli # install the pinned nargo, bb and aztec CLI into .aztec-node/ eval "$(node scripts/aztec_node.mts env)" # export AZTEC_NODE_DIR, the versions, NARGO, BB and AZTEC +node scripts/aztec_node.mts --line v6 # the same for a line, with its own pin and lines/v6/.aztec-node/ +node scripts/aztec_node.mts lines # list the lines (none in production) ``` - `yarn build`, `yarn start` and `./bootstrap.sh` fetch first, so a fresh clone needs nothing else. The checkout is a shallow clone without aztec-node's own `docs/`; `AZTEC_NODE_DIR` moves it elsewhere. - `#include_code` and `references:` paths keep their `labs/` prefix, which names the checkout: `labs/yarn-project/...` reads `.aztec-node/tree/yarn-project/...`, and source links name aztec-node at the pinned commit (a tag can move; a commit cannot). - **Moving the pin:** run `pin `, then do what `check` asks (usually: set `@aztec-foundation/l1-artifacts` in `package.json` to the pinned `BB_VERSION` and run `yarn install`), and commit `aztec-node.json`, `package.json` and `yarn.lock` together. Snippets whose markers moved fail the build until the pages follow. - **Verified nargo:** `pin` needs the GitHub CLI, logged in. It downloads noir's release archive for each platform, has `gh attestation verify` check that noir's `publish-nargo.yml` workflow built it, and records its sha256 in `noirArchives`. `provision nargo` needs no `gh`: it refuses an archive whose sha256 differs from the pin, and a platform the pin has no sha256 for. Never edit `noirArchives` by hand; a mismatch means the release archive changed after the pin. -- **Which tag:** the dev docs stay on aztec-node's v6 line until they carry a separate v7 line. Move their pin to the newest `v6.*` nightly, never to a v7 tag (`main` carries v7 from 2026-11-04): a v6.x release is cut from the dev docs' pages and examples, so it works only while they are v6 content. Then check that the recorded commit is on `v6` (from `docs/`): `gh api "repos/aztec-labs-eng/aztec-node/compare/v6...$(node -p 'require("./aztec-node.json").commit')" --jq .status` prints `behind` or `identical`. -- **Release cuts** move the pin to the release tag inside their own worktree, and the snapshot is generated from that; the dev docs keep their pin. v6.x releases are cut from their tags on aztec-node's `v6` branch. See the `release-docs` and `release-network-docs` skills under `.claude/skills/`. +- **Which tag:** each line's pin follows its own aztec-node branch: `aztec-node.json` follows `main` (v7 from 2026-11-04), and `lines//aztec-node.json` follows ``. A line's `pin` installs its l1-artifacts into `lines//.aztec-node/`, so `package.json` stays as it is, and its `pin` and `check` refuse a tag of another major. After moving a pin, check that the recorded commit is on its branch (from `docs/`): `gh api "repos/aztec-labs-eng/aztec-node/compare/..." --jq .status` prints `behind` or `identical`. +- **Release cuts** move the pins inside their own worktree, and the snapshot is generated from that; the dev docs keep their pins. A v6.x release is cut from its tag on aztec-node's `v6` branch, after `scripts/promote_line.sh` moves the v6 line into the main paths. See the `release-docs` and `release-network-docs` skills under `.claude/skills/`. - **Examples** build with the pinned published toolchain against the default checkout, which their Nargo.toml path dependencies name (`../../../.aztec-node/tree/...`), so `AZTEC_NODE_DIR` does not apply to them. The TS examples install `@aztec-labs/*` at the pinned `AZTEC_VERSION` and `@aztec-foundation/*` at `BB_VERSION`. - Never run nargo inside `docs/` or the checkout: nargo takes `docs/Nargo.toml` as its workspace. From 16caeee87c3d974020e56106518d38c753338942 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 17:54:25 +0000 Subject: [PATCH 20/32] docs(skills): reference and changelog updates cover both dev lines update-doc-references runs by hand, tests a PR's merge commit against every line's pin, maps references in each line and edits each copy whose checkout has the change. updating-changelog reads each line's pin and puts a v6.x entry into both lines, a v7 entry into main only. --- .claude/skills/update-doc-references/SKILL.md | 88 +++++++++++-------- .claude/skills/updating-changelog/SKILL.md | 51 +++++++---- 2 files changed, 83 insertions(+), 56 deletions(-) diff --git a/.claude/skills/update-doc-references/SKILL.md b/.claude/skills/update-doc-references/SKILL.md index 80dc08ea5ac..56a18622e0d 100644 --- a/.claude/skills/update-doc-references/SKILL.md +++ b/.claude/skills/update-doc-references/SKILL.md @@ -8,19 +8,24 @@ description: Analyze source-file changes referenced by documentation, update aff When source files referenced by documentation change, analyze the changes and update the affected documentation if needed. -The docs live in this repository's `docs/`. Most of the code they document is aztec-node's, -at the commit `docs/aztec-node.json` pins, read through a checkout of it -(`docs/.aztec-node/tree`, or `$AZTEC_NODE_DIR`) that `node docs/scripts/aztec_node.mts -fetch` keeps. A page's frontmatter `references` names paths from this repository's root: -`labs/` for Labs code, where the `labs/` prefix names that checkout and -never the `labs/` submodule, and plain paths for this repository's own files -(`docs/examples/...`). A reference ending in `/*` covers every file under that directory. +The docs live in this repository's `docs/`, in two lines that each have their own copy of +every developer and operator page (`docs/CLAUDE.md`, "Two dev lines: change both"). Most of +the code they document is aztec-node's: main's pages describe aztec-node `main` at the commit +`docs/aztec-node.json` pins, read through a checkout of it (`docs/.aztec-node/tree`, or +`$AZTEC_NODE_DIR`), and the v6 line's pages (`docs/lines/v6/`) describe aztec-node `v6` at +the commit `docs/lines/v6/aztec-node.json` pins, read through `docs/lines/v6/.aztec-node/tree`. +`node docs/scripts/aztec_node.mts fetch` keeps both. A page's frontmatter `references` names +paths from this repository's root: `labs/` for Labs code, where the +`labs/` prefix names the checkout of the page's line and never the `labs/` submodule, and +plain paths for this repository's own files (`docs/examples/...`, `docs/lines/v6/examples/...`). +A reference ending in `/*` covers every file under that directory. ## Workflow ### Step 1: Parse the Prompt -Extract from the dispatch prompt: +The dispatcher is offline, so the skill runs by hand: it needs only the PR number and its +repository, and Step 3 finds the pages the PR's files reach. A dispatch prompt gives: - the PR number and its repository: `aztec-labs-eng/aztec-node` for Labs code (the usual case), `AztecProtocol/aztec-packages` for this repository's own files @@ -40,31 +45,34 @@ gh pr diff --repo An aztec-node PR names paths from aztec-node's root: prefix them with `labs/` to compare them with `references`. -The source docs follow the pin's line, aztec-node `v6` until the docs carry a separate v7 -line (`docs/CLAUDE.md`). An aztec-node PR into `main` (`baseRefName` `main`, v7) changes -code the pin may never get. Test its merge commit against the pin (an unmerged PR is not -in it): +Each line's pages describe the code at that line's pin. The PR's base branch +(`baseRefName`: `main` or `v6`) says which line to start with, not which lines it reaches: +test its merge commit against every pin (an unmerged PR is in none): ```bash cd "$(git rev-parse --show-toplevel)" merge=$(gh pr view --repo aztec-labs-eng/aztec-node --json mergeCommit -q .mergeCommit.oid) -pin=$(node -p 'require("./docs/aztec-node.json").commit') -gh api "repos/aztec-labs-eng/aztec-node/compare/$merge...$pin" --jq .status +for pinfile in docs/aztec-node.json docs/lines/*/aztec-node.json; do + echo "$pinfile: $(gh api "repos/aztec-labs-eng/aztec-node/compare/$merge...$(node -p "require('./$pinfile').commit")" --jq .status)" +done ``` -`ahead` or `identical` means the pin has it: continue as for any PR. Otherwise edit no -source docs, since they describe v6. Still run Steps 3 and 4, and report what v7 docs -would need. +`ahead` or `identical` means that line's pin has the change, so its pages may need it. A line +whose pin lacks it gets no edit that quotes the new code: report what its pages will need +when its pin moves. Then compare the symbol or behaviour the pages document in both +checkouts, and edit each copy whose checkout has it (Step 5): a `main` PR can fix behaviour v6 +shares, and a page that is wrong about code both lines have is wrong in both copies. -When the change is a move of the docs' pin rather than a PR (`node -docs/scripts/aztec_node.mts pin `), take it from the two pin commits, fetched into the -checkout. A two-commit diff needs no history between them: +When the change is a move of a pin rather than a PR (`node docs/scripts/aztec_node.mts +[--line ] pin `), take it from the two pin commits, fetched into that line's +checkout. A two-commit diff needs no history between them. For main: ```bash cd "$(git rev-parse --show-toplevel)" -old=$(git show "$(git merge-base origin/next HEAD):docs/aztec-node.json" | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).commit') -new=$(node -p 'require("./docs/aztec-node.json").commit') -node docs/scripts/aztec_node.mts fetch && pin=$(node docs/scripts/aztec_node.mts env) || exit 1 +pinfile=docs/aztec-node.json lineopt= # the v6 line's: pinfile=docs/lines/v6/aztec-node.json lineopt="--line v6" +old=$(git show "$(git merge-base origin/next HEAD):$pinfile" | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).commit') +new=$(node -p "require('./$pinfile').commit") +node docs/scripts/aztec_node.mts $lineopt fetch && pin=$(node docs/scripts/aztec_node.mts $lineopt env) || exit 1 eval "$pin" git -C "$AZTEC_NODE_DIR" fetch --depth 1 --no-tags origin "$old" "$new" git -C "$AZTEC_NODE_DIR" diff --name-only "$old" "$new" | sed 's|^|labs/|' @@ -77,21 +85,23 @@ the report, since the examples and the Solidity snippets follow them. ### Step 3: Identify Affected Documentation The prompt lists which source files changed and which docs reference them. Confirm the -mapping from the docs' frontmatter, from the repository root (the library scans -`docs/docs-developers`, `docs/docs-operate`, `docs/docs-participate` and `docs/docs`, -never the versioned snapshots): +mapping from the docs' frontmatter, from the repository root, once per line (the library +scans a line's `docs-developers`, `docs-operate`, `docs-participate` and `docs`, never the +versioned snapshots): ```bash cd "$(git rev-parse --show-toplevel)" source docs/scripts/lib/extract_doc_references.sh map=$(mktemp) changed=$(mktemp) -extract_references_mapping docs "$map" gh pr view --repo aztec-labs-eng/aztec-node --json files -q '.files[].path' | sed 's|^|labs/|' >"$changed" -awk -F'|' 'NR == FNR {changed[$0]; next} - {ref = $1; dir = sub(/\/\*$/, "/", ref) - for (f in changed) if (dir ? index(f, ref) == 1 : f == ref) print f " -> " $2}' \ - "$changed" "$map" | sort -u +for root in docs docs/lines/*; do + extract_references_mapping "$root" "$map" + awk -F'|' 'NR == FNR {changed[$0]; next} + {ref = $1; dir = sub(/\/\*$/, "/", ref) + for (f in changed) if (dir ? index(f, ref) == 1 : f == ref) print f " -> " $2}' \ + "$changed" "$map" | sort -u +done ``` For a PR of this repository drop the `sed`; for a pin move write Step 2's `--name-only` @@ -125,8 +135,9 @@ Changes that typically do NOT require doc updates: ### Step 5: Make Updates -If documentation updates are needed, edit this repository's source docs (`docs/docs-*` and -`docs/docs/`), never a versioned snapshot, the checkout or `labs/`: +If documentation updates are needed, edit this repository's source docs (`docs/docs-*`, +`docs/docs/` and each line's `docs/lines//docs-*`, per Step 2), never a versioned +snapshot, a checkout or `labs/`: 1. Make precise, minimal changes with the Edit tool. 2. Only modify content directly affected by the code changes. @@ -134,12 +145,15 @@ If documentation updates are needed, edit this repository's source docs (`docs/d 4. Do not make cosmetic changes unrelated to the code changes. 5. Do not add or remove sections unless required by the code change. 6. Update code examples if the API changed; a path in `#include_code` or `references` keeps - its `labs/` prefix. `docs/examples/` builds with the pin's published toolchain, so after a - pin move run `docs/examples/bootstrap.sh` to compile and type-check them at the new pin. + its `labs/` prefix, and a v6 page's paths into its examples keep `docs/lines/v6/`. Each + line's examples build with its pin's published toolchain, so after a pin move run + `docs/examples/bootstrap.sh` (main) or `docs/examples/bootstrap.sh --line v6` to compile and + type-check them at the new pin. 7. Correct explanations if behavior changed. -Then run `docs/scripts/check_doc_references.sh`, which checks `labs/` references in the -pinned checkout (run `node docs/scripts/aztec_node.mts fetch` first; it must print `All +Then run `docs/scripts/check_doc_references.sh` and, for the v6 line, +`docs/scripts/check_doc_references.sh docs/lines/v6`, which check `labs/` references in each +line's checkout (run `node docs/scripts/aztec_node.mts fetch` first; each must print `All referenced paths exist.`), and, from `docs/`, `yarn spellcheck`. An aztec-node PR newer than the pin is not in the checkout yet, so an edit that quotes its code (a new `#include_code` marker, a moved file in `references`) fails until the pin includes it: diff --git a/.claude/skills/updating-changelog/SKILL.md b/.claude/skills/updating-changelog/SKILL.md index 499e0dfcf2d..1a8d10af3f5 100644 --- a/.claude/skills/updating-changelog/SKILL.md +++ b/.claude/skills/updating-changelog/SKILL.md @@ -5,11 +5,15 @@ description: Updates changelog documentation for contract developers and node op # Updating Changelog -The docs live in this repository's `docs/`. The code they describe is mostly aztec-node's, -at the commit `docs/aztec-node.json` pins, read through a checkout of it that -`node docs/scripts/aztec_node.mts fetch` keeps (`$AZTEC_NODE_DIR`, default -`docs/.aztec-node/tree`; its version and its changes are read there, never in the `labs/` -submodule), plus this repository's own code (`l1-contracts/`, `barretenberg/`, …). +The docs live in this repository's `docs/`, in two lines that each have their own copy of +every developer and operator page (`docs/CLAUDE.md`, "Two dev lines: change both"). The code +they describe is mostly aztec-node's: main's pages follow aztec-node `main` at the commit +`docs/aztec-node.json` pins, and the v6 line's (`docs/lines/v6/`) follow aztec-node `v6` at the +commit `docs/lines/v6/aztec-node.json` pins. Each is read through its line's checkout, which +`node docs/scripts/aztec_node.mts fetch` keeps (main's is `$AZTEC_NODE_DIR`, default +`docs/.aztec-node/tree`; v6's is `docs/lines/v6/.aztec-node/tree`); versions and changes are +read there, never in the `labs/` submodule. This repository's own code (`l1-contracts/`, +`barretenberg/`, …) is the rest. aztec-node records operator-facing changes as they land, in `CHANGELOG-OPERATORS.md` at its root. It keeps one copy per release line, on that line's branch, so the checkout holds the copy @@ -21,11 +25,13 @@ The diff is still read, as the check on what the file missed. ### 1. Determine Target Files -The version is the Labs release the docs' pin names, read from the checkout: +The version is the Labs release the line's pin names, read from its checkout. For main +(for the v6 line, `lineopt="--line v6"`): ```bash cd "$(git rev-parse --show-toplevel)" -node docs/scripts/aztec_node.mts fetch && pin=$(node docs/scripts/aztec_node.mts env) || exit 1 +lineopt= +node docs/scripts/aztec_node.mts $lineopt fetch && pin=$(node docs/scripts/aztec_node.mts $lineopt env) || exit 1 eval "$pin" jq -r '."."' "$AZTEC_NODE_DIR/.version-manifest.json" 2>/dev/null || jq -r '."."' "$AZTEC_NODE_DIR/.release-please-manifest.json" @@ -39,16 +45,21 @@ root versions the Foundation tree instead, whose numbers are unrelated to Labs': the version from it. Documenting one aztec-node PR (source 3 below) takes the major from the PR's line instead, -since the pin can lag it or sit on the other line: `baseRefName` `main` is the major `main` carries (v7 from +since a pin can lag it: `baseRefName` `main` is the major `main` carries (v7 from 2026-11-04), `v6` is v6. -**Target files:** +**Which lines get the entry.** A v6.x entry goes into both lines, since v7 readers upgrade +from the latest v6. A v7 entry goes into main only. Released entries are history in either +line: never edit them. + +**Target files**, in each line that gets the entry (main's paths below; the v6 line's are the +same under `docs/lines/v6/`): - Aztec contract developers: `docs/docs-developers/docs/resources/migration_notes.md` - Node operators and Ethereum contract developers: `docs/docs-operate/operators/reference/changelog/v{major}.md` -When that file is new (the first entry of a major), two more files must be updated or -the page ships unreachable: +When that file is new (the first entry of a major), two more files of the same line must +be updated or the page ships unreachable: - `docs/docs-operate/operators/reference/changelog/index.md`: add a `### [](./.md)` entry at the top of `## Version history`. The heading @@ -76,15 +87,17 @@ lives: git diff origin/next...HEAD -- . ':!labs' ``` -2. **Labs code, when this branch moves the docs' pin** (`docs/aztec-node.json`, moved with - `node docs/scripts/aztec_node.mts pin `). Read the range in the checkout; both - commits must be in it, and a two-commit diff needs no history between them: +2. **Labs code, when this branch moves a line's pin** (`docs/aztec-node.json` or + `docs/lines//aztec-node.json`, moved with `node docs/scripts/aztec_node.mts [--line + ] pin `). Read the range in that line's checkout; both commits must be in it, + and a two-commit diff needs no history between them. For main: ```bash cd "$(git rev-parse --show-toplevel)" - old=$(git show "$(git merge-base origin/next HEAD):docs/aztec-node.json" | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).commit') - new=$(node -p 'require("./docs/aztec-node.json").commit') - node docs/scripts/aztec_node.mts fetch && pin=$(node docs/scripts/aztec_node.mts env) || exit 1 + pinfile=docs/aztec-node.json lineopt= # the v6 line's: pinfile=docs/lines/v6/aztec-node.json lineopt="--line v6" + old=$(git show "$(git merge-base origin/next HEAD):$pinfile" | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).commit') + new=$(node -p "require('./$pinfile').commit") + node docs/scripts/aztec_node.mts $lineopt fetch && pin=$(node docs/scripts/aztec_node.mts $lineopt env) || exit 1 eval "$pin" git -C "$AZTEC_NODE_DIR" fetch --depth 1 --no-tags origin "$old" "$new" git -C "$AZTEC_NODE_DIR" diff "$old" "$new" -- CHANGELOG-OPERATORS.md @@ -158,8 +171,8 @@ that no one will notice have drifted apart. ### 4. Edit Documentation -After approval, add entries to the appropriate files in this repository's `docs/` (never -in the checkout or under `labs/`, and never a versioned snapshot). +After approval, add entries to the appropriate files of each line that gets them, in this +repository's `docs/` (never in a checkout or under `labs/`, and never a versioned snapshot). ## Migration Notes Format From c022c3c768eb6f4cff90236fed56e6878e434034 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 18:00:51 +0000 Subject: [PATCH 21/32] docs(skills): cut a v6.x release from the v6 line Both release skills detect a line cut at Step 2b, when the tag's major has a dev line, and promote the line at Step 2c with promote_line.sh. The examples checks and link checks read the line's examples, Step 12 compares against the line's pin, Step 15 builds and runs the line's examples, and the carry-back applies the promoted pages' repairs to the line, with the release's migration and changelog entries also in main. examples/bootstrap.sh --line keeps one stage across runs when DOCS_LINE_STAGE names it, so a cut can run a line's examples step by step. --- .claude/skills/release-docs/SKILL.md | 132 ++++++++++++++----- .claude/skills/release-network-docs/SKILL.md | 72 ++++++++-- docs/CLAUDE.md | 2 +- docs/examples/bootstrap.sh | 23 ++-- 4 files changed, 174 insertions(+), 55 deletions(-) diff --git a/.claude/skills/release-docs/SKILL.md b/.claude/skills/release-docs/SKILL.md index 315e7a2c200..09f59100a86 100644 --- a/.claude/skills/release-docs/SKILL.md +++ b/.claude/skills/release-docs/SKILL.md @@ -48,9 +48,14 @@ paste the key into the conversation or onto a command line. `BB` once provisioned). In the cut that is the release tag's. - **The cut runs in its own worktree** (`$CUT`), created from the destination commit; it moves the cut's pin to the Labs tag. The destination checkout is not touched until Step 17, - and its pin never moves: the destination's dev docs keep following their own pin, a v6 - nightly until the docs carry a separate v7 line (`docs/CLAUDE.md`). A v6.x cut pins its - tag on aztec-node's `v6` branch (Step 12). + and its pins never move: the destination's dev docs keep following their own + (`docs/CLAUDE.md`, "Two dev lines: change both"). +- **A line cut.** A release whose major has a dev line (`docs/lines/v/`: a v6.x + release while `docs/lines/v6/` exists) is cut from that line, on its tag on aztec-node's + `v` branch. Step 2c promotes the line into the cut's main paths + (`docs/scripts/promote_line.sh`), so every later step reads the line's pages and pin + there; its examples stay in `docs/lines/v/examples/` (`$EXAMPLES`), which its pages + quote. Any other release is cut from main's pages. - **Release inputs**, recorded in the commit that lands the cut (Step 17): | Input | Source | @@ -62,15 +67,17 @@ paste the key into the conversation or onto a command line. | Destination | the branch and commit the cut started from | - **Snippet links.** Snippets under `labs/` link to aztec-node at the pinned commit; snippets - from `docs/examples/` link to this repository at `DOCS_COMMIT`, which a release build - requires (`include_code.js` refuses to preprocess a `RELEASE_TYPE` other than `nightly` - without it). The cut never changes `docs/examples/`, so those links show the lines the + from the examples (`$EXAMPLES`: `docs/examples/`, or a line cut's + `docs/lines//examples/`) link to this repository at `DOCS_COMMIT`, which a release + build requires (`include_code.js` refuses to preprocess a `RELEASE_TYPE` other than + `nightly` without it). The cut never changes them, so those links show the lines the snapshot quotes. - **Examples build from the pin.** `docs/examples/bootstrap.sh` builds them with the pin's published toolchain against the default checkout, `docs/.aztec-node/tree`, which their Nargo.toml path dependencies name; it ignores `AZTEC_NODE_DIR`. In the cut that is a second checkout of the tag's commit, so the examples the snapshot quotes build and run - at the tag (Step 15). + at the tag (Step 15). A line cut builds the line's examples in a staged copy instead + (`examples/bootstrap.sh --line `). - **State** lives in `~/.cache/aztec-docs-cut//`: `env.sh` (sourced by every command block from Step 2 on, since shell state does not carry over between commands), the worktree, the CLI's home, the local network and logs. `` is `v`; @@ -188,9 +195,16 @@ git diff --quiet --ignore-submodules=all && git diff --cached --quiet --ignore-s || { echo "commit or discard the changes first" >&2; exit 1; } git fetch origin next DOCS_COMMIT=$(git merge-base HEAD origin/next) -git diff --quiet "$DOCS_COMMIT" HEAD -- docs/examples \ - || { echo "docs/examples differs from origin/next: land those changes on next first" >&2; exit 1; } LABS_TAG=v +major=${LABS_TAG#v} major=${major%%.*} +LINE= EXAMPLES=docs/examples PINFILE=docs/aztec-node.json +if [ -f "docs/lines/v$major/aztec-node.json" ]; then + LINE=v$major EXAMPLES=docs/lines/v$major/examples PINFILE=docs/lines/v$major/aztec-node.json +fi +node -p "require('./$PINFILE').ref" | grep -q "^v$major\." \ + || { echo "no dev docs follow v$major: neither main's pin nor a line (docs/lines/v$major/)" >&2; exit 1; } +git diff --quiet "$DOCS_COMMIT" HEAD -- "$EXAMPLES" \ + || { echo "$EXAMPLES differs from origin/next: land those changes on next first" >&2; exit 1; } CUT_ID= STATE=$HOME/.cache/aztec-docs-cut/$CUT_ID ! git show-ref --verify -q "refs/heads/docs-cut/$CUT_ID" && mkdir -p "${STATE%/*}" && mkdir "$STATE" \ @@ -207,7 +221,9 @@ CLI_HOME=$CLI_HOME DEST_ROOT=$(pwd) DEST=$(git rev-parse HEAD) DEST_BRANCH=$(printf %q "$(git branch --show-current)") -DEST_PIN_REF=$(node -p 'require("./docs/aztec-node.json").ref') +LINE=$LINE +EXAMPLES=$EXAMPLES +DEST_PIN_REF=$(node -p "require('./$PINFILE').ref") DOCS_COMMIT=$DOCS_COMMIT export AZTEC_NODE_DIR=$STATE/aztec-node CLI_PATH=$CUT/docs/.aztec-node/cli/node_modules/.bin:$CUT/docs/.aztec-node/bin:$STATE/aztec-node/aztec-up/bin/0.0.1 @@ -235,8 +251,10 @@ own home, and `CLI_PATH` first on `PATH`; Step 15 sets `CLI_PATH=$INSTALLER_PATH the installer's layout instead. `COMMIT_TAG` keeps its `v`: `#include_aztec_version` emits it as a git ref, and `#include_version_without_prefix` falls back to `latest` when it lacks one. -`DOCS_COMMIT` is the destination's base on `origin/next`, and the check above proves the -destination's `docs/examples/` equals it, so links to it resolve and show the snapshot's +`LINE` names the dev line a line cut promotes, and is empty for a cut of main's pages; +`DEST_PIN_REF` is the pin of the pages the cut starts from. `DOCS_COMMIT` is the +destination's base on `origin/next`, and the check above proves the destination's examples +(`$EXAMPLES`) equal it, so links to it resolve and show the snapshot's lines. `AZTEC_NODE_DIR` puts the cut's checkout outside the worktree: nargo run anywhere under `docs/` takes `docs/Nargo.toml` as its workspace, and nothing run in the checkout (2d, Step 6) may see it. @@ -249,6 +267,23 @@ git -C "$DEST_ROOT" worktree add -b "docs-cut/$CUT_ID" "$CUT" "$DEST" cd "$CUT/docs" && node scripts/aztec_node.mts pin "$LABS_TAG" ``` +**A line cut** (`$LINE` set) promotes the line instead of running that `pin`: + +```bash +. ~/.cache/aztec-docs-cut//env.sh && cd "$CUT/docs" +CUT=$CUT scripts/promote_line.sh "$LINE" "$LABS_TAG" && + git -C "$CUT" add -- docs && git -C "$CUT" commit -q -m "docs: promote the $LINE line at $LABS_TAG" && + node scripts/aztec_node.mts fetch +``` + +`promote_line.sh` refuses, before writing anything, unless it runs in `$CUT`, `docs/` is +clean, and the tag has the line's major and is on aztec-node's `$LINE` branch. Then it pins +the line to the tag (`aztec_node.mts --line $LINE pin`, which also installs the line's +l1-artifacts), replaces `docs-developers/`, `docs-operate/` and both sidebars with the +line's, and copies the line's pin over `docs/aztec-node.json`. That promotion commit is +release-only (Step 17). `fetch` then checks main's checkout out at that pin and reports the +same drift as `pin` below. + `pin` checks the tag's commit out into `$AZTEC_NODE_DIR` (a shallow clone without aztec-node's own `docs/`), writes the commit and the tag's `BB_VERSION` and `NOIR_VERSION` into `docs/aztec-node.json`, then prints every remaining drift with its fix and exits 1 @@ -323,7 +358,7 @@ yet run by a cut): `cd "$AZTEC_NODE_DIR" && env -u root -u ci3 AZTEC_TOOLCHAIN_F node scripts/aztec_node.mts check && echo "pin: ok" docs_env sh -c 'echo "$AZTEC_NODE_REF $AZTEC_NODE_COMMIT DOCS_COMMIT=$DOCS_COMMIT"; "$NARGO" --version; "$BB" --version' node -p 'require("./node_modules/@aztec-foundation/l1-artifacts/package.json").version' -git -C "$CUT" diff --quiet "$DOCS_COMMIT" HEAD -- docs/examples && echo "examples: ok" +git -C "$CUT" diff --quiet "$DOCS_COMMIT" HEAD -- "$EXAMPLES" && echo "examples: ok" ``` All of these must hold; stop at the first that does not: @@ -335,7 +370,7 @@ All of these must hold; stop at the first that does not: `ls-remote` printed. `NARGO` and `BB` are set (`env` exports them only once provisioned at the pinned versions) and print `nargo version = $NOIR_VERSION` and `$BB_VERSION`. - The installed l1-artifacts version equals `BB_VERSION`. -- `examples: ok`: the cut's `docs/examples/` is `DOCS_COMMIT`'s. +- `examples: ok`: the cut's examples (`$EXAMPLES`) are `DOCS_COMMIT`'s. ### Unversioned root pages @@ -829,7 +864,7 @@ only into the cut. `docs:version` snapshots `processed-docs/` (the resolved tree the docs plugins serve), not the raw `docs-*` source, so preprocess first with every release variable. Snippets under `labs/` resolve against the cut's checkout (the tag) and the rest against the cut's -`docs/examples/` (`DOCS_COMMIT`'s). A snippet whose file or marker the tag lacks fails +examples (`$EXAMPLES`, `DOCS_COMMIT`'s). A snippet whose file or marker the tag lacks fails preprocessing: its page quotes code newer than the release. Edit that page in the cut for the release (a release-only change, Step 17), or stop and ask the owner when it documents a feature the release lacks. @@ -857,7 +892,7 @@ grep -rhoE 'github\.com/AztecProtocol/aztec-packages(/(blob|tree)/[^/)"]+(/[^/)" version (as at `v6.0.0-rc.1`), yet this repository's tag of that name holds no Labs code. - Under `noir-projects/` or `yarn-project/`: Labs code, so in the snapshot it names aztec-node (`https://github.com/aztec-labs-eng/aztec-node`) at the tag. - - Under `docs/examples`: this repository's examples, which the snapshot quotes at + - Under `docs/examples` (a line cut's `docs/lines//examples`): this repository's examples, which the snapshot quotes at `DOCS_COMMIT`. Clone steps are the usual hit: the source clones aztec-packages `next`, and the snapshot's clone must check out `DOCS_COMMIT` after cloning (fix the snapshot after 11b, not the source). @@ -875,11 +910,12 @@ grep -rhoE 'github\.com/AztecProtocol/aztec-packages(/(blob|tree)/[^/)"]+(/[^/)" . ~/.cache/aztec-docs-cut//env.sh && cd "$CUT/docs" docs_env sh -c 'echo "aztec-node: $AZTEC_NODE_COMMIT aztec-packages: $DOCS_COMMIT"' grep -rhoE 'github\.com/aztec-labs-eng/aztec-node/blob/[^/)"]+' processed-docs/ | sort | uniq -c -grep -rhoE 'github\.com/AztecProtocol/aztec-packages/blob/[^/)"]+/docs/examples' processed-docs/ | sort | uniq -c +grep -rhoE 'github\.com/AztecProtocol/aztec-packages/blob/[^/)"]+/docs/(lines/[^/]+/)?examples' processed-docs/ | sort | uniq -c ``` -Every aztec-node link must name `AZTEC_NODE_COMMIT` and every `docs/examples` link -`DOCS_COMMIT`. `include_code.js` writes snippet links with exactly those commits, so any +Every aztec-node link must name `AZTEC_NODE_COMMIT` and every examples link +`DOCS_COMMIT`; in a line cut, the promoted pages' example links name +`docs/lines/$LINE/examples`. `include_code.js` writes snippet links with exactly those commits, so any other ref is a hand-written link: give a moving ref (`main`, `next`) the tag in the snapshot after 11b. @@ -981,12 +1017,9 @@ git -C "$AZTEC_NODE_HISTORY" log --oneline --left-right --cherry-pick --name-onl Skip this step when the two refs name the same commit. `--cherry-pick` drops commits that reached both sides as patch-equivalent ports. A hand backport adapted to its line is not -patch-equivalent and shows on both sides: judge it against `$AZTEC_NODE_DIR`, the tag. A -destination pinned to a v7 nightly would list every v7 commit since `v6` left `main` as -`>`, and once `docs/examples/` use v7-only APIs Step 15 fails for a v6.x cut and stops: no -snapshot edit can fix the examples. That is why the dev pin stays on a v6 nightly -(`docs/CLAUDE.md`); if the destination's pin is a v7 tag, stop and report before cutting. -The history goes into a checkout of its own, fetched afresh +patch-equivalent and shows on both sides: judge it against `$AZTEC_NODE_DIR`, the tag. +`DEST_PIN_REF` is the pin of the pages the cut started from, the line's in a line cut, so +both refs are on one aztec-node branch. The history goes into a checkout of its own, fetched afresh down to the refs' merge base, which GitHub names without local history; fetching it into `$AZTEC_NODE_DIR` instead keeps that checkout's earlier shallow boundaries (the merge base goes missing) and shortens the history Step 8 fetched. The check compares the merge base @@ -1146,6 +1179,23 @@ the Aztec.js examples, which are source-only. Long-running and install-heavy. the error. Never edit `docs/examples/` in the cut (Step 17 refuses it): a fix that also holds at the destination's pin lands on `next` first, and the cut restarts. + **A line cut** builds and runs the line's examples instead, in a staged copy that + `DOCS_LINE_STAGE` keeps across the commands (`examples/bootstrap.sh --line` stages one + outside the repository, since nargo would take `docs/Nargo.toml` as the workspace): + + ```bash + . ~/.cache/aztec-docs-cut//env.sh && . "$STATE/network.env" && cd "$CUT/docs" + export DOCS_LINE_STAGE=$STATE/line-examples + examples/bootstrap.sh --line "$LINE" compile-circuits + examples/bootstrap.sh --line "$LINE" compile + PATH="$CLI_HOME/.aztec/current/internal-bin:$PATH" examples/bootstrap.sh --line "$LINE" compile-solidity + TXE_PORT= examples/bootstrap.sh --line "$LINE" test-contracts + docs_env "$DOCS_LINE_STAGE/examples/ts/aztecjs_runner/run.sh" + ``` + + The line's pin is the tag (Step 2c), so these run at the tag like main's. Never edit + `docs/lines/$LINE/examples/` in the cut either: its fixes land on `next` first. + 5. **Stop the network**, which kills only its process group and releases its ports: ```bash @@ -1254,19 +1304,22 @@ Repeat it for `operate` and ``. ### Step 17: Carry the Results Back to the Destination First check that the destination's `docs/` has not moved since Step 2b, then record the -cut on its branch, check that it left `docs/examples/` as `DOCS_COMMIT` has it (the -snapshot's example links name that commit), and list every change the cut made after its -pin commit (Step 2c): +cut on its branch, check that it left the examples (`$EXAMPLES`) as `DOCS_COMMIT` has them +(the snapshot's example links name that commit), and list every change the cut made after +its pin commit (Step 2c): ```bash . ~/.cache/aztec-docs-cut//env.sh git -C "$DEST_ROOT" diff --quiet "$DEST" -- docs || { echo "destination docs/ moved: restart the cut" >&2; exit 1; } cd "$CUT" && git add -- docs && git commit -m "docs: cut $LABS_TAG" -git -C "$CUT" diff --quiet "$DOCS_COMMIT" HEAD -- docs/examples \ - || { echo "the cut changed docs/examples: land the change on next and restart the cut" >&2; exit 1; } +git -C "$CUT" diff --quiet "$DOCS_COMMIT" HEAD -- "$EXAMPLES" \ + || { echo "the cut changed $EXAMPLES: land the change on next and restart the cut" >&2; exit 1; } git -C "$CUT" diff --name-status "$PINNED" HEAD -- docs ``` +A line cut's promotion commit precedes `$PINNED`, so the list never shows it: it stays in +the cut. + Sort every listed path into exactly one class: 1. **Release results**, copied per named path. Directories are synced with @@ -1305,8 +1358,23 @@ Sort every listed path into exactly one class: Read the patch before applying it. Never copy a source directory wholesale: the cut's copy carries release-only edits. -3. **Release-only changes** stay in the cut: the pin commit (`docs/aztec-node.json`, - `docs/package.json`, `docs/yarn.lock`), generated source pages (`aztec_js_reference.md`, + In a line cut the cut's `docs-developers/`, `docs-operate/` and sidebars are the line's + pages, so their repairs belong to the destination's `docs/lines/$LINE/`. Write their + patch relative to `docs/` and apply it under the line: + + ```bash + git -C "$CUT" diff --relative=docs "$PINNED" HEAD -- ... >"$STATE/line-repairs.patch" + git -C "$DEST_ROOT" apply -3 --directory="docs/lines/$LINE" "$STATE/line-repairs.patch" + ``` + + A release's migration-notes and operator-changelog entries (Steps 8 and 10b) also go into + main's copies, since readers of the next major upgrade from this release + (`docs/CLAUDE.md`): apply the same hunks to `docs/docs-developers/` and + `docs/docs-operate/`, resolving by hand where main's copy differs. Other repairs reach + main's pages only when the code they describe is the same at main's pin (the same rule). + +3. **Release-only changes** stay in the cut: the promotion and pin commits + (`docs/aztec-node.json`, `docs/package.json`, `docs/yarn.lock`, a line's pin), generated source pages (`aztec_js_reference.md`, the CLI references, `node-api-reference.md`), and source edits that only the release needs (Steps 8b, 11 and 12). The destination keeps its own pin and the pages generated from it. diff --git a/.claude/skills/release-network-docs/SKILL.md b/.claude/skills/release-network-docs/SKILL.md index dc958c42d8d..66100b9df21 100644 --- a/.claude/skills/release-network-docs/SKILL.md +++ b/.claude/skills/release-network-docs/SKILL.md @@ -63,9 +63,14 @@ The rules of `/release-docs` (its "Release model" section) apply unchanged: same name are unrelated Foundation commits. - Toolchain and package versions come from the pin (`node docs/scripts/aztec_node.mts env`). - The cut runs in its own worktree (`$CUT`) created from the destination commit and moves - the cut's pin to the Labs tag; the destination, and its pin, are not touched until Step 10. -- A release build links `docs/examples/` snippets to this repository at `DOCS_COMMIT`, the - destination's base on `origin/next`, and the cut never changes `docs/examples/`. + the cut's pin to the Labs tag; the destination, and its pins, are not touched until Step 10. +- A release whose major has a dev line (`docs/lines/v/`: a v6.x release while + `docs/lines/v6/` exists) is a **line cut**: Step 2c promotes the line into the cut's main + paths (`docs/scripts/promote_line.sh`), and every later step reads the line's operator + pages and pin there (`docs/CLAUDE.md`, "Two dev lines: change both"). +- A release build links example snippets (`$EXAMPLES`: `docs/examples/`, or a line cut's + `docs/lines//examples/`) to this repository at `DOCS_COMMIT`, the destination's base + on `origin/next`, and the cut never changes them. - State lives in `~/.cache/aztec-docs-cut//`, whose `env.sh` every command block from Step 2 on sources. `` is `network-v`, or another name (`dryrun-network-v`) for a rehearsal or a second cut of the same @@ -147,9 +152,16 @@ git diff --quiet --ignore-submodules=all && git diff --cached --quiet --ignore-s || { echo "commit or discard the changes first" >&2; exit 1; } git fetch origin next DOCS_COMMIT=$(git merge-base HEAD origin/next) -git diff --quiet "$DOCS_COMMIT" HEAD -- docs/examples \ - || { echo "docs/examples differs from origin/next: land those changes on next first" >&2; exit 1; } LABS_TAG=v +major=${LABS_TAG#v} major=${major%%.*} +LINE= EXAMPLES=docs/examples PINFILE=docs/aztec-node.json +if [ -f "docs/lines/v$major/aztec-node.json" ]; then + LINE=v$major EXAMPLES=docs/lines/v$major/examples PINFILE=docs/lines/v$major/aztec-node.json +fi +node -p "require('./$PINFILE').ref" | grep -q "^v$major\." \ + || { echo "no dev docs follow v$major: neither main's pin nor a line (docs/lines/v$major/)" >&2; exit 1; } +git diff --quiet "$DOCS_COMMIT" HEAD -- "$EXAMPLES" \ + || { echo "$EXAMPLES differs from origin/next: land those changes on next first" >&2; exit 1; } CUT_ID= STATE=$HOME/.cache/aztec-docs-cut/$CUT_ID ! git show-ref --verify -q "refs/heads/docs-cut/$CUT_ID" && mkdir -p "${STATE%/*}" && mkdir "$STATE" \ @@ -166,7 +178,9 @@ CLI_HOME=$CLI_HOME DEST_ROOT=$(pwd) DEST=$(git rev-parse HEAD) DEST_BRANCH=$(printf %q "$(git branch --show-current)") -DEST_PIN_REF=$(node -p 'require("./docs/aztec-node.json").ref') +LINE=$LINE +EXAMPLES=$EXAMPLES +DEST_PIN_REF=$(node -p "require('./$PINFILE').ref") DOCS_COMMIT=$DOCS_COMMIT export AZTEC_NODE_DIR=$STATE/aztec-node CLI_PATH=$CUT/docs/.aztec-node/cli/node_modules/.bin:$CUT/docs/.aztec-node/bin:$STATE/aztec-node/aztec-up/bin/0.0.1 @@ -188,7 +202,8 @@ sets the other type's tag to the version its network runs, or is empty. `COMMIT_ keeps its `v` (the preprocessor emits it as a git ref). `docs_env` checks the cut's pin and checkout and exports the pin's versions before running a docs command; it fails when they disagree. `AZTEC_NODE_DIR` keeps the cut's checkout outside the worktree, as -`/release-docs` Step 2b explains. +`/release-docs` Step 2b explains. `LINE` names the line a line cut promotes (empty +otherwise), and `DEST_PIN_REF` is the pin of the pages the cut starts from. #### 2c. Create the worktree and move its pin to the tag @@ -198,6 +213,16 @@ git -C "$DEST_ROOT" worktree add -b "docs-cut/$CUT_ID" "$CUT" "$DEST" cd "$CUT/docs" && node scripts/aztec_node.mts pin "$LABS_TAG" ``` +**A line cut** (`$LINE` set) promotes the line instead of running that `pin`, as +`/release-docs` Step 2c explains: + +```bash +. ~/.cache/aztec-docs-cut//env.sh && cd "$CUT/docs" +CUT=$CUT scripts/promote_line.sh "$LINE" "$LABS_TAG" && + git -C "$CUT" add -- docs && git -C "$CUT" commit -q -m "docs: promote the $LINE line at $LABS_TAG" && + node scripts/aztec_node.mts fetch +``` + `pin` checks the tag out into `$AZTEC_NODE_DIR`, writes its commit and toolchain into `docs/aztec-node.json`, and prints every remaining drift with its fix. It needs `gh` logged in (tested with 2.85.0): it records the sha256 of each noir archive that `gh attestation @@ -217,7 +242,7 @@ yarn install node scripts/aztec_node.mts check && echo "pin: ok" git add aztec-node.json package.json yarn.lock && git commit -m "docs: pin aztec-node $LABS_TAG" echo "PINNED=$(git rev-parse HEAD)" >>"$STATE/env.sh" -git -C "$CUT" diff --quiet "$DOCS_COMMIT" HEAD -- docs/examples && echo "examples: ok" +git -C "$CUT" diff --quiet "$DOCS_COMMIT" HEAD -- "$EXAMPLES" && echo "examples: ok" ``` `check` must print `pin: ok` (the checkout is the tag's commit and clean, and the pin's bb @@ -423,7 +448,8 @@ grep -rhoE 'github\.com/AztecProtocol/aztec-packages(/(blob|tree)/[^/)"]+(/[^/)" gets the tag in the snapshot. - Classify every hit of the last command by path, as `/release-docs` Step 11 does: Labs code (`noir-projects/`, `yarn-project/`) names aztec-node at the tag in the snapshot, - `docs/examples` names `DOCS_COMMIT`, Foundation code (`l1-contracts/`, `barretenberg/`) + the examples (`docs/examples`, or a line cut's `docs/lines//examples`) name + `DOCS_COMMIT`, Foundation code (`l1-contracts/`, `barretenberg/`) takes `#include_foundation_version`, and anything else `v` or `next`. @@ -481,7 +507,8 @@ git -C "$AZTEC_NODE_HISTORY" log --oneline --left-right --cherry-pick --name-onl Never fetch with `--shallow-exclude=`: the merge base is usually reachable from it, so it is cut off and the range lists both sides back to the boundary (v6.0.0 against `v6.0.0-rc.3`: 166/166 commits instead of 57/2). This fetch needs no previous -release, so it also works when that is not a Labs tag. The separate history checkout and +release, so it also works when that is not a Labs tag. In a line cut `DEST_PIN_REF` is the +line's pin, so both refs are on one aztec-node branch. The separate history checkout and a failed count check work as in `/release-docs` Step 12. So do hand backports: one adapted to its line (`v6`) is not patch-equivalent, so `--cherry-pick` keeps it on both sides; judge it against `$AZTEC_NODE_DIR`, the tag. @@ -572,11 +599,14 @@ fixed goes to the owner. . ~/.cache/aztec-docs-cut//env.sh git -C "$DEST_ROOT" diff --quiet "$DEST" -- docs || { echo "destination docs/ moved: restart the cut" >&2; exit 1; } cd "$CUT" && git add -- docs && git commit -m "docs: cut $LABS_TAG network docs" -git -C "$CUT" diff --quiet "$DOCS_COMMIT" HEAD -- docs/examples \ - || { echo "the cut changed docs/examples: land the change on next and restart the cut" >&2; exit 1; } +git -C "$CUT" diff --quiet "$DOCS_COMMIT" HEAD -- "$EXAMPLES" \ + || { echo "the cut changed $EXAMPLES: land the change on next and restart the cut" >&2; exit 1; } git -C "$CUT" diff --name-status "$PINNED" HEAD -- docs ``` +A line cut's promotion commit precedes `$PINNED`, so the list never shows it: it stays in +the cut. + Sort every listed path into exactly one class: 1. **Release results**, copied per named path, with the Step 8 deletions run explicitly: @@ -605,8 +635,22 @@ Sort every listed path into exactly one class: git -C "$DEST_ROOT" apply -3 "$STATE/shared-repairs.patch" ``` -3. **Release-only changes** stay in the cut: the pin commit (`docs/aztec-node.json`, - `docs/package.json`, `docs/yarn.lock`), the regenerated `node-api-reference.md` and + In a line cut the cut's `docs-operate/` and `sidebars-operate.js` are the line's, so + their repairs belong to the destination's `docs/lines/$LINE/`: + + ```bash + git -C "$CUT" diff --relative=docs "$PINNED" HEAD -- ... >"$STATE/line-repairs.patch" + git -C "$DEST_ROOT" apply -3 --directory="docs/lines/$LINE" "$STATE/line-repairs.patch" + ``` + + An operator-changelog entry for the release also goes into main's copy + (`docs/docs-operate/`), since operators of the next major upgrade from it + (`docs/CLAUDE.md`): apply the same hunks there and resolve by hand where main's copy + differs. Other repairs reach main's pages only when the behaviour they describe is the same + at main's pin. + +3. **Release-only changes** stay in the cut: the promotion and pin commits + (`docs/aztec-node.json`, `docs/package.json`, `docs/yarn.lock`, a line's pin), the regenerated `node-api-reference.md` and `cli-reference.md` sources, and source edits only the release needs (Steps 6 and 7a). A destination commit cherry-picked into the cut also shows in this list: it is already on diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 3afebc37832..71d1758e904 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -204,7 +204,7 @@ The `examples/` directory contains runnable code examples that are included in d **Validation (type-checking)**: `examples/bootstrap.sh` compiles Noir circuits, Noir contracts, Solidity, and type-checks TypeScript examples. This runs on every PR. -`examples/bootstrap.sh --line ` builds a line's examples the same way, in a copy staged outside the repository (nargo would take `docs/Nargo.toml` as the workspace). Run it whole: the stage is new every run, so a single step such as `validate-ts` finds no compiled contracts. A line's examples run against a network only at its release cut, never in CI. +`examples/bootstrap.sh --line ` builds a line's examples the same way, in a copy staged outside the repository (nargo would take `docs/Nargo.toml` as the workspace). Run it whole: the stage is new every run, so a single step such as `validate-ts` finds no compiled contracts, unless `DOCS_LINE_STAGE=` keeps one stage across runs (a release cut does). A line's examples run against a network only at its release cut, never in CI. The Solidity examples compile against the l1-contracts sources shipped in the `@aztec-foundation/l1-artifacts` devDependency (see `examples/solidity/foundry.toml`). Its version in `package.json` must equal `toolchain.l1Artifacts` in `aztec-node.json` (the pinned aztec-node's `BB_VERSION`); `node scripts/aztec_node.mts check` fails until both move together. diff --git a/docs/examples/bootstrap.sh b/docs/examples/bootstrap.sh index b8bdb84e5aa..539ac5ac9ba 100755 --- a/docs/examples/bootstrap.sh +++ b/docs/examples/bootstrap.sh @@ -40,15 +40,22 @@ if [ "$cmd" = --line ]; then exit 1 fi aztec_node+=(--line "$DOCS_LINE") - # On disk rather than in /tmp: the contract artifacts are large. - mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" - STAGE=$(mktemp -d "${XDG_CACHE_HOME:-$HOME/.cache}/aztec-docs-$DOCS_LINE.XXXXXX") - cp -R "$line_root/Nargo.toml" "$line_root/examples" "$REPO_ROOT/docs/.yarnrc.yml" "$STAGE/" + # On disk rather than in /tmp: the contract artifacts are large. A release cut runs a line's + # examples one step at a time, so DOCS_LINE_STAGE names a stage it keeps across runs (and removes + # itself): later steps find what earlier ones compiled. + if [ -n "${DOCS_LINE_STAGE:-}" ]; then + stage=$DOCS_LINE_STAGE + else + mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" + STAGE=$(mktemp -d "${XDG_CACHE_HOME:-$HOME/.cache}/aztec-docs-$DOCS_LINE.XXXXXX") + stage=$STAGE + fi + mkdir -p "$stage/node_modules/@aztec-foundation" + cp -R "$line_root/Nargo.toml" "$line_root/examples" "$REPO_ROOT/docs/.yarnrc.yml" "$stage/" # The links may dangle until pin_toolchain fetches the line. - mkdir -p "$STAGE/node_modules/@aztec-foundation" - ln -s "$line_root/.aztec-node" "$STAGE/.aztec-node" - ln -s "$line_root/.aztec-node/l1-artifacts" "$STAGE/node_modules/@aztec-foundation/l1-artifacts" - export DOCS_ROOT=$STAGE + ln -sfn "$line_root/.aztec-node" "$stage/.aztec-node" + ln -sfn "$line_root/.aztec-node/l1-artifacts" "$stage/node_modules/@aztec-foundation/l1-artifacts" + export DOCS_ROOT=$stage else export DOCS_PATH=docs DOCS_ROOT=$REPO_ROOT/docs fi From 92067dd3487f8d17c88f7ef5afbcd4de0dae0c06 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 19:46:11 +0000 Subject: [PATCH 22/32] docs(skills): close the gaps a v6 line-cut rehearsal found - 2a asks for the peeled ref, so 2e compares the commit of an annotated (nightly) tag. - A line cut's pin commit is skipped when nothing drifted: the promotion already carries the pin. - The 8b sweep greps the examples the cut's pages quote ($EXAMPLES). - The changelog checks (10b, 5c) read the line's copy in a line cut. - A nightly is covered by its major's changelog page; a minor release needs its entry first. --- .claude/skills/release-docs/SKILL.md | 37 ++++++++++++-------- .claude/skills/release-network-docs/SKILL.md | 22 +++++++----- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/.claude/skills/release-docs/SKILL.md b/.claude/skills/release-docs/SKILL.md index 09f59100a86..e7182d66b17 100644 --- a/.claude/skills/release-docs/SKILL.md +++ b/.claude/skills/release-docs/SKILL.md @@ -178,11 +178,13 @@ reports in Step 1, and run Step 11b's `update_docs_versions.sh` once per type. #### 2a. The Labs tag must exist ```bash -git ls-remote --exit-code --tags https://github.com/aztec-labs-eng/aztec-node.git refs/tags/v +git ls-remote --exit-code --tags https://github.com/aztec-labs-eng/aztec-node.git \ + "refs/tags/v" "refs/tags/v^{}" ``` **Abort if it exits non-zero**: the release has not been tagged. Ask aztec-node's remote, -never this repository, whose `v6…` tags are unrelated. +never this repository, whose `v6…` tags are unrelated. A release tag prints its commit; an +annotated tag (nightlies are) prints the tag object and, on its `^{}` line, the commit. #### 2b. Record the destination @@ -282,7 +284,9 @@ the line to the tag (`aztec_node.mts --line $LINE pin`, which also installs the l1-artifacts), replaces `docs-developers/`, `docs-operate/` and both sidebars with the line's, and copies the line's pin over `docs/aztec-node.json`. That promotion commit is release-only (Step 17). `fetch` then checks main's checkout out at that pin and reports the -same drift as `pin` below. +same drift as `pin` below. The promotion commit already carries the pin, so the pin commit +below has something to commit only when l1-artifacts drifted; otherwise `PINNED` is the +promotion commit. `pin` checks the tag's commit out into `$AZTEC_NODE_DIR` (a shallow clone without aztec-node's own `docs/`), writes the commit and the tag's `BB_VERSION` and `NOIR_VERSION` @@ -304,7 +308,8 @@ node -e ' yarn install node scripts/aztec_node.mts provision nargo bb node scripts/aztec_node.mts check -git add aztec-node.json package.json yarn.lock && git commit -m "docs: pin aztec-node $LABS_TAG" +git add aztec-node.json package.json yarn.lock && + { git diff --cached --quiet || git commit -m "docs: pin aztec-node $LABS_TAG"; } echo "PINNED=$(git rev-parse HEAD)" >>"$STATE/env.sh" ``` @@ -367,7 +372,7 @@ All of these must hold; stop at the first that does not: file, the pin's bb and noir equal the tag's `BB_VERSION` and `NOIR_VERSION`, and `docs/package.json`'s l1-artifacts equals `BB_VERSION`. - `AZTEC_NODE_REF` is `v` and `AZTEC_NODE_COMMIT` is the commit 2a's - `ls-remote` printed. `NARGO` and `BB` are set (`env` exports them only once provisioned at + `ls-remote` printed (its `^{}` line for an annotated tag). `NARGO` and `BB` are set (`env` exports them only once provisioned at the pinned versions) and print `nargo version = $NOIR_VERSION` and `$BB_VERSION`. - The installed l1-artifacts version equals `BB_VERSION`. - `examples: ok`: the cut's examples (`$EXAMPLES`) are `DOCS_COMMIT`'s. @@ -613,16 +618,17 @@ aztec-node marks breaking changes only with the conventional-commit `!`, never a body of port commits (`chore: port … from monorepo`), which list the breaking commits they carry: read those bodies too. -**2. Locate candidate usages.** For each triple, grep the cut's source docs: +**2. Locate candidate usages.** For each triple, grep the cut's source docs and the examples +its pages quote (`$EXAMPLES`): ```bash -cd ~/.cache/aztec-docs-cut//worktree/docs -grep -rn "" docs-developers/ docs-operate/ docs-participate/ docs/ examples/ +. ~/.cache/aztec-docs-cut//env.sh && cd "$CUT/docs" +grep -rn "" docs-developers/ docs-operate/ docs-participate/ docs/ "${EXAMPLES#docs/}/" ``` Prioritize surfaces with no compiler gate: hand-written code blocks (most of them in `docs-developers/docs/aztec-nr/` and `docs-operate/`), prose that names functions or flags, -and install or CLI commands. A hit in an `#include_code` target or `docs/examples/` usually +and install or CLI commands. A hit in an `#include_code` target or the examples usually means the build already fails; treat those as cross-checks. **3. Adjudicate every candidate; do not fix from grep hits alone.** A symbol grep is noisy @@ -835,21 +841,24 @@ of a major the page usually lands after the tag. . ~/.cache/aztec-docs-cut//env.sh major=$({ jq -r '."."' "$AZTEC_NODE_DIR/.version-manifest.json" 2>/dev/null || jq -r '."."' "$AZTEC_NODE_DIR/.release-please-manifest.json"; } | cut -d. -f1) -git -C "$DEST_ROOT" show "$DEST:docs/docs-operate/operators/reference/changelog/v$major.md" >/dev/null \ +src=docs${LINE:+/lines/$LINE} +git -C "$DEST_ROOT" show "$DEST:$src/docs-operate/operators/reference/changelog/v$major.md" >/dev/null \ && echo "page present" || echo "MISSING: v$major.md" -git -C "$DEST_ROOT" show "$DEST:docs/docs-operate/operators/reference/changelog/index.md" | grep -n '^### ' | head -3 -git -C "$DEST_ROOT" show "$DEST:docs/sidebars-operate.js" | grep -n "changelog/v$major" +git -C "$DEST_ROOT" show "$DEST:$src/docs-operate/operators/reference/changelog/index.md" | grep -n '^### ' | head -3 +git -C "$DEST_ROOT" show "$DEST:$src/sidebars-operate.js" | grep -n "changelog/v$major" ``` The major is the Labs version, read from the manifest in the checkout; the root manifest versions the Foundation. aztec-node renamed that file, so the command reads whichever name the -pinned commit carries. +pinned commit carries. A line cut checks the line's copy (`$src`), which it promoted. All three must hold: 1. The page exists (a major with several releases may use a minor-qualified name such as `v5.2.md`; check that name). 2. `index.md` has a `### ` entry for this release's major (and minor, once the series has - more than one); an rc or patch is covered by its minor's entry. + more than one); an rc or patch is covered by its minor's entry, and a nightly by its + major's page. A minor release such as `6.1.0` needs its `### [v6.1.x]` entry landed + first. 3. `docs/sidebars-operate.js` lists the page; an unlisted page is published but has no nav entry. diff --git a/.claude/skills/release-network-docs/SKILL.md b/.claude/skills/release-network-docs/SKILL.md index 66100b9df21..7f50e88e800 100644 --- a/.claude/skills/release-network-docs/SKILL.md +++ b/.claude/skills/release-network-docs/SKILL.md @@ -240,14 +240,17 @@ node -e ' ' yarn install node scripts/aztec_node.mts check && echo "pin: ok" -git add aztec-node.json package.json yarn.lock && git commit -m "docs: pin aztec-node $LABS_TAG" +git add aztec-node.json package.json yarn.lock && + { git diff --cached --quiet || git commit -m "docs: pin aztec-node $LABS_TAG"; } echo "PINNED=$(git rev-parse HEAD)" >>"$STATE/env.sh" git -C "$CUT" diff --quiet "$DOCS_COMMIT" HEAD -- "$EXAMPLES" && echo "examples: ok" ``` `check` must print `pin: ok` (the checkout is the tag's commit and clean, and the pin's bb and `docs/package.json`'s l1-artifacts equal the tag's `BB_VERSION`), and `examples: ok` -must follow. Never initialize `labs/` in the cut and run no root `make` target there. The +must follow. A line cut's promotion commit already carries the pin, so the pin commit has +something to commit only when l1-artifacts drifted; otherwise `PINNED` is the promotion +commit. Never initialize `labs/` in the cut and run no root `make` target there. The release inputs (`docs/docs-operate/`, `docs/sidebars-operate.js`) are the destination's content; `docs/docs/networks.md` and the existing snapshots are the destination's live copies, which Step 10 copies back. @@ -403,16 +406,19 @@ page usually lands after the tag, so check the destination commit: . ~/.cache/aztec-docs-cut//env.sh major=$({ jq -r '."."' "$AZTEC_NODE_DIR/.version-manifest.json" 2>/dev/null || jq -r '."."' "$AZTEC_NODE_DIR/.release-please-manifest.json"; } | cut -d. -f1) -git -C "$DEST_ROOT" show "$DEST:docs/docs-operate/operators/reference/changelog/v$major.md" >/dev/null \ +src=docs${LINE:+/lines/$LINE} +git -C "$DEST_ROOT" show "$DEST:$src/docs-operate/operators/reference/changelog/v$major.md" >/dev/null \ && echo "page present" || echo "MISSING: v$major.md" -git -C "$DEST_ROOT" show "$DEST:docs/docs-operate/operators/reference/changelog/index.md" | grep -n '^### ' | head -3 -git -C "$DEST_ROOT" show "$DEST:docs/sidebars-operate.js" | grep -n "changelog/v$major" +git -C "$DEST_ROOT" show "$DEST:$src/docs-operate/operators/reference/changelog/index.md" | grep -n '^### ' | head -3 +git -C "$DEST_ROOT" show "$DEST:$src/sidebars-operate.js" | grep -n "changelog/v$major" ``` The page exists (or its minor-qualified name, such as `v5.2.md`), `index.md` has a `### ` -entry for this release's major (and minor, once the series has more than one), and the -sidebar lists the page. The cut started from the destination, so its copy is the -destination's. +entry for this release's major (and minor, once the series has more than one; a nightly is +covered by its major's page, and a minor release such as `6.1.0` needs its `### [v6.1.x]` +entry landed first), and the sidebar lists the page. The cut started from the destination, +so its copy is the destination's; a line cut checks the line's copy (`$src`), which it +promoted. **Owner approval required.** If any check fails, stop and report which: the page lands on the destination first (`/updating-changelog`), through a PR the owner opens and merges, From 27d3f008167817b2587004b2edd60c2ea5d5a95d Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 20:48:26 +0000 Subject: [PATCH 23/32] fix(docs): resolve #api_ref_version to the API folder the build serves A testnet build named the testnet/ API folder even while testnet and mainnet read one version, when the build serves that version's reference under mainnet/ only. A network docs cut builds exactly that way, and its agent readiness check failed on aztec_js_reference.md's link to /typescript-api/testnet/llms.txt. The macro now takes the folder apiReferenceDirs serves for the network's version; line pages keep the folder of the network on their major, and nightly and devnet keep next. --- docs/src/__tests__/network_docs.test.js | 7 +++++++ docs/src/network_docs.js | 14 ++++++++++++++ .../preprocess/__tests__/include_version.test.js | 10 ++++++++-- docs/src/preprocess/include_version.js | 10 +++++----- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/docs/src/__tests__/network_docs.test.js b/docs/src/__tests__/network_docs.test.js index 0e3bf38f24b..ebccf37200b 100644 --- a/docs/src/__tests__/network_docs.test.js +++ b/docs/src/__tests__/network_docs.test.js @@ -10,6 +10,7 @@ const { lineApiFolder, lineInstances, llmsRoutes, + networkApiFolder, networkSection, startHere, startHereLinks, @@ -96,6 +97,12 @@ describe("Aztec.nr API folders", () => { }); }); + it("give each network the folder of its version, shared when the networks share one", () => { + const folders = (config) => ["mainnet", "testnet", "devnet"].map((type) => networkApiFolder(config, type)); + assert.deepStrictEqual(folders(shared), ["mainnet", "mainnet", undefined]); + assert.deepStrictEqual(folders(split), ["mainnet", "testnet", undefined]); + }); + it("send the API page of every released version to a folder that exists, whatever its label", () => { const config = require("../../developer_version_config.json"); const [developer] = docsInstances(config, config, true); diff --git a/docs/src/network_docs.js b/docs/src/network_docs.js index 7b1af099c82..f708f67dda3 100644 --- a/docs/src/network_docs.js +++ b/docs/src/network_docs.js @@ -139,6 +139,19 @@ function lineApiFolders(developerConfig, lines) { ); } +/** + * The API reference folder a network's docs link: the one `apiReferenceDirs` serves for the version + * that network reads, so networks on one version share it. + * + * @param {Record} developerConfig release type → version + * @param {string} network a release type + * @returns {string | undefined} undefined for a release type no network maps (nightly, devnet) + */ +function networkApiFolder(developerConfig, network) { + const version = NETWORKS.includes(network) && developerConfig[network]; + return version ? aztecNrApiFolders(docsInstances(developerConfig, {}, true)[0])[version] : undefined; +} + /** * The API reference folders of each docs version a network reads, one per kind, in the folder * aztecNrApiFolders names (the TypeScript reference is published per network the same way). @@ -363,6 +376,7 @@ module.exports = { lineApiFolders, lineInstances, llmsRoutes, + networkApiFolder, networkSection, scopedApiIndexes, startHere, diff --git a/docs/src/preprocess/__tests__/include_version.test.js b/docs/src/preprocess/__tests__/include_version.test.js index f5c0d6a0e87..a40c9e33438 100644 --- a/docs/src/preprocess/__tests__/include_version.test.js +++ b/docs/src/preprocess/__tests__/include_version.test.js @@ -187,11 +187,17 @@ describe("preprocessIncludeVersion", () => { describe("#api_ref_version macro", () => { // Only testnet and mainnet API references are published; other builds use the generated next/. + // A network links the folder the build serves for its version, which it shares with any network + // on the same version. + const { apiReferenceDirs, docsInstances } = require("../../network_docs"); + const config = require("../../../developer_version_config.json"); + const served = (network) => + apiReferenceDirs(docsInstances(config, {}, true)[0]).find((d) => d.networks.includes(network)).folder; const testCases = [ ["nightly", "next"], ["devnet", "next"], - ["testnet", "testnet"], - ["mainnet", "mainnet"], + ["testnet", served("testnet")], + ["mainnet", served("mainnet")], ]; testCases.forEach(([releaseType, expected]) => { diff --git a/docs/src/preprocess/include_version.js b/docs/src/preprocess/include_version.js index 109df9568de..769c306b322 100644 --- a/docs/src/preprocess/include_version.js +++ b/docs/src/preprocess/include_version.js @@ -2,7 +2,7 @@ const fs = require("fs"); const path = require("path"); const { processConditionalBlocks } = require("./conditional_content"); const { releaseNameFor, versionLabel } = require("../release_names"); -const { lineApiFolders } = require("../network_docs"); +const { lineApiFolders, networkApiFolder } = require("../network_docs"); const { lineOf, readPin } = require("./line"); // Load version defaults from config file (source of truth for version→type mapping) @@ -128,10 +128,10 @@ async function preprocessIncludeVersion(markdownContent, filePath = "unknown", r ); const releaseNetwork = getReleaseNetwork(releaseType); - // API ref paths - const apiRefVersion = releaseType === "testnet" ? "testnet" - : releaseType === "mainnet" ? "mainnet" - : "next"; + // A testnet on mainnet's version has no folder of its own: the build serves that version's + // reference under mainnet/ only. + const apiRefVersion = networkApiFolder(developerVersionConfig, releaseType) + ?? (releaseType === "testnet" || releaseType === "mainnet" ? releaseType : "next"); // Step 1: Process conditional blocks FIRST (before version substitution) // This allows conditionals to contain version macros From 98f97bb68fa7f4c45861645e740984816e635bd2 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 20:52:49 +0000 Subject: [PATCH 24/32] docs(skills): give the release CLI its own homes and Node, and walk tutorials as readers meet them - The installer, aztec_cli and the local network set NARGO_HOME, AZTEC_HOME and the XDG directories inside CLI_HOME instead of clearing them: noirup's installer writes to NARGO_HOME, so one inherited from the real shell rewrote the real home's noirup. - Step 15.1 reads the Node the release's installer requires from its versions file and runs the installer and the release's commands on it, first on PATH, so the machine's default Node never changes. - Step 15.3 records the publication-day Yarn age gate against each page that hits it, then continues with a pre-approval limited to the tag's own packages; it never turns the gate off. - Step 15.3 walks a line's in-repository tutorials as written before checking their code in the staged layout, so a staged pass never hides a reader's failure. --- .claude/skills/release-docs/SKILL.md | 64 ++++++++++++++++---- .claude/skills/release-docs/local_network.sh | 14 +++-- .claude/skills/release-network-docs/SKILL.md | 5 +- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/.claude/skills/release-docs/SKILL.md b/.claude/skills/release-docs/SKILL.md index e7182d66b17..091da3ccec3 100644 --- a/.claude/skills/release-docs/SKILL.md +++ b/.claude/skills/release-docs/SKILL.md @@ -237,8 +237,9 @@ docs_env() { } aztec_cli() { [ -x $CUT/docs/.aztec-node/cli/node_modules/.bin/aztec ] || { echo "no release CLI: run Step 3" >&2; return 1; } - env -u XDG_CONFIG_HOME -u XDG_DATA_HOME -u XDG_CACHE_HOME -u NARGO_HOME -u AZTEC_HOME \\ - -u ANVIL_BIN -u FORGE_BIN -u CAST_BIN HOME=$CLI_HOME PATH=\$CLI_PATH:\$PATH "\$@" + env -u ANVIL_BIN -u FORGE_BIN -u CAST_BIN HOME=$CLI_HOME NARGO_HOME=$CLI_HOME/.nargo AZTEC_HOME=$CLI_HOME/.aztec \\ + XDG_CONFIG_HOME=$CLI_HOME/.config XDG_DATA_HOME=$CLI_HOME/.local/share XDG_CACHE_HOME=$CLI_HOME/.cache \\ + PATH=\$CLI_PATH:\$PATH "\$@" } EOF ``` @@ -1117,8 +1118,16 @@ the Aztec.js examples, which are source-only. Long-running and install-heavy. . ~/.cache/aztec-docs-cut//env.sh && cd "$CUT" pin=$(node docs/scripts/aztec_node.mts env) || exit 1 eval "$pin" - env -u XDG_CONFIG_HOME -u XDG_DATA_HOME -u XDG_CACHE_HOME -u NARGO_HOME -u NARGO -u AZTEC_HOME \ - HOME="$CLI_HOME" NON_INTERACTIVE=1 bash <(curl -fsSL "https://install.aztec.network/$AZTEC_VERSION") + need=$(curl -fsSL "https://install.aztec.network/$AZTEC_VERSION/versions" | sed -n 's/^node: //p') + NODE_BIN= + have=$("$NODE_BIN/node" --version | cut -c2-) + [ -n "$need" ] && [ "$(printf '%s\n' "$need" "$have" | sort -V | head -n1)" = "$need" ] \ + || { echo "the installer needs Node ${need:-?}; $NODE_BIN has $have" >&2; exit 1; } + printf 'NODE_BIN=%q\nINSTALLER_PATH=$INSTALLER_PATH:$NODE_BIN\n' "$NODE_BIN" >>"$STATE/env.sh" + INSTALLER_PATH=$INSTALLER_PATH:$NODE_BIN + env -u NARGO HOME="$CLI_HOME" NARGO_HOME="$CLI_HOME/.nargo" AZTEC_HOME="$CLI_HOME/.aztec" \ + XDG_CONFIG_HOME="$CLI_HOME/.config" XDG_DATA_HOME="$CLI_HOME/.local/share" XDG_CACHE_HOME="$CLI_HOME/.cache" \ + PATH="$NODE_BIN:$PATH" NON_INTERACTIVE=1 bash <(curl -fsSL "https://install.aztec.network/$AZTEC_VERSION") CLI_PATH=$INSTALLER_PATH aztec_cli aztec --version foundry=$(sed -n 's/^ENV FOUNDRY_VERSION=v//p' "$AZTEC_NODE_DIR/build-images/src/Dockerfile") for tool in anvil forge cast; do @@ -1129,11 +1138,21 @@ the Aztec.js examples, which are source-only. Long-running and install-heavy. . "$STATE/network.env" ``` + The installer requires the Node its `versions` file names (`node: 26.12.0` for v6.1 + nightlies; check, never assume). `NODE_BIN` is any directory holding at least that Node, + such as an nvm version's `bin`. It goes first on `PATH` for the installer and, through + `INSTALLER_PATH` (appended to `env.sh`), for every release command of this step, so the + machine's default Node stays as it is: never run `nvm alias default`. The installer + cannot change it either: it looks for nvm under `$HOME`, which is `$CLI_HOME`, and + without a new enough Node it stops with `Minimum Node.js version … not found`. + The CLI must report `AZTEC_VERSION`. The installer runs with `HOME` set to `$CLI_HOME`, - so its node, foundry, nargo and shell-profile edits stay out of the real home. Clearing - `NARGO_HOME` and `AZTEC_HOME` keeps noirup and the toolchain under that home too (with - `NARGO_HOME` set, noirup lands in the real home and the install fails), and clearing - `NARGO` stops the installer from copying that binary in as the release's nargo. + so its node, foundry, nargo and shell-profile edits stay out of the real home. It also + gets `NARGO_HOME`, `AZTEC_HOME` and the XDG directories inside `$CLI_HOME`, set rather + than inherited: noirup's own installer writes to `$NARGO_HOME`, and the release + installer then runs `$HOME/.nargo/bin/noirup`, so a `NARGO_HOME` from the real shell + rewrites the real home's noirup and fails the install. Clearing `NARGO` stops the + installer from copying that binary in as the release's nargo. `NON_INTERACTIVE=1` skips the prompt and the closing `exec $SHELL`. foundryup refuses to install while any `anvil` runs on the host (`'anvil' is currently running`): wait for it to exit, and never stop another run's anvil. The release's L1 deployment needs the @@ -1167,6 +1186,27 @@ the Aztec.js examples, which are source-only. Long-running and install-heavy. `$AZTEC_NODE_URL` in place of its `localhost` URLs. Record drift: renamed commands, changed flags, stale addresses, different output. + - **On the release's publication day**, a fresh project that `yarn init` gives Yarn 4 + refuses the release's packages for their first day (`All versions satisfying … are + quarantined`). Record that against every page that hits it without the + publication-day tip `token_bridge.md` carries. Then, in that walk project only, add + the tip's scoped pre-approval, limited to the tag's own packages, to its `.yarnrc.yml`: + + ```yaml + npmPreapprovedPackages: + - "@aztec-labs/*@" + ``` + + Yarn matches each entry as a package glob plus a version range, so this admits + nothing else. Never turn the gate off (`npmMinimalAgeGate: 0`, + `YARN_NPM_MINIMAL_AGE_GATE`), and never edit the repository's `.yarnrc.yml`. + - **A line cut's tutorials whose project is the line's examples** (they clone this + repository and build under `docs/lines//examples/`) may not build there: nargo + takes `docs/Nargo.toml` as the workspace, as `examples/bootstrap.sh --line` notes. + Walk each one as written first, and record every failure with its doc line. Only then + run its commands in a copy of the `DOCS_LINE_STAGE` stage of item 4, to check the code + itself; a pass there never clears the as-written failure. + 4. **Build and run the Aztec.js examples at the tag**, in the cut, against the local network: ```bash @@ -1175,15 +1215,15 @@ the Aztec.js examples, which are source-only. Long-running and install-heavy. examples/bootstrap.sh compile PATH="$CLI_HOME/.aztec/current/internal-bin:$PATH" examples/bootstrap.sh compile-solidity TXE_PORT= examples/bootstrap.sh test-contracts - docs_env examples/ts/aztecjs_runner/run.sh + PATH="$NODE_BIN:$PATH" docs_env examples/ts/aztecjs_runner/run.sh ``` `examples/bootstrap.sh` provisions the tag's nargo, bb and CLI and fetches its commit into `$CUT/docs/.aztec-node/tree` (Release model), so the circuit and contracts compile and the TXE tests run at the tag (`recursive_verification` reads the circuit's artifact); `compile-solidity` builds the artifacts `example_swap` imports, with the forge item 1 - checked. `run.sh` installs the tag's packages into each example and runs it - against `$AZTEC_NODE_URL`. The examples are the destination's: a failure here means the + checked. `run.sh` installs the tag's packages into each example and runs it, on the + installer's Node, against `$AZTEC_NODE_URL`. The examples are the destination's: a failure here means the snapshot quotes code that does not work at the release. Report it with the example and the error. Never edit `docs/examples/` in the cut (Step 17 refuses it): a fix that also holds at the destination's pin lands on `next` first, and the cut restarts. @@ -1199,7 +1239,7 @@ the Aztec.js examples, which are source-only. Long-running and install-heavy. examples/bootstrap.sh --line "$LINE" compile PATH="$CLI_HOME/.aztec/current/internal-bin:$PATH" examples/bootstrap.sh --line "$LINE" compile-solidity TXE_PORT= examples/bootstrap.sh --line "$LINE" test-contracts - docs_env "$DOCS_LINE_STAGE/examples/ts/aztecjs_runner/run.sh" + PATH="$NODE_BIN:$PATH" docs_env "$DOCS_LINE_STAGE/examples/ts/aztecjs_runner/run.sh" ``` The line's pin is the tag (Step 2c), so these run at the tag like main's. Never edit diff --git a/.claude/skills/release-docs/local_network.sh b/.claude/skills/release-docs/local_network.sh index b16833ecc53..86314d5731a 100755 --- a/.claude/skills/release-docs/local_network.sh +++ b/.claude/skills/release-docs/local_network.sh @@ -6,9 +6,10 @@ # local_network.sh start # local_network.sh stop # -# CLI_HOME is the HOME the network runs with, so nothing is written under the real home; CLI_PATH -# (prepended to PATH) holds the release's aztec CLI and the tools it runs. start waits up to READY_TIMEOUT seconds (default 600) for the node to -# report , then writes /network.env (AZTEC_NODE_URL, ETHEREUM_HOST, the ports) +# CLI_HOME is the HOME the network runs with, and holds its NARGO_HOME, AZTEC_HOME and XDG +# directories, so nothing is written under the real home; CLI_PATH (prepended to PATH) holds the +# release's aztec CLI and the Node and tools it runs. start waits up to READY_TIMEOUT seconds +# (default 600) for the node to report , then writes /network.env (AZTEC_NODE_URL, ETHEREUM_HOST, the ports) # for later commands to source; the log is /network.log. set -euo pipefail @@ -119,9 +120,10 @@ function cmd_start { open STDOUT, ">>", $log or die "$log: $!"; open STDERR, ">&", \*STDOUT; exec @ARGV or die "exec: $!"; - ' "$state/network.log" env -u XDG_CONFIG_HOME -u XDG_DATA_HOME -u XDG_CACHE_HOME -u NARGO_HOME -u AZTEC_HOME \ - -u ANVIL_BIN -u FORGE_BIN -u CAST_BIN \ - HOME="$CLI_HOME" PATH="$CLI_PATH:$PATH" \ + ' "$state/network.log" env -u ANVIL_BIN -u FORGE_BIN -u CAST_BIN \ + HOME="$CLI_HOME" NARGO_HOME="$CLI_HOME/.nargo" AZTEC_HOME="$CLI_HOME/.aztec" \ + XDG_CONFIG_HOME="$CLI_HOME/.config" XDG_DATA_HOME="$CLI_HOME/.local/share" XDG_CACHE_HOME="$CLI_HOME/.cache" \ + PATH="$CLI_PATH:$PATH" \ ANVIL_PORT="${ports[2]}" DATA_DIRECTORY="$state/network-data" P2P_ENABLED=false \ aztec start --local-network --port "${ports[0]}" --admin-port "${ports[1]}") with_registry repoint_rows "$run" "$pgid" diff --git a/.claude/skills/release-network-docs/SKILL.md b/.claude/skills/release-network-docs/SKILL.md index 7f50e88e800..5374ddb14a9 100644 --- a/.claude/skills/release-network-docs/SKILL.md +++ b/.claude/skills/release-network-docs/SKILL.md @@ -191,8 +191,9 @@ docs_env() { } aztec_cli() { [ -x $CUT/docs/.aztec-node/cli/node_modules/.bin/aztec ] || { echo "no release CLI: run Step 5b" >&2; return 1; } - env -u XDG_CONFIG_HOME -u XDG_DATA_HOME -u XDG_CACHE_HOME -u NARGO_HOME -u AZTEC_HOME \\ - -u ANVIL_BIN -u FORGE_BIN -u CAST_BIN HOME=$CLI_HOME PATH=\$CLI_PATH:\$PATH "\$@" + env -u ANVIL_BIN -u FORGE_BIN -u CAST_BIN HOME=$CLI_HOME NARGO_HOME=$CLI_HOME/.nargo AZTEC_HOME=$CLI_HOME/.aztec \\ + XDG_CONFIG_HOME=$CLI_HOME/.config XDG_DATA_HOME=$CLI_HOME/.local/share XDG_CACHE_HOME=$CLI_HOME/.cache \\ + PATH=\$CLI_PATH:\$PATH "\$@" } EOF ``` From cd088d34997058269ff55ec0e754d7cdbf46b33d Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 21:17:39 +0000 Subject: [PATCH 25/32] docs: pin the v6 line's bridge tutorials to a reviewed hardhat example commit The v6 line's token bridge and Aave bridge tutorials clone a third-party Hardhat starter and then run yarn, which runs that repository's postinstall. Check out the reviewed commit right after entering the clone, matching the change to main's pages, so later upstream changes never run on a reader's machine. --- .../docs/tutorials/js_tutorials/aave_bridge.md | 3 +++ .../docs/tutorials/js_tutorials/token_bridge.md | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md index 13a0aabd17c..67a5a912ac7 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md @@ -115,8 +115,11 @@ This template is a community-maintained starter. If the repository is unavailabl ```bash git clone https://github.com/critesjosh/hardhat-aztec-example cd hardhat-aztec-example +git checkout 3be1b31f8054cd4a81817b6940171849950b8dfb ``` +The `git checkout` pins the starter to a commit we reviewed, so the tutorial always uses reviewed code. + Before adding dependencies, make the starter install with yarn 4: create the `.yarnrc.yml`, swap `@aztec/l1-contracts` for `@aztec-foundation/l1-artifacts`, point the `postinstall` remappings at the new package, and add the root `remappings.txt`. These are the first three steps of the [Token Bridge tutorial's project setup](./token_bridge.md#project-setup); without them `yarn add` fails on the starter's git dependencies and `npx hardhat compile` cannot resolve the `@aztec/*` imports. When complete, your project will have this structure: diff --git a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md index 6785a52fa01..7e86bd7e5f4 100644 --- a/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md +++ b/docs/lines/v6/docs-developers/docs/tutorials/js_tutorials/token_bridge.md @@ -28,13 +28,15 @@ Let's start simple. Since this is an Ethereum project, it's easier to just start ```bash git clone https://github.com/critesjosh/hardhat-aztec-example +cd hardhat-aztec-example +git checkout 3be1b31f8054cd4a81817b6940171849950b8dfb ``` -You're cloning a repo here to make it easier for Aztec's `l1-contracts` to be mapped correctly. You should now have a `hardhat-aztec-example` folder with Hardhat's default starter, with a few changes in `package.json`. +You're cloning a repo here to make it easier for Aztec's `l1-contracts` to be mapped correctly. You should now have a `hardhat-aztec-example` folder with Hardhat's default starter, with a few changes in `package.json`. The `git checkout` pins the starter to a commit we reviewed, so the tutorial always uses reviewed code. We want to add a few more dependencies now before we start. The starter needs four changes before it installs with yarn 4. -First, from the `hardhat-aztec-example` directory (`cd hardhat-aztec-example`), tell yarn to install into `node_modules` (the starter's scripts and Hardhat expect a real `node_modules` tree) and allow the starter's one git dependency, `forge-std`. Create `.yarnrc.yml`: +First, from the `hardhat-aztec-example` directory, tell yarn to install into `node_modules` (the starter's scripts and Hardhat expect a real `node_modules` tree) and allow the starter's one git dependency, `forge-std`. Create `.yarnrc.yml`: ```yaml nodeLinker: node-modules From 6002d044e03cea5ea85b54e4331cb708c846a343 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 21:31:22 +0000 Subject: [PATCH 26/32] fix(docs): check the pinned commit, not the tag, against the line's branch promote_line.sh compared the tag with the line's branch before pinning, in the repository the line named before. A tag moved between that compare and the pin could promote a commit never checked. Compare the commit the pin recorded, in the repository the pin enforces, and restore the line when that check refuses. --- .claude/skills/release-docs/SKILL.md | 9 ++++---- docs/scripts/__tests__/promote_line.test.mts | 24 ++++++++++++-------- docs/scripts/promote_line.sh | 21 +++++++++++------ 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.claude/skills/release-docs/SKILL.md b/.claude/skills/release-docs/SKILL.md index 091da3ccec3..ea5a23756c3 100644 --- a/.claude/skills/release-docs/SKILL.md +++ b/.claude/skills/release-docs/SKILL.md @@ -280,10 +280,11 @@ CUT=$CUT scripts/promote_line.sh "$LINE" "$LABS_TAG" && ``` `promote_line.sh` refuses, before writing anything, unless it runs in `$CUT`, `docs/` is -clean, and the tag has the line's major and is on aztec-node's `$LINE` branch. Then it pins -the line to the tag (`aztec_node.mts --line $LINE pin`, which also installs the line's -l1-artifacts), replaces `docs-developers/`, `docs-operate/` and both sidebars with the -line's, and copies the line's pin over `docs/aztec-node.json`. That promotion commit is +clean, and the tag has the line's major. Then it pins the line to the tag +(`aztec_node.mts --line $LINE pin`, which also installs the line's l1-artifacts), and +refuses, restoring the line, unless the pinned commit is on aztec-node's `$LINE` branch. +Only then does it replace `docs-developers/`, `docs-operate/` and both sidebars with the +line's, and copy the line's pin over `docs/aztec-node.json`. That promotion commit is release-only (Step 17). `fetch` then checks main's checkout out at that pin and reports the same drift as `pin` below. The promotion commit already carries the pin, so the pin commit below has something to commit only when l1-artifacts drifted; otherwise `PINNED` is the diff --git a/docs/scripts/__tests__/promote_line.test.mts b/docs/scripts/__tests__/promote_line.test.mts index ec6d95f0174..38b51a8515b 100644 --- a/docs/scripts/__tests__/promote_line.test.mts +++ b/docs/scripts/__tests__/promote_line.test.mts @@ -14,12 +14,15 @@ function write(file: string, text: string) { } const MAIN_PATHS = ["docs-developers", "docs-operate", "sidebars-developer.js", "sidebars-operate.js", "aztec-node.json"]; -const pinFile = (ref: string, commit: string) => - JSON.stringify({ repository: "https://github.com/aztec-labs-eng/aztec-node", ref, commit }, null, 2) + "\n"; +const PINNED = "b".repeat(40); +const pinFile = (ref: string, commit: string, repository = "https://github.com/aztec-labs-eng/aztec-node") => + JSON.stringify({ repository, ref, commit }, null, 2) + "\n"; -// A cut worktree: main's paths and a v6 line holding different pages. The pin tool is a stand-in -// (the real one is tested in aztec_node.test.mts) that pins the line like `pin` does, or writes the -// pin and fails like a failed check; `gh` answers the compare for v6...v6.0.1 only. +// A cut worktree: main's paths and a v6 line holding different pages, whose pin names a fork. The pin +// tool is a stand-in (the real one is tested in aztec_node.test.mts) that pins the line to PINNED in +// the canonical repository like `pin` does, or writes the pin and fails like a failed check. `gh` +// answers only the compare of the v6 branch with PINNED in the canonical repository, so comparing +// the tag or the fork fails. let root: string, docs: string, bin: string; before(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), "promote-line-")); @@ -31,13 +34,13 @@ before(() => { `import fs from "node:fs"; const [flag, line, cmd, tag] = process.argv.slice(2); if (flag !== "--line" || cmd !== "pin") process.exit(2); -fs.writeFileSync(new URL(\`../lines/\${line}/aztec-node.json\`, import.meta.url), ${JSON.stringify(pinFile("TAG", "b".repeat(40)))}.replace("TAG", tag)); +fs.writeFileSync(new URL(\`../lines/\${line}/aztec-node.json\`, import.meta.url), ${JSON.stringify(pinFile("TAG", PINNED))}.replace("TAG", tag)); process.exit(process.env.PIN_FAIL ? 1 : 0); `, ); write( path.join(bin, "gh"), - `#!/usr/bin/env bash\n[ "$2" = repos/aztec-labs-eng/aztec-node/compare/v6...v6.0.1 ] && echo "\${COMPARE_STATUS:-behind}" || exit 1\n`, + `#!/usr/bin/env bash\n[ "$2" = repos/aztec-labs-eng/aztec-node/compare/v6...${PINNED} ] && echo "\${COMPARE_STATUS:-behind}" || exit 1\n`, ); fs.chmodSync(path.join(bin, "gh"), 0o755); for (const [dir, who] of [[docs, "main"], [path.join(docs, "lines/v6"), "line"]]) { @@ -48,7 +51,7 @@ process.exit(process.env.PIN_FAIL ? 1 : 0); } write(path.join(docs, "docs-developers/docs/main-only.md"), "v7 only\n"); write(path.join(docs, "aztec-node.json"), pinFile("v7.0.0-nightly.1", "a".repeat(40))); - write(path.join(docs, "lines/v6/aztec-node.json"), pinFile("v6.1.0-nightly.1", "c".repeat(40))); + write(path.join(docs, "lines/v6/aztec-node.json"), pinFile("v6.1.0-nightly.1", "c".repeat(40), "https://github.com/a-fork/aztec-node")); git(root, "init", "-q"); git(root, "add", "-A"); git(root, "commit", "-q", "-m", "cut"); @@ -85,14 +88,14 @@ test("a line cut serves the line's pages and its tag's pin from the main paths", assert.equal(JSON.parse(fs.readFileSync(path.join(docs, "aztec-node.json"), "utf8")).ref, "v6.0.1"); }); -test("a wrong worktree, a dirty docs/, another major, a tag off the branch and a failed pin leave the main paths untouched", () => { +test("a wrong worktree, a dirty docs/, another major, a pinned commit off the branch and a failed pin leave the main paths and the line untouched", () => { const before = snapshot(docs); const cases: [string, () => ReturnType, RegExp][] = [ ["no cut", () => promote("v6.0.1", { CUT: undefined }), /not the cut worktree/], ["another worktree", () => promote("v6.0.1", { CUT: os.tmpdir() }), /not the cut worktree/], ["dirty docs/", () => (write(path.join(docs, "docs-developers/docs/wip.md"), "wip\n"), promote("v6.0.1")), /uncommitted changes/], ["another major", () => promote("v7.0.0"), /v7\.0\.0 is not a v6 release/], - ["off the branch", () => promote("v6.0.1", { COMPARE_STATUS: "diverged" }), /not on aztec-labs-eng\/aztec-node's v6 branch .*diverged/], + ["off the branch", () => promote("v6.0.1", { COMPARE_STATUS: "diverged" }), new RegExp(`${PINNED}\\) is not on aztec-labs-eng/aztec-node's v6 branch .*diverged`)], ["failed pin", () => promote("v6.0.1", { PIN_FAIL: "1" }), /pinning line v6 to v6\.0\.1 failed/], ]; for (const [name, run, message] of cases) { @@ -103,5 +106,6 @@ test("a wrong worktree, a dirty docs/, another major, a tag off the branch and a assert.match(r.stderr, message, name); fs.rmSync(path.join(docs, "docs-developers/docs/wip.md"), { force: true }); assert.deepEqual(snapshot(docs), before, name); + assert.equal(git(root, "status", "--porcelain"), "", name); } }); diff --git a/docs/scripts/promote_line.sh b/docs/scripts/promote_line.sh index c8496874d54..db37b461ada 100755 --- a/docs/scripts/promote_line.sh +++ b/docs/scripts/promote_line.sh @@ -8,14 +8,19 @@ # CUT= docs/scripts/promote_line.sh # # It refuses, before writing anything, unless it runs in the worktree $CUT names (the cut's env.sh), -# docs/ is clean, the tag has the line's major, and the tag is on aztec-node's branch. A failed -# pin leaves the main paths untouched. +# docs/ is clean, and the tag has the line's major. After pinning, it refuses unless the pinned commit +# is on aztec-node's branch. A refusal after the pin restores the line and leaves the main +# paths untouched. set -euo pipefail fail() { echo "promote_line: $*" >&2 exit 1 } +unpin() { + git -C "$root" checkout -q -- "docs/lines/$line" + fail "$@" +} [ $# -eq 2 ] || { echo "usage: CUT= $0 " >&2; exit 2; } line=$1 tag=$2 docs=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P) @@ -29,15 +34,17 @@ line_root=$docs/lines/$line fail "this is $root, not the cut worktree: run it from the cut, with the cut's env.sh sourced (\$CUT)" [ -z "$(git -C "$root" status --porcelain -- docs)" ] || fail "docs/ has uncommitted changes: commit or discard them first" [[ "$tag" =~ ^v${major}\. ]] || fail "$tag is not a v$major release, and line $line takes only v$major tags" -repo=$(node -p 'new URL(require(process.argv[1]).repository).pathname.slice(1)' "$line_root/aztec-node.json") -status=$(gh api "repos/$repo/compare/$line...$tag" --jq .status) || fail "cannot compare $tag with $repo's $line branch" + +node "$docs/scripts/aztec_node.mts" --line "$line" pin "$tag" || unpin "pinning line $line to $tag failed; the main paths are untouched" +# The commit the pin recorded, not the tag, which can move after the pin resolves it; and the +# repository the pin enforces, not the one the line named before. +read -r repo commit < <(node -p 'const p = require(process.argv[1]); new URL(p.repository).pathname.slice(1) + " " + p.commit' "$line_root/aztec-node.json") +status=$(gh api "repos/$repo/compare/$line...$commit" --jq .status) || unpin "cannot compare $tag ($commit) with $repo's $line branch" case "$status" in behind | identical) ;; - *) fail "$tag is not on $repo's $line branch (compare $line...$tag: $status)" ;; + *) unpin "$tag ($commit) is not on $repo's $line branch (compare $line...$commit: $status)" ;; esac -node "$docs/scripts/aztec_node.mts" --line "$line" pin "$tag" || fail "pinning line $line to $tag failed; the main paths are untouched" - for tree in docs-developers docs-operate; do rm -rf "${docs:?}/$tree" cp -R "$line_root/$tree" "$docs/$tree" From ac2bc974a77233553269c239806c7b93c8dfc0ed Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 21:40:29 +0000 Subject: [PATCH 27/32] fix(docs): link the API folder a release serves once it maps its version A release cut freezes #api_ref_version into its snapshot at Step 11, before 11b maps the new version. Resolving against the developer config as it was named mainnet's folder for a testnet release that splits from mainnet, so the snapshot linked the older version's API. Resolve against the config with the release type reading the version being built. A network-only cut leaves the developer versions alone, so its builds must keep linking the folders served now: release-network-docs sets KEEP_DEVELOPER_VERSIONS=1, and without it agent readiness fails the build. --- .claude/skills/release-network-docs/SKILL.md | 8 ++++-- docs/CLAUDE.md | 1 + .../__tests__/include_version.test.js | 28 +++++++++++++++++-- docs/src/preprocess/include_version.js | 11 ++++++-- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/.claude/skills/release-network-docs/SKILL.md b/.claude/skills/release-network-docs/SKILL.md index 5374ddb14a9..38cdb2296f0 100644 --- a/.claude/skills/release-network-docs/SKILL.md +++ b/.claude/skills/release-network-docs/SKILL.md @@ -187,7 +187,8 @@ CLI_PATH=$CUT/docs/.aztec-node/cli/node_modules/.bin:$CUT/docs/.aztec-node/bin:$ docs_env() { local pin; pin=\$(node "$CUT/docs/scripts/aztec_node.mts" env) || return 1 eval "\$pin" - COMMIT_TAG=$LABS_TAG RELEASE_TYPE= = DOCS_COMMIT=$DOCS_COMMIT "\$@" + COMMIT_TAG=$LABS_TAG RELEASE_TYPE= = DOCS_COMMIT=$DOCS_COMMIT \\ + KEEP_DEVELOPER_VERSIONS=1 "\$@" } aztec_cli() { [ -x $CUT/docs/.aztec-node/cli/node_modules/.bin/aztec ] || { echo "no release CLI: run Step 5b" >&2; return 1; } @@ -200,7 +201,10 @@ EOF `` is `MAINNET_TAG` or `TESTNET_TAG`, and its value has no `v`; `` sets the other type's tag to the version its network runs, or is empty. `COMMIT_TAG` -keeps its `v` (the preprocessor emits it as a git ref). `docs_env` checks the cut's pin +keeps its `v` (the preprocessor emits it as a git ref). `KEEP_DEVELOPER_VERSIONS=1` is +required: this cut leaves the developer docs' versions alone, so their API reference links +must name the folders the build serves now. Without it the dev pages can link a folder this +build does not serve, and agent readiness fails the build. `docs_env` checks the cut's pin and checkout and exports the pin's versions before running a docs command; it fails when they disagree. `AZTEC_NODE_DIR` keeps the cut's checkout outside the worktree, as `/release-docs` Step 2b explains. `LINE` names the line a line cut promotes (empty diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 71d1758e904..b3b3c7be6ff 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -97,6 +97,7 @@ The preprocessing system uses these environment variables: | `TESTNET_TAG` | Version for testnet builds | from `developer_version_config.json` | | `MAINNET_TAG` | Version for mainnet builds | from `developer_version_config.json` | | `COMMIT_TAG` | Legacy variable, used as fallback for `NIGHTLY_TAG` | `ref` in `aztec-node.json` | +| `KEEP_DEVELOPER_VERSIONS` | `1` when the release leaves the developer docs' versions alone (a network-only cut, `/release-network-docs`): `#api_ref_version` then names the folder `developer_version_config.json` serves now, not the one it serves once `RELEASE_TYPE` reads the version being built | unset | ### Preprocessing Macros diff --git a/docs/src/preprocess/__tests__/include_version.test.js b/docs/src/preprocess/__tests__/include_version.test.js index a40c9e33438..071c637750f 100644 --- a/docs/src/preprocess/__tests__/include_version.test.js +++ b/docs/src/preprocess/__tests__/include_version.test.js @@ -200,13 +200,35 @@ describe("preprocessIncludeVersion", () => { ["mainnet", served("mainnet")], ]; + const TAGS = ["NIGHTLY_TAG", "DEVNET_TAG", "TESTNET_TAG", "MAINNET_TAG", "KEEP_DEVELOPER_VERSIONS"]; + const apiRefVersion = async (vars) => { + for (const v of TAGS) delete process.env[v]; + setEnv(vars); + return (await preprocessIncludeVersion("#api_ref_version", "test.md")).content; + }; + testCases.forEach(([releaseType, expected]) => { it(`resolves to ${expected} for ${releaseType}`, async () => { - setEnv({ RELEASE_TYPE: releaseType }); - const { content } = await preprocessIncludeVersion("#api_ref_version", "test.md"); - assert.strictEqual(content, expected); + assert.strictEqual(await apiRefVersion({ RELEASE_TYPE: releaseType }), expected); }); }); + + // A release cut freezes these links into its snapshot before it maps the new version, so they + // name the folder the build serves once the release type reads the version being built. + it("names testnet's own folder for a testnet release on a version mainnet does not run", async () => { + assert.strictEqual(await apiRefVersion({ RELEASE_TYPE: "testnet", TESTNET_TAG: "999.0.0" }), "testnet"); + }); + + it("names mainnet's folder for a testnet release on mainnet's version", async () => { + assert.strictEqual(await apiRefVersion({ RELEASE_TYPE: "testnet", TESTNET_TAG: config.mainnet }), "mainnet"); + }); + + it("keeps the folder the developer versions serve now when the release does not change them", async () => { + assert.strictEqual( + await apiRefVersion({ RELEASE_TYPE: "testnet", TESTNET_TAG: "999.0.0", KEEP_DEVELOPER_VERSIONS: "1" }), + served("testnet"), + ); + }); }); describe("on a line page", () => { diff --git a/docs/src/preprocess/include_version.js b/docs/src/preprocess/include_version.js index 769c306b322..a3f06f600fe 100644 --- a/docs/src/preprocess/include_version.js +++ b/docs/src/preprocess/include_version.js @@ -128,9 +128,14 @@ async function preprocessIncludeVersion(markdownContent, filePath = "unknown", r ); const releaseNetwork = getReleaseNetwork(releaseType); - // A testnet on mainnet's version has no folder of its own: the build serves that version's - // reference under mainnet/ only. - const apiRefVersion = networkApiFolder(developerVersionConfig, releaseType) + // The folder the build serves once this release type reads the version being built: a release + // cut freezes these links into its snapshot before it maps that version. A testnet on mainnet's + // version has no folder of its own. A release that leaves the developer docs alone (a network-only + // cut) keeps their versions, so its build links folders it serves. + const developerVersions = process.env.KEEP_DEVELOPER_VERSIONS === "1" + ? developerVersionConfig + : { ...developerVersionConfig, [releaseType]: `v${stripV(releaseVersion)}` }; + const apiRefVersion = networkApiFolder(developerVersions, releaseType) ?? (releaseType === "testnet" || releaseType === "mainnet" ? releaseType : "next"); // Step 1: Process conditional blocks FIRST (before version substitution) From 6942373a2e6ada634c2033001a065f9fea0f50ce Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 21:45:02 +0000 Subject: [PATCH 28/32] fix(docs): restore only the line's pin when promote_line refuses after pinning --- .claude/skills/release-docs/SKILL.md | 2 +- docs/scripts/promote_line.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/release-docs/SKILL.md b/.claude/skills/release-docs/SKILL.md index ea5a23756c3..ad524afea48 100644 --- a/.claude/skills/release-docs/SKILL.md +++ b/.claude/skills/release-docs/SKILL.md @@ -282,7 +282,7 @@ CUT=$CUT scripts/promote_line.sh "$LINE" "$LABS_TAG" && `promote_line.sh` refuses, before writing anything, unless it runs in `$CUT`, `docs/` is clean, and the tag has the line's major. Then it pins the line to the tag (`aztec_node.mts --line $LINE pin`, which also installs the line's l1-artifacts), and -refuses, restoring the line, unless the pinned commit is on aztec-node's `$LINE` branch. +refuses, restoring the line's pin, unless the pinned commit is on aztec-node's `$LINE` branch. Only then does it replace `docs-developers/`, `docs-operate/` and both sidebars with the line's, and copy the line's pin over `docs/aztec-node.json`. That promotion commit is release-only (Step 17). `fetch` then checks main's checkout out at that pin and reports the diff --git a/docs/scripts/promote_line.sh b/docs/scripts/promote_line.sh index db37b461ada..151f7cce63a 100755 --- a/docs/scripts/promote_line.sh +++ b/docs/scripts/promote_line.sh @@ -9,7 +9,7 @@ # # It refuses, before writing anything, unless it runs in the worktree $CUT names (the cut's env.sh), # docs/ is clean, and the tag has the line's major. After pinning, it refuses unless the pinned commit -# is on aztec-node's branch. A refusal after the pin restores the line and leaves the main +# is on aztec-node's branch. A refusal after the pin restores the line's pin and leaves the main # paths untouched. set -euo pipefail @@ -18,7 +18,7 @@ fail() { exit 1 } unpin() { - git -C "$root" checkout -q -- "docs/lines/$line" + git -C "$root" checkout -q -- "docs/lines/$line/aztec-node.json" fail "$@" } [ $# -eq 2 ] || { echo "usage: CUT= $0 " >&2; exit 2; } From 8e9d45dde2c9fe8a2828689d341167be441a7f8c Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 21:46:03 +0000 Subject: [PATCH 29/32] docs(skills): a shared release snapshot renders as mainnet until testnet is supported --- .claude/skills/release-docs/SKILL.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.claude/skills/release-docs/SKILL.md b/.claude/skills/release-docs/SKILL.md index ad524afea48..73c1561a198 100644 --- a/.claude/skills/release-docs/SKILL.md +++ b/.claude/skills/release-docs/SKILL.md @@ -173,6 +173,11 @@ grep -rlE '#if\((devnet|testnet|mainnet|nightly)\)' docs/docs-developers/ docs/d Then set each other type's `` in `docs_env` (2b) to the version its network reports in Step 1, and run Step 11b's `update_docs_versions.sh` once per type. +A version both networks read serves its API reference from `mainnet/`, but Step 6 and +`#api_ref_version` use the rendered type's folder, so a shared snapshot rendered as testnet +fails Step 13's agent readiness check. Until that is supported, offer the owner a mainnet +rendering only. + ### Step 2: Prepare the Cut Worktree #### 2a. The Labs tag must exist From 5d87f280fc830a679520dcfe00f1734af389ac75 Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 21:59:05 +0000 Subject: [PATCH 30/32] fix(docs): key the docs build cache on its context and release variables The key named the release type, COMMIT_TAG and DOCS_COMMIT but not CONTEXT, the per-type tags or KEEP_DEVELOPER_VERSIONS, so a production build could restore a preview build's output (with the dev docs and the lines) from the cache. --- docs/bootstrap.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/bootstrap.sh b/docs/bootstrap.sh index 84fba747683..409774a9642 100755 --- a/docs/bootstrap.sh +++ b/docs/bootstrap.sh @@ -12,11 +12,13 @@ hash=$( $(find docs docs-developers docs-operate docs-participate developer_versioned_docs network_versioned_docs $([ -d lines ] && echo lines) -type f -name "*.md*" -exec grep '^#include_code' {} \; | \ awk '{ gsub("^/", "", $3); if ($3 !~ /^labs\//) print "^" $3 }' | sort -u) ) -# A release build renders its own versions and links this repository's files at DOCS_COMMIT, so it -# never shares a cache entry with a dev build. +# Production serves neither the dev docs nor the lines, and a release build renders its own versions +# and links this repository's files at DOCS_COMMIT: every variable that changes the output keys it. # cache_content_hash hashes blob ids without their names, so the docs tree id keys renamed pages. hash=$(hash_str "$hash" "$(git -C "$repo_root" rev-parse HEAD:docs)" \ - "${COMMIT_TAG:-}" "${RELEASE_TYPE:-}" "${DOCS_COMMIT:-}") + "CONTEXT=${CONTEXT:-}" "COMMIT_TAG=${COMMIT_TAG:-}" "RELEASE_TYPE=${RELEASE_TYPE:-}" "DOCS_COMMIT=${DOCS_COMMIT:-}" \ + "NIGHTLY_TAG=${NIGHTLY_TAG:-}" "DEVNET_TAG=${DEVNET_TAG:-}" "TESTNET_TAG=${TESTNET_TAG:-}" \ + "MAINNET_TAG=${MAINNET_TAG:-}" "KEEP_DEVELOPER_VERSIONS=${KEEP_DEVELOPER_VERSIONS:-}") # Every function that reads aztec-node code fetches and checks the pinned checkout first, on a cache # hit too, since later steps read it (scripts/aztec_node.mts; a no-op without network when current). From c7adc6a07c8006e0c721c72fc09ff190f7a8e11a Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 21:59:05 +0000 Subject: [PATCH 31/32] docs: spellcheck a line's pages through their preprocessed copies cspell reads a line's pages only under processed-docs/, so a bare yarn spellcheck after editing one checks a stale copy or none. --- .claude/skills/update-doc-references/SKILL.md | 3 ++- docs/CLAUDE.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.claude/skills/update-doc-references/SKILL.md b/.claude/skills/update-doc-references/SKILL.md index 56a18622e0d..a08351cfca1 100644 --- a/.claude/skills/update-doc-references/SKILL.md +++ b/.claude/skills/update-doc-references/SKILL.md @@ -154,7 +154,8 @@ snapshot, a checkout or `labs/`: Then run `docs/scripts/check_doc_references.sh` and, for the v6 line, `docs/scripts/check_doc_references.sh docs/lines/v6`, which check `labs/` references in each line's checkout (run `node docs/scripts/aztec_node.mts fetch` first; each must print `All -referenced paths exist.`), and, from `docs/`, `yarn spellcheck`. An aztec-node PR newer +referenced paths exist.`), and, from `docs/`, `yarn preprocess && yarn spellcheck` (a v6 +page is spellchecked through its preprocessed copy). An aztec-node PR newer than the pin is not in the checkout yet, so an edit that quotes its code (a new `#include_code` marker, a moved file in `references`) fails until the pin includes it: make such edits together with the pin move, or report them, rather than editing them to diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index b3b3c7be6ff..415ed57ac69 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -36,7 +36,7 @@ This project uses Yarn 4.13.0 as specified in the `packageManager` field of pack - `yarn build` - Build production site with full validation (includes clean, preprocess, spellcheck, and move steps) - It ends with the search index check: renaming or moving a page a search probe expects (`PROBES` in `scripts/check_search_index.mts`) fails the build until the probe lists the new path - `yarn serve` - Serve the built static site -- `yarn spellcheck` - Run spell checking with cspell on markdown files +- `yarn spellcheck` - Run spell checking with cspell on markdown files. A line's pages are checked only through their preprocessed copies, so run `yarn preprocess` first after editing one - `yarn clean` - Clean build artifacts and processed docs ### API Documentation Generation From 46ab30f10596d1dc0b27ba8e91c18fd471ac710c Mon Sep 17 00:00:00 2001 From: alejoamiras Date: Thu, 5 Nov 2026 21:59:05 +0000 Subject: [PATCH 32/32] docs: tighten promote_line.sh's header --- docs/scripts/promote_line.sh | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/scripts/promote_line.sh b/docs/scripts/promote_line.sh index 151f7cce63a..1b63c4618c4 100755 --- a/docs/scripts/promote_line.sh +++ b/docs/scripts/promote_line.sh @@ -1,16 +1,10 @@ #!/usr/bin/env bash -# Promotes a dev line (docs/lines//) into the main paths of a release cut, at the cut's Step 2 -# (release-docs, release-network-docs): it pins the line to the release tag, then replaces -# docs-developers/, docs-operate/ and both sidebars with the line's, and docs/aztec-node.json with the -# line's pin. Every later step reads the line's pages through the main paths. Their snippets keep -# quoting docs/lines//, which include_code allows while both pins name one commit. +# Promotes a dev line (docs/lines//) into a release cut's main paths, pinned to the release tag. +# Only a cut's worktree ($CUT, from its env.sh) may run it: it rewrites main's pages. The promoted +# pages keep quoting docs/lines// snippets, which include_code allows while both pins name one +# commit. # # CUT= docs/scripts/promote_line.sh -# -# It refuses, before writing anything, unless it runs in the worktree $CUT names (the cut's env.sh), -# docs/ is clean, and the tag has the line's major. After pinning, it refuses unless the pinned commit -# is on aztec-node's branch. A refusal after the pin restores the line's pin and leaves the main -# paths untouched. set -euo pipefail fail() {