diff --git a/.claude/agents/network-logs.md b/.claude/agents/network-logs.md index b0dc08117ba1..b4bad7eab61a 100644 --- a/.claude/agents/network-logs.md +++ b/.claude/agents/network-logs.md @@ -62,10 +62,31 @@ You can read this output directly — no parsing needed. --format='table[no-heading](timestamp, resource.labels.pod_name, jsonPayload.message.slice(0,150))' ``` +## Cluster Mapping + +Aztec runs two GKE clusters: + +| Cluster | Aztec namespaces | +|---------|-----------------| +| `aztec-gke-private` | `mainnet` (ignition — active), `next-net`, `staging-ignition`, `staging-public`, and various test/scenario namespaces | +| `aztec-gke-public` | `mainnet` (public — currently in standby), `testnet`, and other public-facing infrastructure | + +**Important: `mainnet` exists in BOTH clusters.** +- The **private** cluster's `mainnet` runs the **ignition** network (active, fisherman mode). +- The **public** cluster's `mainnet` is the next rollup upgrade (currently in standby, waiting for L1 contract alignment). It also runs in fisherman mode. + +When querying `mainnet`, you MUST include a `resource.labels.cluster_name` filter to disambiguate: +- If the user says "mainnet" without qualification, query the **private** cluster (ignition) by default — it's the active one. +- If the user says "mainnet public", "public cluster mainnet", or "mainnet on public", query the **public** cluster. +- If uncertain, query **both** clusters in parallel and report results separately. + +For all other namespaces, the cluster filter is optional but recommended for clarity. + ## GCP Log Structure Aztec network logs use: - `resource.type="k8s_container"` +- `resource.labels.cluster_name` — the GKE cluster (`aztec-gke-private` or `aztec-gke-public`) - `resource.labels.namespace_name` — the deployment namespace - `resource.labels.pod_name` — the specific pod - `resource.labels.container_name` — usually `aztec` @@ -92,7 +113,7 @@ Pods follow the pattern `{namespace}-{component}-{index}`: ## Deployment-Specific Notes - **next-net** redeploys every morning at ~4am UTC. Always use timestamp range filters (not `--freshness`) when querying next-net for a specific date, and expect logs to only cover a single instance of the network. Because next-net resets daily, its block height should start near 0 after ~4am UTC. If you are running a morning healthcheck and the block height is unexpectedly large (e.g., hundreds or thousands), flag this as an error — it likely means the nightly redeploy failed and the network is running a stale instance. -- **mainnet** does not run sequencer validators. Instead, it runs infrastructure in **fisherman mode**: nodes simulate building a block for every slot but never actually submit the L1 transaction. This means you will see "built block" or similar messages but no "Published checkpoint" or L1 submission logs. Errors with hash `0xf3e591ac` are a known artifact of fisherman mode and are safe to ignore. +- **mainnet** (both private/ignition and public) does not run sequencer validators. Both deployments run in **fisherman mode**: nodes simulate building a block for every slot but never actually submit the L1 transaction. This means you will see "built block" or similar messages but no "Published checkpoint" or L1 submission logs. Errors with hash `0xf3e591ac` are a known artifact of fisherman mode and are safe to ignore. See the Cluster Mapping section above for how to disambiguate between the two mainnet deployments. ## Filter Building diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 9ddcfd61b65a..000000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(gh pr:*)", - "Bash(curl -s -X POST https://ethereum-sepolia-rpc.publicnode.com -H \"Content-Type: application/json\" -d '{\"\"\"\"jsonrpc\"\"\"\":\"\"\"\"2.0\"\"\"\",\"\"\"\"method\"\"\"\":\"\"\"\"eth_getBlockByNumber\"\"\"\",\"\"\"\"params\"\"\"\":[\"\"\"\"finalized\"\"\"\",false],\"\"\"\"id\"\"\"\":1}')", - "Bash(curl -s -X POST https://ethereum-rpc.publicnode.com -H \"Content-Type: application/json\" -d '{\"\"\"\"jsonrpc\"\"\"\":\"\"\"\"2.0\"\"\"\",\"\"\"\"method\"\"\"\":\"\"\"\"eth_getBlockByNumber\"\"\"\",\"\"\"\"params\"\"\"\":[\"\"\"\"finalized\"\"\"\",false],\"\"\"\"id\"\"\"\":1}')", - "Bash(curl -s -X POST https://ethereum-sepolia-rpc.publicnode.com -H \"Content-Type: application/json\" -d '{\"\"\"\"jsonrpc\"\"\"\":\"\"\"\"2.0\"\"\"\",\"\"\"\"method\"\"\"\":\"\"\"\"eth_getBlockByNumber\"\"\"\",\"\"\"\"params\"\"\"\":[\"\"\"\"latest\"\"\"\",false],\"\"\"\"id\"\"\"\":1}')", - "Bash(curl -s -X POST https://ethereum-rpc.publicnode.com -H \"Content-Type: application/json\" -d '{\"\"\"\"jsonrpc\"\"\"\":\"\"\"\"2.0\"\"\"\",\"\"\"\"method\"\"\"\":\"\"\"\"eth_getBlockByNumber\"\"\"\",\"\"\"\"params\"\"\"\":[\"\"\"\"latest\"\"\"\",false],\"\"\"\"id\"\"\"\":1}')" - ] - } -} diff --git a/.github/workflows/ci3-external.yml b/.github/workflows/ci3-external.yml index 832d16d52181..2e34d5db7563 100644 --- a/.github/workflows/ci3-external.yml +++ b/.github/workflows/ci3-external.yml @@ -22,6 +22,9 @@ jobs: runs-on: ubuntu-latest # exclusive with ci3.yml, only run on forks. if: github.event.pull_request.head.repo.fork + permissions: + contents: read + pull-requests: write steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 diff --git a/.github/workflows/deploy-staging-public.yml b/.github/workflows/deploy-staging-public.yml index 5c521e6c0e7e..e26a2538b650 100644 --- a/.github/workflows/deploy-staging-public.yml +++ b/.github/workflows/deploy-staging-public.yml @@ -26,28 +26,27 @@ jobs: token: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }} fetch-depth: 0 - - name: Read version from manifest - id: manifest - run: | - VERSION=$(jq -r '."."' .release-please-manifest.json) - echo "version=$VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Poll for tag at HEAD + - name: Poll for semver tag at HEAD id: poll-tag run: | - # wait for tag to be pushed (either RC or stable release) - VERSION="${{ steps.manifest.outputs.version }}" HEAD_SHA=$(git rev-parse HEAD) MAX_ATTEMPTS=60 - echo "Looking for tag matching v${VERSION} or v${VERSION}-rc.* at HEAD ($HEAD_SHA)" + echo "Looking for any semver tag at HEAD ($HEAD_SHA)" for i in $(seq 1 $MAX_ATTEMPTS); do git fetch --tags --force - TAG=$(git tag --points-at HEAD | grep -E "^v${VERSION}(-rc\.[0-9]+)?$" | sort -V | tail -n 1 || true) + # Collect all valid semver tags pointing at HEAD + SEMVER_TAGS=() + for t in $(git tag --points-at HEAD); do + if ci3/semver check "$t"; then + SEMVER_TAGS+=("$t") + fi + done - if [ -n "$TAG" ]; then + # If we found valid semver tags, pick the highest + if [ ${#SEMVER_TAGS[@]} -gt 0 ]; then + TAG=$(ci3/semver sort "${SEMVER_TAGS[@]}" | tail -n 1) echo "Found tag: $TAG" SEMVER="${TAG#v}" echo "tag=$TAG" >> $GITHUB_OUTPUT @@ -55,11 +54,11 @@ jobs: exit 0 fi - echo "Attempt $i/$MAX_ATTEMPTS: No matching tag yet, waiting 10s..." + echo "Attempt $i/$MAX_ATTEMPTS: No semver tag yet, waiting 10s..." sleep 10 done - echo "Error: No tag found for v${VERSION} at HEAD after 10 minutes" + echo "Error: No semver tag found at HEAD after 10 minutes" exit 1 wait-for-ci3: diff --git a/.gitignore b/.gitignore index dbcd78ff2261..8557c7d148e7 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ docs/docs/protocol-specs/public-vm/gen/ __pycache__ *.local.md +.claude/settings.local.json diff --git a/.test_patterns.yml b/.test_patterns.yml index 6eb021c46794..a63d790255f9 100644 --- a/.test_patterns.yml +++ b/.test_patterns.yml @@ -339,6 +339,12 @@ tests: owners: - *palla + # http://ci.aztec-labs.com/153f5dcbb0f3799c + - regex: "src/e2e_offchain_payment.test.ts" + error_regex: "✕ reprocesses an offchain-delivered payment after an L1 reorg" + owners: + - *martin + - regex: "yarn-project/scripts/run_test.sh bb-prover/src/avm_proving_tests/avm_" error_regex: "timeout: sending signal" owners: diff --git a/Makefile b/Makefile index 22958e50f811..4f2c17cf8292 100644 --- a/Makefile +++ b/Makefile @@ -235,7 +235,7 @@ bb-sol: bb-cpp-native bb-crs # Barretenberg Tests #============================================================================== -bb-cpp-native-tests: bb-cpp-native +bb-cpp-native-tests: bb-cpp-native bb-crs $(call test,$@,barretenberg/cpp,native) bb-cpp-wasm-threads-tests: bb-cpp-wasm-threads diff --git a/aztec-up/bin/0.0.1/aztec-install b/aztec-up/bin/0.0.1/aztec-install index 7013482ae380..0b0d5bc9092c 100755 --- a/aztec-up/bin/0.0.1/aztec-install +++ b/aztec-up/bin/0.0.1/aztec-install @@ -3,6 +3,9 @@ # This script installs aztec-up and then delegates to it for version installation. # Usage: bash -i <(curl -s https://install.aztec.network) # or: VERSION=0.85.0 bash -i <(curl -s https://install.aztec.network) + +# Guard against truncated curl downloads: bash must parse the entire {} block before executing any of it. +{ set -euo pipefail # Colors (truecolor with 256-color fallback) @@ -30,12 +33,20 @@ VERSION=${VERSION:-0.0.1} # Install URI (root, not version-specific) INSTALL_URI="${INSTALL_URI:-https://install.aztec-labs.com}" +# Check if version string is valid semver +function is_semver { + local version="$1" + local semver_regex='^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.]+)?$' + [[ "$version" =~ $semver_regex ]] +} + # Resolve alias (like "nightly") to actual version number. function resolve_version { local version="$1" - local semver_regex='^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.]+)?$' - if [[ "$version" =~ $semver_regex ]]; then - echo "$version" + # Strip leading v from semver-like inputs (v0.85.0 -> 0.85.0), but not aliases (v4-nightly) + local stripped="${version#v}" + if is_semver "$stripped"; then + echo "$stripped" else local resolved if ! resolved=$(curl -fsSL "$INSTALL_URI/aliases/$version" 2>/dev/null); then @@ -194,7 +205,7 @@ function update_path_env_var { if grep -q '\.aztec' "$shell_profile" 2>/dev/null; then # Remove old aztec PATH entries. local tmp_file=$(mktemp) - grep -Ev 'export PATH=.*/\.aztec/' "$shell_profile" > "$tmp_file" || true + grep -Ev 'export PATH="\$(HOME/\.aztec/|PATH:.*\.aztec/)' "$shell_profile" > "$tmp_file" || true mv "$tmp_file" "$shell_profile" fi @@ -242,3 +253,7 @@ function main { } main "$@" + +# Explicit exit prevents bash from reading past the closing brace. +exit +} diff --git a/aztec-up/bin/0.0.1/aztec-up b/aztec-up/bin/0.0.1/aztec-up index edf35680e404..8f8652954a05 100755 --- a/aztec-up/bin/0.0.1/aztec-up +++ b/aztec-up/bin/0.0.1/aztec-up @@ -103,8 +103,10 @@ function is_semver { # Resolve alias (like "nightly") to actual version number function resolve_version { local version="$1" - if is_semver "$version"; then - echo "$version" + # Strip leading v from semver-like inputs (v0.85.0 -> 0.85.0), but not aliases (v4-nightly) + local stripped="${version#v}" + if is_semver "$stripped"; then + echo "$stripped" else # Fetch alias file to get actual version local resolved @@ -314,6 +316,11 @@ function cmd_uninstall { exit 1 fi + if ! is_semver "$version"; then + echo "Error: Invalid version '$version'. Expected a semver string (e.g. 0.85.0)." + exit 1 + fi + local current_version current_version=$(get_current_version) diff --git a/aztec-up/bin/0.0.1/install b/aztec-up/bin/0.0.1/install index fddf1c45b54d..877d3c28dc09 100755 --- a/aztec-up/bin/0.0.1/install +++ b/aztec-up/bin/0.0.1/install @@ -2,6 +2,9 @@ # Per-version installer script # This script is called by aztec-up to install a specific version of the Aztec toolchain. # It expects VERSION and INSTALL_URI to be set. + +# Guard against truncated curl downloads: bash must parse the entire {} block before executing any of it. +{ set -euo pipefail # Colors @@ -232,3 +235,7 @@ function main { } main "$@" + +# Explicit exit prevents bash from reading past the closing brace. +exit +} diff --git a/barretenberg/acir_tests/browser-test-app/src/index.ts b/barretenberg/acir_tests/browser-test-app/src/index.ts index b6ef9924a3aa..af4bf7202a53 100644 --- a/barretenberg/acir_tests/browser-test-app/src/index.ts +++ b/barretenberg/acir_tests/browser-test-app/src/index.ts @@ -97,7 +97,7 @@ function installChonkGlobal() { logger.debug("starting test..."); const bb = await Barretenberg.new({ threads, logger: bbLogger }); const backend = new AztecClientBackend(acirBufs, bb, circuitNames); - const [_, proof, verificationKey] = await backend.prove( + const { proof, vk: verificationKey } = await backend.prove( witnessBufs, vkBufs ); diff --git a/barretenberg/cpp/bootstrap.sh b/barretenberg/cpp/bootstrap.sh index 7d6dff3049c7..74a44ede4f46 100755 --- a/barretenberg/cpp/bootstrap.sh +++ b/barretenberg/cpp/bootstrap.sh @@ -380,6 +380,8 @@ case "$cmd" in build ;; "ci") + # Ensure CRS is downloaded before running tests + ../crs/bootstrap.sh build test ;; diff --git a/barretenberg/cpp/pil/vm2/bytecode/bc_retrieval.pil b/barretenberg/cpp/pil/vm2/bytecode/bc_retrieval.pil index 360aca65b943..570945c7d132 100644 --- a/barretenberg/cpp/pil/vm2/bytecode/bc_retrieval.pil +++ b/barretenberg/cpp/pil/vm2/bytecode/bc_retrieval.pil @@ -140,16 +140,18 @@ pol commit instance_exists; // @boolean (by lookup into contract_instance_retrie #[CONTRACT_INSTANCE_RETRIEVAL] sel { address, - current_class_id, - instance_exists, + nullifier_tree_root, public_data_tree_root, - nullifier_tree_root + instance_exists, + current_class_id } in contract_instance_retrieval.sel { contract_instance_retrieval.address, - contract_instance_retrieval.current_class_id, - contract_instance_retrieval.exists, + contract_instance_retrieval.nullifier_tree_root, contract_instance_retrieval.public_data_tree_root, - contract_instance_retrieval.nullifier_tree_root + contract_instance_retrieval.exists, + // Note: don't need deployer_addr for bc retrieval + contract_instance_retrieval.current_class_id + // Note: don't need init_hash for bc retrieval }; //////////////////////////////////////////////// diff --git a/barretenberg/cpp/pil/vm2/opcodes/get_contract_instance.pil b/barretenberg/cpp/pil/vm2/opcodes/get_contract_instance.pil index 38098cde12ae..0cea9409ff82 100644 --- a/barretenberg/cpp/pil/vm2/opcodes/get_contract_instance.pil +++ b/barretenberg/cpp/pil/vm2/opcodes/get_contract_instance.pil @@ -1,92 +1,97 @@ include "../bytecode/contract_instance_retrieval.pil"; +include "../constants_gen.pil"; +include "../memory.pil"; +include "../precomputed.pil"; /** - * Dedicated opcode gadget for the GetContractInstance opcode. - * It interacts with core ContractInstanceRetrieval gadget. - * It performs write out-of-bounds checking, member enum validation, - * contract instance retrieval, instance member selection, and two memory writes. + * Dedicated opcode gadget for the GetContractInstance opcode. Dispatched by execution.pil + * to retrieve a contract instance member and write the result to memory. * - * In more detail: - * - Performs bounds checking for dst_offset+1 using AVM_HIGHEST_MEM_ADDRESS - * - Validates member_enum <= MAX (3) - * - Retrieves contract instance using the shared ContractInstanceRetrieval gadget - * - Only does this if there are no errors (write out-of-bounds or invalid enum) - * - Selects appropriate member based on enum (deployer/class_id/init_hash) - * - Performs two memory writes and tag assignments (again, only if there are no errors): - * - exists flag (U1) at dst_offset - * - member value (FF) at dst_offset+1 + * GADGET LOGIC: + * - Bounds-check dst_offset+1 via #[WRITE_OUT_OF_BOUNDS_CHECK]. If dst_offset is the + * maximum valid address, dst_offset+1 would be out of bounds. + * - Look up member_enum in the precomputed table (#[PRECOMPUTED_INFO]) to determine + * validity and which member is selected (deployer, class_id, or init_hash). + * The precomputed table covers the full 8-bit range: + * +-------+----------------------+-------------+-------------+--------------+ + * | idx | is_valid_member_enum | is_deployer | is_class_id | is_init_hash | + * +-------+----------------------+-------------+-------------+--------------+ + * | 0 | 1 | 1 | 0 | 0 | + * | 1 | 1 | 0 | 1 | 0 | + * | 2 | 1 | 0 | 0 | 1 | + * | 3+ | 0 | 0 | 0 | 0 | + * +-------+----------------------+-------------+-------------+--------------+ + * - Aggregate errors via #[ERROR_AGGREGATION]. sel_error is set when either + * is_valid_writes_in_bounds or is_valid_member_enum is false. + * - [no error only] Retrieve contract instance via the ContractInstanceRetrieval gadget + * (#[CONTRACT_INSTANCE_RETRIEVAL]). + * - [no error only] Select the member indicated by the enum (#[SELECTED_MEMBER]). + * - [no error only] Write two values to memory: + * - exists flag (U1) at dst_offset (#[MEM_WRITE_CONTRACT_INSTANCE_EXISTS]). + * - member value (FF) at dst_offset+1 (#[MEM_WRITE_CONTRACT_INSTANCE_MEMBER]). * - * Note: this gadget relies on execution to perform address resolution for address_offset and dst_offset, - * and to perform the memory-read and tag-checking of address. + * PRECONDITIONS: + * - Execution resolves rop[0] (address_offset) and rop[1] (dst_offset) via addressing, + * ensuring dst_offset is within valid memory bounds. + * - Execution reads register[0] = M[address_offset] (the contract address) from memory + * and tag-checks it as FF. This gadget receives the resolved value directly. + * - rop[2] (member_enum) is a U8 immediate operand, guaranteed to be 8-bit by execution. * - * Opcode operands (relevant in EXECUTION when interacting with this gadget): - * - rop[0]: address_offset (input offset operand) - * - rop[1]: dst_offset (output offset operand) - * - rop[2]: member_enum (immediate operand) - * Memory I/O: - * - register[0]: M[address_offset} aka address (input - read from memory BY EXECUTION, not here) - * - address is tagged-checked by execution/registers to be FF based on instruction spec. - * - register[1]: M[dst_offset] aka exists (output - written to memory by this gadget) - * - tagged by this gadget to be U1. - * - M[rop[1]+1]: M[dst_offset+1] aka memberValue (output - written to memory by this gadget) - * - tagged by this gadget to be FF. + * USAGE: + * Execution dispatches into this gadget via a permutation on sel + * (#[DISPATCH_TO_GET_CONTRACT_INSTANCE] in execution.pil): * - * Possible errors: - * - dst_offset+1 is an out-of-bounds memory offset. - * - enum value is invalid (out of range). + * sel_exec_dispatch_get_contract_instance { + * // inputs + * clk, + * register[0], // contract_address + * rop[1], // dst_offset + * rop[2], // member_enum + * context_id, // space_id + * nullifier_tree_root, + * public_data_tree_root, + * // outputs/errors + * sel_opcode_error + * } is get_contract_instance.sel { + * // inputs + * get_contract_instance.clk, + * get_contract_instance.contract_address, + * get_contract_instance.dst_offset, + * get_contract_instance.member_enum, + * get_contract_instance.space_id, + * get_contract_instance.nullifier_tree_root, + * get_contract_instance.public_data_tree_root, + * // outputs/errors + * get_contract_instance.sel_error + * }; * - * A precomputed table is used to retrieve the following selectors based on member_enum: - * - is_valid_member_enum - * - is_deployer - * - is_class_id - * - is_init_hash - * +-------+----------------------+---------------+-------------+---------------+ - * | Row | is_valid_member_enum | is_deployer | is_class_id | is_init_hash | - * | (idx) | | | | | - * +-------+----------------------+---------------+-------------+---------------+ - * | 0 | 1 | 1 | 0 | 0 | - * | 1 | 1 | 0 | 1 | 0 | - * | 2 | 1 | 0 | 0 | 1 | - * | 3+ | 0 | 0 | 0 | 0 | - * +-------+----------------------+---------------+-------------+---------------+ + * TRACE SHAPE: + * 1 row per event. Row 0 is reserved (sel=0) for the skippable gadget optimization. + * Both error and normal events produce exactly 1 row. * - * Usage from execution: + * ERROR HANDLING: + * Two error conditions, which are NOT mutually exclusive: + * - Write out-of-bounds: dst_offset == AVM_HIGHEST_MEM_ADDRESS (dst_offset+1 overflows). + * - Invalid member enum: member_enum >= 3 (precomputed table returns is_valid_member_enum=0). + * On error, the row has sel_error=1. The contract instance retrieval lookup and memory + * write permutations are disabled (their selectors is_valid_member_enum / is_valid_writes_in_bounds + * are 0), so no destination interactions fire for error rows. * - * sel_exec_dispatch_get_contract_instance { - * // inputs - * clk, - * register[0], - * rop[1], - * rop[2], - * context_id, - * context_stack.nullifier_tree_root, - * context_stack.public_data_tree_root, - * // outputs/errors - * sel_opcode_error - * } is get_contract_instance.sel { - * // inputs - * get_contract_instance.clk, - * get_contract_instance.contract_address, - * get_contract_instance.dst_offset, - * get_contract_instance.member_enum, - * get_contract_instance.space_id, - * get_contract_instance.nullifier_tree_root, - * get_contract_instance.public_data_tree_root, - * // outputs/errors - * get_contract_instance.sel_error - * }; + * INTERACTIONS: + * - precomputed.pil: Member enum validation and selector lookup (#[PRECOMPUTED_INFO]) + * - contract_instance_retrieval.pil: Instance retrieval (#[CONTRACT_INSTANCE_RETRIEVAL]) + * - memory.pil: Write exists flag to M[dst_offset] (#[MEM_WRITE_CONTRACT_INSTANCE_EXISTS]) + * - memory.pil: Write member value to M[dst_offset+1] (#[MEM_WRITE_CONTRACT_INSTANCE_MEMBER]) */ namespace get_contract_instance; - // Selector for when this gadget is active - pol commit sel; // @boolean - sel * (1 - sel) = 0; - - // No relations will be checked if this identity is satisfied. #[skippable_if] sel = 0; + pol commit sel; // @boolean + sel * (1 - sel) = 0; + // Interface columns pol commit clk; pol commit contract_address; @@ -107,12 +112,18 @@ namespace get_contract_instance; is_valid_writes_in_bounds * (1 - is_valid_writes_in_bounds) = 0; pol WRITES_OUT_OF_BOUNDS = 1 - is_valid_writes_in_bounds; pol DST_OFFSET_DIFF_MAX = constants.AVM_HIGHEST_MEM_ADDRESS - dst_offset; - pol commit dst_offset_diff_max_inv; + pol commit dst_offset_diff_max_inv; // @zero-check + // See https://github.com/AztecProtocol/aztec-packages/blob/next/barretenberg/cpp/pil/vm2/docs/recipes.md#with-error-support. #[WRITE_OUT_OF_BOUNDS_CHECK] sel * (DST_OFFSET_DIFF_MAX * (WRITES_OUT_OF_BOUNDS * (1 - dst_offset_diff_max_inv) + dst_offset_diff_max_inv) - 1 + WRITES_OUT_OF_BOUNDS) = 0; // Member selection helper columns // (from precomputed.pil's GETCONTRACTINSTANCE opcode precomputed columns) + // These are constrained only via the #[PRECOMPUTED_INFO] lookup when is_valid_writes_in_bounds == 1. + // When the lookup is disabled (writes out of bounds), is_valid_member_enum is forced to 0 by + // #[IS_VALID_MEMBER_ENUM_ONLY_SET_BY_PRECOMPUTED_LOOKUP]. is_deployer/is_class_id/is_init_hash + // are free in that case, but safe: they are only consumed in #[SELECTED_MEMBER] and the memory + // write permutations, all of which are gated on is_valid_member_enum (which is 0). pol commit is_valid_member_enum; // @boolean (by lookup when is_valid_writes_in_bounds == 1) pol commit is_deployer; // @boolean (by lookup when is_valid_writes_in_bounds == 1) pol commit is_class_id; // @boolean (by lookup when is_valid_writes_in_bounds == 1) @@ -152,7 +163,10 @@ namespace get_contract_instance; #[ERROR_AGGREGATION] sel_error = sel * (1 - is_valid_writes_in_bounds * is_valid_member_enum); - // Retrieved instance members and existence (from lookup to the core instance retrieval gadget) + // Retrieved instance members and existence (from lookup to the core instance retrieval gadget). + // These are constrained only via #[CONTRACT_INSTANCE_RETRIEVAL] when is_valid_member_enum == 1. + // When the lookup is disabled (error), these columns are free, but safe: they are only consumed + // in #[SELECTED_MEMBER] and the memory write permutations, all gated on is_valid_member_enum. pol commit instance_exists; pol commit retrieved_deployer_addr; pol commit retrieved_class_id; @@ -191,7 +205,7 @@ namespace get_contract_instance; pol commit member_write_offset; #[MEMBER_WRITE_OFFSET] member_write_offset = is_valid_writes_in_bounds * (dst_offset + 1); - // TODO: remove these once we can use constants in permutations + // Lookup constant support: Can be removed when we support constants in permutations. pol commit exists_tag; exists_tag = is_valid_writes_in_bounds * constants.MEM_TAG_U1; pol commit member_tag; @@ -214,8 +228,7 @@ namespace get_contract_instance; memory.tag, memory.rw }; - // TODO(dbanks12): consider reusing a single memory permutation for both writes, - // or do the first write in execution (after returning `exists` to execution). + #[MEM_WRITE_CONTRACT_INSTANCE_MEMBER] is_valid_member_enum { clk, diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_srs.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_srs.cpp index 0c886449656d..365eee1bb3ae 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_srs.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_srs.cpp @@ -15,14 +15,40 @@ namespace bb::bbapi { SrsInitSrs::Response SrsInitSrs::execute(BB_UNUSED BBApiRequest& request) && { - // Decompress 32-byte compressed points in parallel using native field arithmetic + constexpr size_t COMPRESSED_POINT_SIZE = 32; + constexpr size_t UNCOMPRESSED_POINT_SIZE = sizeof(g1::affine_element); // 64 + + size_t bytes_per_point = num_points > 0 ? points_buf.size() / num_points : 0; std::vector g1_points(num_points); - parallel_for([&](ThreadChunk chunk) { - for (auto i : chunk.range(static_cast(num_points))) { - uint256_t c = from_buffer(points_buf.data(), i * 32); - g1_points[i] = g1::affine_element::from_compressed(c); - } - }); + std::vector uncompressed_out; + + if (bytes_per_point == UNCOMPRESSED_POINT_SIZE) { + // Already uncompressed: fast path with from_buffer + parallel_for([&](ThreadChunk chunk) { + for (auto i : chunk.range(static_cast(num_points))) { + g1_points[i] = from_buffer(points_buf.data(), i * UNCOMPRESSED_POINT_SIZE); + } + }); + } else if (bytes_per_point == COMPRESSED_POINT_SIZE) { + // Compressed: decompress and return uncompressed bytes for caller to cache + parallel_for([&](ThreadChunk chunk) { + for (auto i : chunk.range(static_cast(num_points))) { + uint256_t c = from_buffer(points_buf.data(), i * COMPRESSED_POINT_SIZE); + g1_points[i] = g1::affine_element::from_compressed(c); + } + }); + // Serialize uncompressed points to return to caller for caching + uncompressed_out.resize(static_cast(num_points) * UNCOMPRESSED_POINT_SIZE); + parallel_for([&](ThreadChunk chunk) { + for (auto i : chunk.range(static_cast(num_points))) { + auto buf = to_buffer(g1_points[i]); + std::copy(buf.begin(), buf.end(), &uncompressed_out[i * UNCOMPRESSED_POINT_SIZE]); + } + }); + } else { + throw_or_abort("SrsInitSrs: invalid points_buf size. Expected 32 or 64 bytes per point, got " + + std::to_string(bytes_per_point)); + } // Parse G2 point from buffer (128 bytes) auto g2_point_elem = from_buffer(g2_point.data()); @@ -30,7 +56,7 @@ SrsInitSrs::Response SrsInitSrs::execute(BB_UNUSED BBApiRequest& request) && // Initialize BN254 SRS bb::srs::init_bn254_mem_crs_factory(g1_points, g2_point_elem); - return {}; + return { .points_buf = std::move(uncompressed_out) }; } SrsInitGrumpkinSrs::Response SrsInitGrumpkinSrs::execute(BB_UNUSED BBApiRequest& request) && diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_srs.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_srs.hpp index 8d789b6d568e..f59fc3ab4357 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_srs.hpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_srs.hpp @@ -22,12 +22,13 @@ struct SrsInitSrs { struct Response { static constexpr const char MSGPACK_SCHEMA_NAME[] = "SrsInitSrsResponse"; - uint8_t dummy = 0; // Empty response needs a dummy field for msgpack - SERIALIZATION_FIELDS(dummy); + std::vector + points_buf; // Uncompressed G1 points (64 bytes each), empty if input was already uncompressed + SERIALIZATION_FIELDS(points_buf); bool operator==(const Response&) const = default; }; - std::vector points_buf; // G1 points (32 bytes each, compressed) + std::vector points_buf; // G1 points: compressed (32 bytes each) or uncompressed (64 bytes each) uint32_t num_points; std::vector g2_point; // G2 point (128 bytes) Response execute(BBApiRequest& request) &&; diff --git a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp index 5e02b62258c0..ef886c47e147 100644 --- a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp +++ b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp @@ -5,10 +5,12 @@ #include "barretenberg/lmdblib/lmdb_write_transaction.hpp" #include "barretenberg/lmdblib/types.hpp" #include "lmdb.h" +#include #include #include #include #include +#include #include namespace bb::lmdblib { @@ -176,6 +178,51 @@ void LMDBStore::get(KeysVector& keys, OptionalValuesVector& values, LMDBDatabase } } +void LMDBStore::has(const KeyOptionalValuesVector& entries, std::vector& results, const std::string& name) +{ + auto string_cmp = [](const Key& a, const Key& b) { + return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end()); + }; + + std::set key_set(string_cmp); + for (const auto& entry : entries) { + key_set.insert(entry.first); + } + + KeysVector keys(key_set.begin(), key_set.end()); + OptionalValuesVector vals; + get(keys, vals, name); + + results.reserve(entries.size()); + + for (const auto& entry : entries) { + const auto& key = entry.first; + const auto& requested_values = entry.second; + + const auto key_it = std::find(keys.begin(), keys.end(), key); + if (key_it == keys.end()) { + results.push_back(false); + continue; + } + + const auto& values = vals[static_cast(key_it - keys.begin())]; + + if (!values.has_value()) { + results.push_back(false); + continue; + } + + if (!requested_values.has_value()) { + results.push_back(true); + continue; + } + + results.push_back(std::all_of(requested_values->begin(), requested_values->end(), [&](const auto& val) { + return std::find(values->begin(), values->end(), val) != values->end(); + })); + } +} + LMDBStore::Cursor::Ptr LMDBStore::create_cursor(ReadTransaction::SharedPtr tx, const std::string& dbName) { Database::SharedPtr db = get_database(dbName); diff --git a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp index a65a2324f805..ffcf8d8cdc33 100644 --- a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp +++ b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp @@ -47,6 +47,7 @@ class LMDBStore : public LMDBStoreBase { void put(std::vector& data); void get(KeysVector& keys, OptionalValuesVector& values, const std::string& name); + void has(const KeyOptionalValuesVector& entries, std::vector& results, const std::string& name); Cursor::Ptr create_cursor(ReadTransaction::SharedPtr tx, const std::string& dbName); diff --git a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp index a46e28eee454..75c677adc14b 100644 --- a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp +++ b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp @@ -1372,3 +1372,132 @@ TEST_F(LMDBStoreTest, can_read_data_from_multiple_threads) } } } + +TEST_F(LMDBStoreTest, has_returns_false_for_missing_key) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name); + + KeyOptionalValuesVector entries = { { get_key(0), std::nullopt } }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 1UL); + EXPECT_FALSE(results[0]); +} + +TEST_F(LMDBStoreTest, has_returns_true_for_existing_key) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name); + write_test_data({ name }, 3, 1, *store); + + KeyOptionalValuesVector entries = { { get_key(0), std::nullopt }, { get_key(1), std::nullopt } }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 2UL); + EXPECT_TRUE(results[0]); + EXPECT_TRUE(results[1]); +} + +TEST_F(LMDBStoreTest, has_returns_true_when_value_exists) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name, true); + write_test_data({ name }, 2, 3, *store); + + // Check that key 0 has value (0, 0) + ValuesVector requested = { get_value(0, 0) }; + KeyOptionalValuesVector entries = { { get_key(0), requested } }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 1UL); + EXPECT_TRUE(results[0]); +} + +TEST_F(LMDBStoreTest, has_returns_false_when_value_missing) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name, true); + write_test_data({ name }, 2, 3, *store); + + // Check for a value that doesn't exist under key 0 + ValuesVector requested = { get_value(99, 99) }; + KeyOptionalValuesVector entries = { { get_key(0), requested } }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 1UL); + EXPECT_FALSE(results[0]); +} + +TEST_F(LMDBStoreTest, has_checks_all_requested_values) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name, true); + write_test_data({ name }, 1, 3, *store); + + // All values present + ValuesVector all_present = { get_value(0, 0), get_value(0, 1), get_value(0, 2) }; + KeyOptionalValuesVector entries_all = { { get_key(0), all_present } }; + std::vector results_all; + store->has(entries_all, results_all, name); + EXPECT_TRUE(results_all[0]); + + // One value missing + ValuesVector one_missing = { get_value(0, 0), get_value(99, 99) }; + KeyOptionalValuesVector entries_missing = { { get_key(0), one_missing } }; + std::vector results_missing; + store->has(entries_missing, results_missing, name); + EXPECT_FALSE(results_missing[0]); +} + +TEST_F(LMDBStoreTest, has_handles_mixed_entries) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name, true); + write_test_data({ name }, 3, 2, *store); + + KeyOptionalValuesVector entries = { + { get_key(0), std::nullopt }, // key exists, no value check -> true + { get_key(99), std::nullopt }, // key missing -> false + { get_key(1), ValuesVector{ get_value(1, 0) } }, // key exists, value present -> true + { get_key(2), ValuesVector{ get_value(99, 99) } }, // key exists, value missing -> false + }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 4UL); + EXPECT_TRUE(results[0]); + EXPECT_FALSE(results[1]); + EXPECT_TRUE(results[2]); + EXPECT_FALSE(results[3]); +} + +TEST_F(LMDBStoreTest, has_deduplicates_keys) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name); + write_test_data({ name }, 2, 1, *store); + + // Same key appearing twice with different value checks + KeyOptionalValuesVector entries = { + { get_key(0), std::nullopt }, + { get_key(0), ValuesVector{ get_value(0, 0) } }, + }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 2UL); + EXPECT_TRUE(results[0]); + EXPECT_TRUE(results[1]); +} diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp index b98dcb892409..94b3ca562283 100644 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp +++ b/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp @@ -3,10 +3,8 @@ #include "barretenberg/lmdblib/types.hpp" #include "barretenberg/nodejs_module/lmdb_store/lmdb_store_message.hpp" #include "napi.h" -#include #include #include -#include #include #include #include @@ -117,52 +115,9 @@ GetResponse LMDBStoreWrapper::get(const GetRequest& req) HasResponse LMDBStoreWrapper::has(const HasRequest& req) { - auto string_cmp = [](const std::vector& a, const std::vector& b) { - return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end()); - }; - verify_store(); - std::set key_set(string_cmp); - for (const auto& entry : req.entries) { - key_set.insert(entry.first); - } - - lmdblib::KeysVector keys(key_set.begin(), key_set.end()); - lmdblib::OptionalValuesVector vals; - _store->get(keys, vals, req.db); - std::vector exists; - - for (const auto& entry : req.entries) { - const auto& key = entry.first; - const auto& requested_values = entry.second; - - const auto& key_it = std::find(keys.begin(), keys.end(), key); - if (key_it == keys.end()) { - // this shouldn't happen. It means we missed a key when we created the key_set - exists.push_back(false); - continue; - } - - // should be fine to convert this to an index in the array? - const auto& values = vals[static_cast(key_it - keys.begin())]; - - if (!values.has_value()) { - exists.push_back(false); - continue; - } - - // client just wanted to know if the key exists - if (!requested_values.has_value()) { - exists.push_back(true); - continue; - } - - exists.push_back(std::all_of(requested_values->begin(), requested_values->end(), [&](const auto& val) { - return std::find(values->begin(), values->end(), val) != values->begin(); - })); - } - + _store->has(req.entries, exists, req.db); return { exists }; } diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/world_state/world_state.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/world_state/world_state.cpp index 2386799b19a0..9087aceeb19e 100644 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/world_state/world_state.cpp +++ b/barretenberg/cpp/src/barretenberg/nodejs_module/world_state/world_state.cpp @@ -514,6 +514,8 @@ bool WorldStateWrapper::find_leaf_indices(msgpack::object& obj, msgpack::sbuffer request.value.revision, request.value.treeId, r3.value.leaves, response.indices, r3.value.startIndex); break; } + default: + throw std::runtime_error("Unsupported tree type"); } MsgHeader header(request.header.messageId); @@ -555,6 +557,8 @@ bool WorldStateWrapper::find_sibling_paths(msgpack::object& obj, msgpack::sbuffe request.value.revision, request.value.treeId, r3.value.leaves, response.paths); break; } + default: + throw std::runtime_error("Unsupported tree type"); } MsgHeader header(request.header.messageId); @@ -607,6 +611,8 @@ bool WorldStateWrapper::append_leaves(msgpack::object& obj, msgpack::sbuffer& bu _ws->append_leaves(r3.value.treeId, r3.value.leaves, r3.value.forkId); break; } + default: + throw std::runtime_error("Unsupported tree type"); } MsgHeader header(request.header.messageId); diff --git a/barretenberg/cpp/src/barretenberg/srs/factories/crs_factory.test.cpp b/barretenberg/cpp/src/barretenberg/srs/factories/crs_factory.test.cpp index 01fe026da882..d08beea40f0e 100644 --- a/barretenberg/cpp/src/barretenberg/srs/factories/crs_factory.test.cpp +++ b/barretenberg/cpp/src/barretenberg/srs/factories/crs_factory.test.cpp @@ -25,13 +25,8 @@ void check_bn254_consistency(const fs::path& crs_download_path, size_t num_point { NativeBn254CrsFactory file_crs(crs_download_path, allow_download); - // read compressed G1 and decompress - auto g1_compressed = read_file(bb::srs::bb_crs_path() / "bn254_g1_compressed.dat", num_points * sizeof(uint256_t)); - std::vector g1_points(num_points); - for (size_t i = 0; i < num_points; ++i) { - auto c = from_buffer(g1_compressed, i * sizeof(uint256_t)); - g1_points[i] = g1::affine_element::from_compressed(c); - } + // Use get_bn254_g1_data to load reference points (handles compressed/uncompressed automatically) + auto g1_points = bb::get_bn254_g1_data(bb::srs::bb_crs_path(), num_points, /*allow_download=*/false); // read G2 auto g2_buf = read_file(bb::srs::bb_crs_path() / "bn254_g2.dat", sizeof(g2::affine_element)); diff --git a/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.cpp b/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.cpp index db0c179ab55f..4b95fcefb2a9 100644 --- a/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.cpp +++ b/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.cpp @@ -18,6 +18,37 @@ constexpr const char* CRS_PRIMARY_URL = "http://crs.aztec-cdn.foundation/g1_comp // Fallback CRS URL (AWS S3) constexpr const char* CRS_FALLBACK_URL = "http://crs.aztec-labs.com/g1_compressed.dat"; constexpr size_t COMPRESSED_POINT_SIZE = 32; +constexpr size_t UNCOMPRESSED_POINT_SIZE = 64; // sizeof(g1::affine_element) + +/** + * @brief Write decompressed G1 points to a file in uncompressed format (64 bytes each). + */ +void write_uncompressed_g1_points(const std::vector& points, const std::filesystem::path& path) +{ + std::vector buf(points.size() * UNCOMPRESSED_POINT_SIZE); + bb::parallel_for([&](bb::ThreadChunk chunk) { + for (auto i : chunk.range(points.size())) { + auto serialized = to_buffer(points[i]); + std::copy(serialized.begin(), serialized.end(), &buf[i * UNCOMPRESSED_POINT_SIZE]); + } + }); + bb::write_file(path, buf); +} + +/** + * @brief Read uncompressed G1 points (64 bytes each) from a file. + */ +std::vector read_uncompressed_g1_points(const std::filesystem::path& path, size_t num_points) +{ + auto data = bb::read_file(path, num_points * UNCOMPRESSED_POINT_SIZE); + std::vector points(num_points); + bb::parallel_for([&](bb::ThreadChunk chunk) { + for (auto i : chunk.range(num_points)) { + points[i] = from_buffer(data, i * UNCOMPRESSED_POINT_SIZE); + } + }); + return points; +} /** * @brief Round num_points up to the next chunk boundary so every downloaded byte is hash-verified. @@ -166,20 +197,32 @@ std::vector get_bn254_g1_data(const std::filesystem::path& p BB_BENCH_NAME("get_bn254_g1_data"); std::filesystem::create_directories(path); + auto uncompressed_path = path / "bn254_g1.dat"; auto compressed_path = path / "bn254_g1_compressed.dat"; auto lock_path = path / "crs.lock"; // Acquire exclusive lock to prevent simultaneous downloads FileLockGuard lock(lock_path.string()); + // 1. Prefer cached uncompressed (fastest: parallel from_buffer, ~0.3s for 2^20 points) + size_t uncompressed_points = get_file_size(uncompressed_path) / UNCOMPRESSED_POINT_SIZE; + if (uncompressed_points >= num_points) { + vinfo("using cached uncompressed bn254 crs with ", uncompressed_points, " points at ", uncompressed_path); + return read_uncompressed_g1_points(uncompressed_path, num_points); + } + + // 2. Fall back to compressed on disk: decompress and cache uncompressed size_t compressed_points = get_file_size(compressed_path) / COMPRESSED_POINT_SIZE; if (compressed_points >= num_points) { - vinfo("using cached bn254 crs with ", std::to_string(compressed_points), " points at ", compressed_path); + vinfo("decompressing cached compressed bn254 crs (", compressed_points, " points)..."); auto data = read_file(compressed_path, num_points * COMPRESSED_POINT_SIZE); - return decompress_g1_points(data, num_points); + auto points = decompress_g1_points(data, num_points); + write_uncompressed_g1_points(points, uncompressed_path); + vinfo("cached uncompressed bn254 crs at ", uncompressed_path); + return points; } if (!allow_download && compressed_points == 0) { - throw_or_abort("bn254 g1 compressed data not found at " + compressed_path.string() + + throw_or_abort("bn254 g1 data not found at " + path.string() + " and bb does not automatically download in this context." + " Run barretenberg/crs/bootstrap.sh to download."); } else if (!allow_download) { @@ -191,16 +234,26 @@ std::vector get_bn254_g1_data(const std::filesystem::path& p } // Double-check after acquiring lock (another process may have downloaded while we waited) + uncompressed_points = get_file_size(uncompressed_path) / UNCOMPRESSED_POINT_SIZE; + if (uncompressed_points >= num_points) { + return read_uncompressed_g1_points(uncompressed_path, num_points); + } compressed_points = get_file_size(compressed_path) / COMPRESSED_POINT_SIZE; if (compressed_points >= num_points) { auto data = read_file(compressed_path, num_points * COMPRESSED_POINT_SIZE); - return decompress_g1_points(data, num_points); + auto points = decompress_g1_points(data, num_points); + write_uncompressed_g1_points(points, uncompressed_path); + return points; } + // 3. Download compressed, decompress, cache uncompressed vinfo("downloading bn254 crs..."); auto data = download_bn254_g1_data(num_points, primary_url, fallback_url); write_file(compressed_path, data); - return decompress_g1_points(data, num_points); + auto points = decompress_g1_points(data, num_points); + write_uncompressed_g1_points(points, uncompressed_path); + vinfo("cached uncompressed bn254 crs at ", uncompressed_path); + return points; } // Default overload using production URLs diff --git a/barretenberg/cpp/src/barretenberg/vm2/generated/relations/lookups_bc_retrieval.hpp b/barretenberg/cpp/src/barretenberg/vm2/generated/relations/lookups_bc_retrieval.hpp index c06fe50c9e96..701c6a427314 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/generated/relations/lookups_bc_retrieval.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/generated/relations/lookups_bc_retrieval.hpp @@ -23,17 +23,17 @@ struct lookup_bc_retrieval_contract_instance_retrieval_settings_ { static constexpr Column INVERSES = Column::lookup_bc_retrieval_contract_instance_retrieval_inv; static constexpr std::array SRC_COLUMNS = { ColumnAndShifts::bc_retrieval_address, - ColumnAndShifts::bc_retrieval_current_class_id, - ColumnAndShifts::bc_retrieval_instance_exists, + ColumnAndShifts::bc_retrieval_nullifier_tree_root, ColumnAndShifts::bc_retrieval_public_data_tree_root, - ColumnAndShifts::bc_retrieval_nullifier_tree_root + ColumnAndShifts::bc_retrieval_instance_exists, + ColumnAndShifts::bc_retrieval_current_class_id }; static constexpr std::array DST_COLUMNS = { ColumnAndShifts::contract_instance_retrieval_address, - ColumnAndShifts::contract_instance_retrieval_current_class_id, - ColumnAndShifts::contract_instance_retrieval_exists, + ColumnAndShifts::contract_instance_retrieval_nullifier_tree_root, ColumnAndShifts::contract_instance_retrieval_public_data_tree_root, - ColumnAndShifts::contract_instance_retrieval_nullifier_tree_root + ColumnAndShifts::contract_instance_retrieval_exists, + ColumnAndShifts::contract_instance_retrieval_current_class_id }; }; diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/events/get_contract_instance_event.hpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/events/get_contract_instance_event.hpp index 5b4eeeb37de1..a9d7b7195c10 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/events/get_contract_instance_event.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/events/get_contract_instance_event.hpp @@ -18,20 +18,20 @@ struct GetContractInstanceException : public std::runtime_error { struct GetContractInstanceEvent { // Interface columns - uint32_t execution_clk; - AztecAddress contract_address; - MemoryAddress dst_offset; - uint8_t member_enum; - uint16_t space_id; - FF nullifier_tree_root; - FF public_data_tree_root; + uint32_t execution_clk = 0; + AztecAddress contract_address = 0; + MemoryAddress dst_offset = 0; + uint8_t member_enum = 0; + uint16_t space_id = 0; + FF nullifier_tree_root = 0; + FF public_data_tree_root = 0; // Instance retrieval results including all three members which are all needed for tracegen // despite only needing the selected member in simulation. - bool instance_exists; - FF retrieved_deployer_addr; - FF retrieved_class_id; - FF retrieved_init_hash; + bool instance_exists = false; + FF retrieved_deployer_addr = 0; + FF retrieved_class_id = 0; + FF retrieved_init_hash = 0; }; } // namespace bb::avm2::simulation diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/get_contract_instance.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/get_contract_instance.cpp index df55e5d05bc1..674a2d035ab9 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/get_contract_instance.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/get_contract_instance.cpp @@ -1,18 +1,24 @@ #include "barretenberg/vm2/simulation/gadgets/get_contract_instance.hpp" -#include -#include #include +#include +#include #include "barretenberg/vm2/common/aztec_constants.hpp" -#include "barretenberg/vm2/common/aztec_types.hpp" -#include "barretenberg/vm2/common/field.hpp" -#include "barretenberg/vm2/common/memory_types.hpp" #include "barretenberg/vm2/common/stringify.hpp" -#include "barretenberg/vm2/simulation/events/get_contract_instance_event.hpp" +#include "barretenberg/vm2/common/uint1.hpp" +#include "barretenberg/vm2/simulation/interfaces/memory.hpp" namespace bb::avm2::simulation { +/** + * @brief Construct a GetContractInstance gadget with its dependencies. + * + * @param execution_id_manager Provides monotonic execution clock values for event ordering. + * @param merkle_db High-level Merkle database for reading tree state (roots). + * @param event_emitter Emitter for GetContractInstanceEvent used by tracegen. + * @param instance_manager Manager that retrieves (and caches) contract instances from the world state. + */ GetContractInstance::GetContractInstance(ExecutionIdManagerInterface& execution_id_manager, HighLevelMerkleDBInterface& merkle_db, EventEmitterInterface& event_emitter, @@ -23,61 +29,88 @@ GetContractInstance::GetContractInstance(ExecutionIdManagerInterface& execution_ , instance_manager(instance_manager) {} +/** + * @brief Retrieve a contract instance member and write the result to memory. + * + * Validates that dst_offset+1 is in bounds and that member_enum is valid, then retrieves the + * contract instance via the ContractInstanceManager. Writes the existence flag (U1) to dst_offset + * and the selected member value (FF) to dst_offset+1. + * + * @param memory The memory interface for the current context. + * @param contract_address The address of the contract to look up. + * @param dst_offset The memory offset at which to write the exists flag. + * @param member_enum The enum selecting which instance member to retrieve (deployer/class_id/init_hash). + * @throws GetContractInstanceException If dst_offset+1 is out of bounds (checked first). + * @throws GetContractInstanceException If member_enum is invalid (checked after bounds check). + */ void GetContractInstance::get_contract_instance(MemoryInterface& memory, const AztecAddress& contract_address, MemoryAddress dst_offset, uint8_t member_enum) { const auto& tree_state = merkle_db.get_tree_state(); - GetContractInstanceEvent event{ - .execution_clk = execution_id_manager.get_execution_id(), - .contract_address = contract_address, - .dst_offset = dst_offset, - .member_enum = member_enum, - .space_id = memory.get_space_id(), - .nullifier_tree_root = tree_state.nullifier_tree.tree.root, - .public_data_tree_root = tree_state.public_data_tree.tree.root, - }; + const auto execution_clk = execution_id_manager.get_execution_id(); + const auto space_id = memory.get_space_id(); + const auto& nullifier_tree_root = tree_state.nullifier_tree.tree.root; + const auto& public_data_tree_root = tree_state.public_data_tree.tree.root; // Memory bounds checking for dst_offset+1 // Note that execution does address resolution for dst_offset, so we already // know that dst_offset is in bounds. // So, the only scenario when dstOffset+1 can be out of bounds is if dstOffset == MAX address. if (dst_offset == AVM_HIGHEST_MEM_ADDRESS) { - event_emitter.emit(std::move(event)); + event_emitter.emit({ .execution_clk = execution_clk, + .contract_address = contract_address, + .dst_offset = dst_offset, + .member_enum = member_enum, + .space_id = space_id, + .nullifier_tree_root = nullifier_tree_root, + .public_data_tree_root = public_data_tree_root }); throw GetContractInstanceException("Write dst out of range: " + field_to_string(dst_offset)); } // Member enum validation if (member_enum > static_cast(ContractInstanceMember::MAX)) { - event_emitter.emit(std::move(event)); + event_emitter.emit({ .execution_clk = execution_clk, + .contract_address = contract_address, + .dst_offset = dst_offset, + .member_enum = member_enum, + .space_id = space_id, + .nullifier_tree_root = nullifier_tree_root, + .public_data_tree_root = public_data_tree_root }); throw GetContractInstanceException("Invalid member enum: " + std::to_string(member_enum)); } // Retrieve contract instance using shared ContractInstanceManager - auto maybe_instance = instance_manager.get_contract_instance(event.contract_address); - bool instance_exists = maybe_instance.has_value(); - event.instance_exists = instance_exists; + auto maybe_instance = instance_manager.get_contract_instance(contract_address); + const bool instance_exists = maybe_instance.has_value(); - // Extract all member values for event (even if we only use one for the memory write) - // This is needed for the PIL gadget trace generation which includes all retrieved members - FF selected_member_value = 0; // default if instance does not exist - if (instance_exists) { - const auto& instance = maybe_instance.value(); - event.retrieved_deployer_addr = instance.deployer; - event.retrieved_class_id = instance.current_contract_class_id; - event.retrieved_init_hash = instance.initialization_hash; - - // Select the requested member based on the enum - selected_member_value = select_instance_member(instance, member_enum); - } - - // Perform two memory writes + // Select the requested member and write results to memory + const FF selected_member_value = + instance_exists ? select_instance_member(maybe_instance.value(), member_enum) : FF(0); write_results(memory, dst_offset, instance_exists, selected_member_value); - event_emitter.emit(std::move(event)); + event_emitter.emit({ .execution_clk = execution_clk, + .contract_address = contract_address, + .dst_offset = dst_offset, + .member_enum = member_enum, + .space_id = space_id, + .nullifier_tree_root = nullifier_tree_root, + .public_data_tree_root = public_data_tree_root, + .instance_exists = instance_exists, + .retrieved_deployer_addr = instance_exists ? maybe_instance->deployer : FF(0), + .retrieved_class_id = instance_exists ? maybe_instance->current_contract_class_id : FF(0), + .retrieved_init_hash = instance_exists ? maybe_instance->initialization_hash : FF(0) }); } +/** + * @brief Write the contract instance existence flag and member value to memory. + * + * @param memory The memory interface for the current context. + * @param dst_offset The memory offset at which to write the exists flag (U1). + * @param exists Whether the contract instance was found. + * @param member_value The selected member value to write at dst_offset+1 (FF). + */ void GetContractInstance::write_results(MemoryInterface& memory, MemoryAddress dst_offset, bool exists, @@ -89,6 +122,14 @@ void GetContractInstance::write_results(MemoryInterface& memory, memory.set(dst_offset + 1, MemoryValue::from(member_value)); } +/** + * @brief Select a contract instance member by enum value. + * + * @param instance The contract instance to select from. + * @param member_enum The enum value identifying which member to return. + * @return The field value of the selected member. + * @throws std::runtime_error If member_enum is not a valid ContractInstanceMember (should be unreachable). + */ FF GetContractInstance::select_instance_member(const ContractInstance& instance, uint8_t member_enum) { switch (static_cast(member_enum)) { diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/get_contract_instance.hpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/get_contract_instance.hpp index 86fda62ea73b..899c73e0dbd0 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/get_contract_instance.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/get_contract_instance.hpp @@ -7,8 +7,8 @@ #include "barretenberg/vm2/common/memory_types.hpp" #include "barretenberg/vm2/simulation/events/event_emitter.hpp" #include "barretenberg/vm2/simulation/events/get_contract_instance_event.hpp" -#include "barretenberg/vm2/simulation/gadgets/contract_instance_manager.hpp" -#include "barretenberg/vm2/simulation/gadgets/memory.hpp" +#include "barretenberg/vm2/simulation/interfaces/contract_instance_manager.hpp" +#include "barretenberg/vm2/simulation/interfaces/db.hpp" #include "barretenberg/vm2/simulation/interfaces/get_contract_instance.hpp" #include "barretenberg/vm2/simulation/lib/execution_id_manager.hpp" diff --git a/barretenberg/cpp/src/barretenberg/vm2/tracegen/lib/get_contract_instance_spec.cpp b/barretenberg/cpp/src/barretenberg/vm2/tracegen/lib/get_contract_instance_spec.cpp index db8c249c24e5..3522e60e2187 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/tracegen/lib/get_contract_instance_spec.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/tracegen/lib/get_contract_instance_spec.cpp @@ -3,7 +3,15 @@ namespace bb::avm2::tracegen { -// See ASCII table in `get_contract_instance.pil` for reference. +/** + * @brief Look up the precomputed table entry for a given member enum value. + * + * Returns boolean selectors indicating whether the enum is valid and which member it selects. + * See the ASCII table in get_contract_instance.pil for the full mapping. + * + * @param member_enum The member enum value (0=deployer, 1=class_id, 2=init_hash, 3+=invalid). + * @return A Table struct with is_valid_member_enum and the per-member selector flags. + */ GetContractInstanceSpec::Table GetContractInstanceSpec::get_table(uint8_t member_enum) { // default for invalid enum diff --git a/barretenberg/cpp/src/barretenberg/vm2/tracegen/lib/get_contract_instance_spec.hpp b/barretenberg/cpp/src/barretenberg/vm2/tracegen/lib/get_contract_instance_spec.hpp index 234507f6b5ed..18509f022d31 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/tracegen/lib/get_contract_instance_spec.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/tracegen/lib/get_contract_instance_spec.hpp @@ -2,8 +2,6 @@ #include -#include "barretenberg/vm2/common/aztec_types.hpp" - namespace bb::avm2::tracegen { class GetContractInstanceSpec { diff --git a/barretenberg/cpp/src/barretenberg/vm2/tracegen/opcodes/get_contract_instance_trace.cpp b/barretenberg/cpp/src/barretenberg/vm2/tracegen/opcodes/get_contract_instance_trace.cpp index 8061d021c868..bb01dbf659b5 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/tracegen/opcodes/get_contract_instance_trace.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/tracegen/opcodes/get_contract_instance_trace.cpp @@ -1,24 +1,31 @@ #include "barretenberg/vm2/tracegen/opcodes/get_contract_instance_trace.hpp" -#include #include #include "barretenberg/vm2/common/aztec_constants.hpp" -#include "barretenberg/vm2/common/aztec_types.hpp" -#include "barretenberg/vm2/common/constants.hpp" -#include "barretenberg/vm2/common/memory_types.hpp" +#include "barretenberg/vm2/common/field.hpp" #include "barretenberg/vm2/common/tagged_value.hpp" #include "barretenberg/vm2/generated/columns.hpp" #include "barretenberg/vm2/generated/relations/lookups_get_contract_instance.hpp" -#include "barretenberg/vm2/simulation/events/event_emitter.hpp" -#include "barretenberg/vm2/simulation/events/get_contract_instance_event.hpp" #include "barretenberg/vm2/tracegen/lib/get_contract_instance_spec.hpp" -#include "barretenberg/vm2/tracegen/lib/interaction_def.hpp" namespace bb::avm2::tracegen { using C = Column; +/** + * @brief Process the GetContractInstance events and populate the relevant columns in the trace. + * + * Events are emitted in the following flavors: + * - Out-of-bounds error: dst_offset == AVM_HIGHEST_MEM_ADDRESS. Instance retrieval fields are + * unpopulated (defaults). Event is emitted before the exception is thrown. + * - Invalid enum error: member_enum > MAX. Instance retrieval fields are unpopulated (defaults). + * Event is emitted before the exception is thrown. + * - Normal execution: all fields populated including instance_exists and retrieved member values. + * + * @param events Container of GetContractInstanceEvent to process. + * @param trace The trace container to populate. + */ void GetContractInstanceTraceBuilder::process( const simulation::EventEmitterInterface::Container& events, TraceContainer& trace) diff --git a/barretenberg/cpp/src/barretenberg/vm2/tracegen/opcodes/get_contract_instance_trace.hpp b/barretenberg/cpp/src/barretenberg/vm2/tracegen/opcodes/get_contract_instance_trace.hpp index 3b8c8c52a2ec..41d81f3c2936 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/tracegen/opcodes/get_contract_instance_trace.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/tracegen/opcodes/get_contract_instance_trace.hpp @@ -1,6 +1,5 @@ #pragma once -#include "barretenberg/vm2/generated/columns.hpp" #include "barretenberg/vm2/simulation/events/event_emitter.hpp" #include "barretenberg/vm2/simulation/events/get_contract_instance_event.hpp" #include "barretenberg/vm2/tracegen/lib/interaction_def.hpp" diff --git a/barretenberg/ts/src/barretenberg/backend.ts b/barretenberg/ts/src/barretenberg/backend.ts index 75b37731e531..c08ec0454c9a 100644 --- a/barretenberg/ts/src/barretenberg/backend.ts +++ b/barretenberg/ts/src/barretenberg/backend.ts @@ -276,6 +276,34 @@ export class UltraHonkBackend { } } +/** + * Result of proving with the AztecClientBackend. + */ +export interface AztecClientProveResult { + /** Flat proof field elements (Uint8Array[] of 32-byte fields, includes public inputs). */ + proofFields: Uint8Array[]; + /** Msgpack-encoded ChonkProof (for verification via the bb API). */ + proof: Uint8Array; + /** Verification key bytes. */ + vk: Uint8Array; + /** Chonk-compressed proof bytes (point compression + u256 encoding, ~1.7x smaller). */ + compressedProof?: Uint8Array; +} + +/** + * Flatten a structured bb.js ChonkProof into a flat array of field element Uint8Arrays. + * The order matches the C++ ChonkProof::to_field_elements() layout. + */ +export function flattenChonkProofFields(proof: ChonkProof): Uint8Array[] { + return [ + proof.hidingOinkProof, + proof.mergeProof, + proof.eccvmProof, + proof.ipaProof, + proof.jointProof, + ].flat(); +} + export class AztecClientBackend { // These type assertions are used so that we don't // have to initialize `api` in the constructor. @@ -288,7 +316,12 @@ export class AztecClientBackend { private circuitNames: string[] = [], ) {} - async prove(witnessBuf: Uint8Array[], vksBuf: Uint8Array[] = []): Promise<[Uint8Array[], Uint8Array, Uint8Array]> { + async prove( + witnessBuf: Uint8Array[], + vksBuf?: Uint8Array[], + options?: { compress?: boolean }, + ): Promise { + vksBuf = vksBuf ?? []; if (vksBuf.length !== 0 && this.acirBuf.length !== witnessBuf.length) { throw new AztecClientBackendError('Witness and bytecodes must have the same stack depth!'); } @@ -335,19 +368,18 @@ export class AztecClientBackend { }, }); - const proofFields = [ - proveResult.proof.hidingOinkProof, - proveResult.proof.mergeProof, - proveResult.proof.eccvmProof, - proveResult.proof.ipaProof, - proveResult.proof.jointProof, - ].flat(); + const proofFields = flattenChonkProofFields(proveResult.proof); // Verify using native proof directly to avoid redundant encode/decode cycle if (!(await this.verifyNative(proveResult.proof, vkResult.bytes))) { throw new AztecClientBackendError('Failed to verify the private (Chonk) transaction proof!'); } - return [proofFields, proof, vkResult.bytes]; + + const compressedProof = options?.compress + ? (await this.api.chonkCompressProof({ proof: proveResult.proof })).compressedProof + : undefined; + + return { proofFields, proof, vk: vkResult.bytes, compressedProof }; } async verify(proof: Uint8Array, vk: Uint8Array): Promise { @@ -358,6 +390,30 @@ export class AztecClientBackend { return result.valid; } + /** + * Compress a ChonkProof using point compression and u256 encoding. + * Reduces proof size by ~1.7x (e.g. ~60KB to ~35KB). + * + * @param proof - Structured ChonkProof from the bb API (e.g. from chonkProve) + * @returns Compressed proof bytes (each element encoded as 32 bytes) + */ + async compressProof(proof: ChonkProof): Promise { + const result = await this.api.chonkCompressProof({ proof }); + return result.compressedProof; + } + + /** + * Decompress a previously compressed ChonkProof. + * The number of public inputs is derived automatically from the compressed size. + * + * @param compressedProof - Compressed proof bytes from compressProof() + * @returns Structured ChonkProof suitable for verification + */ + async decompressProof(compressedProof: Uint8Array): Promise { + const result = await this.api.chonkDecompressProof({ compressedProof }); + return result.proof; + } + /** * Internal verification using native ChonkProof type. * Avoids encode/decode cycle when called from prove(). diff --git a/barretenberg/ts/src/barretenberg/index.ts b/barretenberg/ts/src/barretenberg/index.ts index 2290165aeb4a..96c34a5b2bc3 100644 --- a/barretenberg/ts/src/barretenberg/index.ts +++ b/barretenberg/ts/src/barretenberg/index.ts @@ -9,8 +9,10 @@ export { UltraHonkBackend, UltraHonkVerifierBackend, AztecClientBackend, + flattenChonkProofFields, fieldToString, fieldsToStrings, + type AztecClientProveResult, type UltraHonkBackendOptions, type VerifierTarget, } from './backend.js'; @@ -80,8 +82,16 @@ export class Barretenberg extends AsyncApi { const grumpkinCrs = await GrumpkinCrs.new(2 ** 16, this.options.crsPath, this.options.logger); // Load CRS into wasm global CRS state. - // TODO: Make RawBuffer be default behavior, and have a specific Vector type for when wanting length prefixed. - await this.srsInitSrs({ pointsBuf: crs.getG1Data(), numPoints: crs.numPoints, g2Point: crs.getG2Data() }); + // srsInitSrs auto-detects compressed (32B/point) vs uncompressed (64B/point). + // When decompressing, it returns the uncompressed bytes so we can cache them. + const response = await this.srsInitSrs({ + pointsBuf: crs.getG1Data(), + numPoints: crs.numPoints, + g2Point: crs.getG2Data(), + }); + if (response.pointsBuf.length > 0) { + await crs.cacheUncompressed(response.pointsBuf); + } await this.srsInitGrumpkinSrs({ pointsBuf: grumpkinCrs.getG1Data(), numPoints: grumpkinCrs.numPoints }); } diff --git a/barretenberg/ts/src/bbapi/exception_handling.test.ts b/barretenberg/ts/src/bbapi/exception_handling.test.ts index 3e95168623dc..11dd8eb7699d 100644 --- a/barretenberg/ts/src/bbapi/exception_handling.test.ts +++ b/barretenberg/ts/src/bbapi/exception_handling.test.ts @@ -47,7 +47,7 @@ describe('BBApi Exception Handling from bb.js', () => { expect(error).toBeInstanceOf(Error); expect((error as Error).message).toBeTruthy(); expect((error as Error).message.length).toBeGreaterThan(0); - expect((error as Error).message).toContain('g1_identity'); + expect((error as Error).message).toContain('invalid points_buf size'); console.log('Successfully caught exception from bb.js with message:', (error as Error).message); } }); diff --git a/barretenberg/ts/src/cbind/schema_visitor.ts b/barretenberg/ts/src/cbind/schema_visitor.ts index bad05c3c4373..ed4f83db7ccc 100644 --- a/barretenberg/ts/src/cbind/schema_visitor.ts +++ b/barretenberg/ts/src/cbind/schema_visitor.ts @@ -197,6 +197,7 @@ export class SchemaVisitor { 'unsigned int': 'u32', 'unsigned short': 'u16', 'unsigned long': 'u64', + 'unsigned long long': 'u64', 'unsigned char': 'u8', 'double': 'f64', 'string': 'string', diff --git a/barretenberg/ts/src/crs/browser/cached_net_crs.ts b/barretenberg/ts/src/crs/browser/cached_net_crs.ts index c50578643c1f..2c0acddda94d 100644 --- a/barretenberg/ts/src/crs/browser/cached_net_crs.ts +++ b/barretenberg/ts/src/crs/browser/cached_net_crs.ts @@ -19,19 +19,21 @@ export class CachedNetCrs { * Download the data. */ async init() { - const g1Compressed = await get('g1DataCompressed'); const g2Data = await get('g2Data'); - const netCrs = new NetCrs(this.numPoints); - const compressedLength = this.numPoints * 32; - if (g1Compressed && g1Compressed.length >= compressedLength) { - this.g1Data = g1Compressed; + // Prefer cached uncompressed (64 bytes/point, fast path: no decompression needed) + const g1Uncompressed = await get('g1Data'); + const uncompressedLength = this.numPoints * 64; + if (g1Uncompressed && g1Uncompressed.length >= uncompressedLength) { + this.g1Data = g1Uncompressed; } else { + // Download compressed from CDN + const netCrs = new NetCrs(this.numPoints); this.g1Data = await netCrs.downloadG1Data(); - await set('g1DataCompressed', this.g1Data); } if (!g2Data) { + const netCrs = new NetCrs(this.numPoints); this.g2Data = await netCrs.downloadG2Data(); await set('g2Data', this.g2Data); } else { @@ -40,12 +42,19 @@ export class CachedNetCrs { } /** - * G1 points data for prover key (compressed, 32 bytes/point). + * G1 points data for prover key (compressed or uncompressed). */ getG1Data(): Uint8Array { return this.g1Data; } + /** + * Cache uncompressed G1 data in IndexedDB after WASM decompression. + */ + async cacheUncompressed(data: Uint8Array): Promise { + await set('g1Data', data); + } + /** * G2 points data for verification key. * @returns The points data. diff --git a/barretenberg/ts/src/crs/node/index.ts b/barretenberg/ts/src/crs/node/index.ts index 75f911c54e2c..e8400661d441 100644 --- a/barretenberg/ts/src/crs/node/index.ts +++ b/barretenberg/ts/src/crs/node/index.ts @@ -26,23 +26,36 @@ export class Crs { return crs; } + private hasUncompressed = false; + async init(): Promise { mkdirSync(this.path, { recursive: true }); - const compressedFileSize = await stat(this.path + '/bn254_g1_compressed.dat') - .then(stats => stats.size) - .catch(() => 0); const g2FileSize = await stat(this.path + '/bn254_g2.dat') .then(stats => stats.size) .catch(() => 0); - const hasCompressed = compressedFileSize >= this.numPoints * 32 && compressedFileSize % 32 == 0; + // Prefer cached uncompressed (64 bytes/point, no decompression needed) + const uncompressedFileSize = await stat(this.path + '/bn254_g1.dat') + .then(stats => stats.size) + .catch(() => 0); + if (uncompressedFileSize >= this.numPoints * 64 && uncompressedFileSize % 64 == 0 && g2FileSize == 128) { + this.logger(`Using cached uncompressed CRS of size ${uncompressedFileSize / 64}`); + this.hasUncompressed = true; + return; + } - if (hasCompressed && g2FileSize == 128) { - this.logger(`Using cached compressed CRS of size ${compressedFileSize / 32}`); + // Fall back to compressed on disk + const compressedFileSize = await stat(this.path + '/bn254_g1_compressed.dat') + .then(stats => stats.size) + .catch(() => 0); + if (compressedFileSize >= this.numPoints * 32 && compressedFileSize % 32 == 0 && g2FileSize == 128) { + this.logger(`Using cached compressed CRS of size ${compressedFileSize / 32} (will decompress once)`); + this.hasUncompressed = false; return; } + // Download compressed from CDN this.logger(`Downloading CRS of size ${this.numPoints} into ${this.path}`); const crs = new NetCrs(this.numPoints); const g1Stream = await crs.streamG1Data(); @@ -52,14 +65,23 @@ export class Crs { finished(Readable.fromWeb(g1Stream as any).pipe(createWriteStream(this.path + '/bn254_g1_compressed.dat'))), finished(Readable.fromWeb(g2Stream as any).pipe(createWriteStream(this.path + '/bn254_g2.dat'))), ]); + this.hasUncompressed = false; } /** - * G1 points data for prover key (compressed, 32 bytes/point). - * Decompression happens in C++ via SrsInitSrs. + * G1 points data for prover key. Returns uncompressed (64 bytes/point) if cached, + * otherwise compressed (32 bytes/point) for WASM to decompress. */ getG1Data(): Uint8Array { const numPoints = Math.max(this.numPoints, 1); + if (this.hasUncompressed) { + const length = numPoints * 64; + const fd = openSync(this.path + '/bn254_g1.dat', 'r'); + const data = new Uint8Array(length); + readSync(fd, data, 0, length, 0); + closeSync(fd); + return data; + } const compressedLength = numPoints * 32; const fd = openSync(this.path + '/bn254_g1_compressed.dat', 'r'); const compressed = new Uint8Array(compressedLength); @@ -68,6 +90,14 @@ export class Crs { return compressed; } + /** + * Cache uncompressed G1 data to disk after WASM decompression. + */ + async cacheUncompressed(data: Uint8Array): Promise { + writeFileSync(this.path + '/bn254_g1.dat', data); + this.hasUncompressed = true; + } + /** * G2 points data for verification key. * @returns The points data. diff --git a/barretenberg/ts/src/index.ts b/barretenberg/ts/src/index.ts index 909f661306a3..8b1b40242fcf 100644 --- a/barretenberg/ts/src/index.ts +++ b/barretenberg/ts/src/index.ts @@ -7,8 +7,10 @@ export { UltraHonkVerifierBackend, UltraHonkBackend, AztecClientBackend, + flattenChonkProofFields, fieldToString, fieldsToStrings, + type AztecClientProveResult, type UltraHonkBackendOptions, type VerifierTarget, } from './barretenberg/index.js'; diff --git a/cspell.json b/cspell.json index 0c64bcaf57a8..9766afc1ea5a 100644 --- a/cspell.json +++ b/cspell.json @@ -6,6 +6,7 @@ "acvm", "addmod", "addrs", + "allow_phase_change", "alphanet", "anytype", "archiver", @@ -23,6 +24,7 @@ "autorun", "awslogs", "awsvpc", + "aztecnr", "aztecprotocol", "barretenberg", "batchable", @@ -233,7 +235,6 @@ "noinitcheck", "noirc", "noirup", - "allow_phase_change", "nophasecheck", "Nullifiable", "offchain", diff --git a/docs/.gitignore b/docs/.gitignore index 7c110daba05a..782638bd6916 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -33,4 +33,5 @@ test-results !CLAUDE.md target +foundry.lock static/aztec-nr-api/next diff --git a/docs/Nargo.toml b/docs/Nargo.toml index 7afcd72e7d1c..04fd9cd4b78d 100644 --- a/docs/Nargo.toml +++ b/docs/Nargo.toml @@ -6,5 +6,6 @@ members = [ "examples/contracts/nft", "examples/contracts/nft_bridge", "examples/contracts/recursive_verification_contract", + "examples/contracts/aave_bridge", "examples/contracts/example_uniswap" ] diff --git a/docs/bootstrap.sh b/docs/bootstrap.sh index 18f8d0975d0a..3400d0ce9ef8 100755 --- a/docs/bootstrap.sh +++ b/docs/bootstrap.sh @@ -44,6 +44,9 @@ function test_cmds { local test_hash=$hash echo "$test_hash cd docs && yarn spellcheck" + + # Delegate to examples for their test commands + (cd examples && ./bootstrap.sh test_cmds) } function test { diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_pay_fees.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_pay_fees.md index dde2bed29109..8fb1867fe2b9 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_pay_fees.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_pay_fees.md @@ -34,6 +34,10 @@ Mana is Aztec's unit of computational effort (like gas on Ethereum), and Fee Jui ## 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 estimate the mana it will consume by simulating with `estimateGas: true`: ```typescript @@ -300,6 +304,10 @@ const receipt = await contract.methods.myFunction().send({ ### Use automatic gas estimation +:::note +When using `EmbeddedWallet`, gas estimation happens automatically on every `send()` — you don't need to pass `estimateGas`. This option is useful for custom wallet implementations or when you want to estimate gas during a `simulate()` call. +::: + ```typescript // contract, aliceAddress, and paymentMethod are from the examples above const receipt = await contract.methods.myFunction().send({ diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_send_transaction.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_send_transaction.md index 44065fc3661b..0aa7bebf9edf 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_send_transaction.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_send_transaction.md @@ -42,6 +42,23 @@ console.log(`Transaction fee: ${receipt.transactionFee}`); 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. + +This means a simple `.send()` is all most apps need. You can adjust the gas padding if desired: + +```typescript +wallet.setEstimatedGasPadding(0.2); // 20% padding instead of the default 10% +``` + +:::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: diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_use_authwit.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_use_authwit.md index beea28f3d4ce..3103c25e60f4 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_use_authwit.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/how_to_use_authwit.md @@ -7,6 +7,14 @@ description: Step-by-step guide to implementing authentication witnesses in Azte 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. @@ -30,6 +38,10 @@ The authwit system supports different intent types depending on your use case: ## Create private authwits +:::note +If you're using `EmbeddedWallet`, this section is handled for you automatically — see the tip above. The manual approach below is for custom wallet implementations or advanced use cases. +::: + 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): diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/typescript_api_reference.mdx b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/typescript_api_reference.mdx index f3e07b31f59c..609cbade5ad4 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/typescript_api_reference.mdx +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-js/typescript_api_reference.mdx @@ -14,6 +14,7 @@ export const useApiVersion = () => { if (versionName === "current") return "next"; if (versionName.includes("nightly")) return "nightly"; if (versionName.includes("devnet")) return "devnet"; + if (versionName.includes("rc") || versionName.includes("testnet")) return "testnet"; return versionName; }; diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/api.mdx b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/api.mdx index a3bd8005a1f8..3313a3a39e7b 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/api.mdx +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/api.mdx @@ -16,6 +16,7 @@ export const useApiVersion = () => { if (versionName === "current") return "next"; if (versionName.includes("nightly")) return "nightly"; if (versionName.includes("devnet")) return "devnet"; + if (versionName.includes("rc") || versionName.includes("testnet")) return "testnet"; return versionName; }; diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/foundational-topics/wallets.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/foundational-topics/wallets.md index d230ce874344..6f6fdb549163 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/foundational-topics/wallets.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/foundational-topics/wallets.md @@ -36,6 +36,8 @@ Private functions use a UTXO model, so their execution trace is determined entir 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. + 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 @@ -44,6 +46,8 @@ Account contracts in Aztec expose an interface for other contracts to validate [ 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. +Modern 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. diff --git a/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md b/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md index e7db37a1e477..7209f9b2d98f 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md +++ b/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md @@ -38,6 +38,10 @@ Mana is Aztec's unit of computational effort (like gas on Ethereum), and Fee Jui ## 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 estimate the mana it will consume by simulating with `estimateGas: true`: ```typescript @@ -267,6 +271,10 @@ const receipt = await contract.methods.myFunction().send({ ### Use automatic gas estimation +:::note +When using `EmbeddedWallet`, gas estimation happens automatically on every `send()` — you don't need to pass `estimateGas`. This option is useful for custom wallet implementations or when you want to estimate gas during a `simulate()` call. +::: + ```typescript // contract, aliceAddress, and paymentMethod are from the examples above const receipt = await contract.methods.myFunction().send({ diff --git a/docs/docs-developers/docs/aztec-js/how_to_send_transaction.md b/docs/docs-developers/docs/aztec-js/how_to_send_transaction.md index cca99f9d1541..3197b74c4237 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_send_transaction.md +++ b/docs/docs-developers/docs/aztec-js/how_to_send_transaction.md @@ -44,6 +44,23 @@ console.log(`Transaction fee: ${receipt.transactionFee}`); 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. + +This means a simple `.send()` is all most apps need. You can adjust the gas padding if desired: + +```typescript +wallet.setEstimatedGasPadding(0.2); // 20% padding instead of the default 10% +``` + +:::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: diff --git a/docs/docs-developers/docs/aztec-js/how_to_use_authwit.md b/docs/docs-developers/docs/aztec-js/how_to_use_authwit.md index 757cd6d9f8af..1b38f508a8fe 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_use_authwit.md +++ b/docs/docs-developers/docs/aztec-js/how_to_use_authwit.md @@ -7,6 +7,14 @@ description: Step-by-step guide to implementing authentication witnesses in Azte 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. @@ -32,6 +40,10 @@ The authwit system supports different intent types depending on your use case: ## 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): diff --git a/docs/docs-developers/docs/aztec-js/typescript_api_reference.mdx b/docs/docs-developers/docs/aztec-js/typescript_api_reference.mdx index c78efd42834d..4987f0f70085 100644 --- a/docs/docs-developers/docs/aztec-js/typescript_api_reference.mdx +++ b/docs/docs-developers/docs/aztec-js/typescript_api_reference.mdx @@ -14,6 +14,7 @@ export const useApiVersion = () => { if (versionName === "current") return "next"; if (versionName.includes("nightly")) return "nightly"; if (versionName.includes("devnet")) return "devnet"; + if (versionName.includes("rc") || versionName.includes("testnet")) return "testnet"; return versionName; }; diff --git a/docs/docs-developers/docs/aztec-nr/api.mdx b/docs/docs-developers/docs/aztec-nr/api.mdx index a3bd8005a1f8..3313a3a39e7b 100644 --- a/docs/docs-developers/docs/aztec-nr/api.mdx +++ b/docs/docs-developers/docs/aztec-nr/api.mdx @@ -16,6 +16,7 @@ export const useApiVersion = () => { if (versionName === "current") return "next"; if (versionName.includes("nightly")) return "nightly"; if (versionName.includes("devnet")) return "devnet"; + if (versionName.includes("rc") || versionName.includes("testnet")) return "testnet"; return versionName; }; diff --git a/docs/docs-developers/docs/foundational-topics/wallets.md b/docs/docs-developers/docs/foundational-topics/wallets.md index ed63508f827a..4c0bc46c4457 100644 --- a/docs/docs-developers/docs/foundational-topics/wallets.md +++ b/docs/docs-developers/docs/foundational-topics/wallets.md @@ -35,6 +35,8 @@ Private functions use a UTXO model, so their execution trace is determined entir 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. + 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 @@ -43,6 +45,8 @@ Account contracts in Aztec expose an interface for other contracts to validate [ 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. diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index b41bea27caa9..74eb36ee0f59 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,116 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [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 = CapsuleArray::at(contract_address, slot); ++ let array: CapsuleArray = 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, // msg_content + MessageContext, // message_context ++ AztecAddress, // scope + ); +``` + +**Impact**: Contracts that implement a custom message handler must update the function signature. +### [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, ++ }); +``` + ### [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`. @@ -86,6 +196,7 @@ The `scope` field in `ExecuteUtilityOptions` has been renamed to `scopes` and ch ``` **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`). @@ -97,7 +208,7 @@ Most contracts are not affected, as the macro-generated `sync_state` and `proces **Migration:** ```diff - attempt_note_discovery( + attempt_note_discovery( contract_address, tx_hash, unique_note_hashes_in_tx, diff --git a/docs/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md b/docs/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md new file mode 100644 index 000000000000..7ae089ed6e70 --- /dev/null +++ b/docs/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md @@ -0,0 +1,518 @@ +--- +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 2 L2 blocks (required for L1→L2 message availability) + 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). + +You may need to update the `@aztec/l1-contracts` tag in the template's `package.json` to match your Aztec version, e.g.: + +```json +"@aztec/l1-contracts": "git+https://github.com/AztecProtocol/l1-contracts.git#v5.0.0-nightly.20260311" +``` + +::: + +```bash +git clone https://github.com/critesjosh/hardhat-aztec-example +cd hardhat-aztec-example +``` + +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/ + contract/src/main.nr + contract/src/config.nr + contract/Nargo.toml + aave_bridge_test/ + src/Nargo.toml + src/lib.nr + scripts/ + index.ts # Integration script + artifacts/ # Generated by aztec codegen +``` + +Add the Aztec dependencies: + +```bash +yarn add @aztec/aztec.js@#include_version_without_prefix @aztec/accounts@#include_version_without_prefix @aztec/wallets@#include_version_without_prefix @aztec/stdlib@#include_version_without_prefix @aztec/foundation@#include_version_without_prefix @aztec/ethereum@#include_version_without_prefix @aztec/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 a `contract` crate and a `test` crate. Replace the generated test file at `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::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/AaveBridge").with_public_initializer(deployer, initializer); +} +``` + +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/AztecProtocol/aztec-packages", tag = "#include_aztec_version", directory = "noir-projects/aztec-nr/aztec" } +token_portal_content_hash_lib = { git="https://github.com/AztecProtocol/aztec-packages", tag = "#include_aztec_version", directory = "noir-projects/noir-contracts/contracts/libs/token_portal_content_hash_lib" } +token = { git="https://github.com/AztecProtocol/aztec-packages", 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`: + + + +```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/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 `anvil --fork-url `. + +::: + +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 {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`: + + + +```solidity +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IRegistry} from "@aztec/l1-contracts/src/governance/interfaces/IRegistry.sol"; +import {IInbox} from "@aztec/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol"; +import {IOutbox} from "@aztec/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol"; +import {IRollup} from "@aztec/l1-contracts/src/core/interfaces/IRollup.sol"; +import {DataStructures} from "@aztec/l1-contracts/src/core/libraries/DataStructures.sol"; +import {Hash} from "@aztec/l1-contracts/src/core/libraries/crypto/Hash.sol"; +import {Epoch} from "@aztec/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/accounts/testing"; +import { AztecAddress, EthAddress } from "@aztec/aztec.js/addresses"; +import { SetPublicAuthwitContractInteraction } from "@aztec/aztec.js/authorization"; +import { Fr } from "@aztec/aztec.js/fields"; +import { createAztecNodeClient, waitForNode } from "@aztec/aztec.js/node"; +import { createExtendedL1Client } from "@aztec/ethereum/client"; +import { deployL1Contract } from "@aztec/ethereum/deploy-l1-contract"; +import { sha256ToField } from "@aztec/foundation/crypto/sha256"; +import { + computeL2ToL1MessageHash, + computeSecretHash, +} from "@aztec/stdlib/hash"; +import { computeL2ToL1MembershipWitness } from "@aztec/stdlib/messaging"; +import { EmbeddedWallet } from "@aztec/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/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 + +#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. L1-to-L2 messages require 2 L2 blocks before they can be consumed on L2. 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 + +L1-to-L2 messages need 2 L2 blocks after the L1 transaction before they become consumable. 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 `anvil --fork-url` 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/docs-words.txt b/docs/docs-words.txt index b09581f54668..25be85cef638 100644 --- a/docs/docs-words.txt +++ b/docs/docs-words.txt @@ -9,6 +9,7 @@ additonal addressnote airgapped analysed +Aave Anoncast arithmetisation asymptotics @@ -403,6 +404,9 @@ critesjosh mcpServers NethermindEth Windsurf +remappings +atoken +wagmi Uniswap uniswap WETH diff --git a/docs/examples/.rebuild_patterns b/docs/examples/.rebuild_patterns new file mode 100644 index 000000000000..a4de2f4e4bcc --- /dev/null +++ b/docs/examples/.rebuild_patterns @@ -0,0 +1,3 @@ +^docs/examples/ +^yarn-project/ +^noir-projects/ diff --git a/docs/examples/bootstrap.sh b/docs/examples/bootstrap.sh index efe8d2f6e368..8db8936c1cce 100755 --- a/docs/examples/bootstrap.sh +++ b/docs/examples/bootstrap.sh @@ -11,6 +11,16 @@ export STRIP_AZTEC_NR_PREFIX=${STRIP_AZTEC_NR_PREFIX:-"$REPO_ROOT/noir-projects/ export BB_HASH=${BB_HASH:-$("$REPO_ROOT/barretenberg/cpp/bootstrap.sh" hash)} export NOIR_HASH=${NOIR_HASH:-$("$REPO_ROOT/noir/bootstrap.sh" hash)} +# 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 + +hash=$(hash_str \ + $BB_HASH \ + $NOIR_HASH \ + $(cache_content_hash .rebuild_patterns)) + function compile-circuits { echo_header "Compiling vanilla Noir circuits" local CIRCUITS_DIR="$REPO_ROOT/docs/examples/circuits" @@ -117,6 +127,15 @@ function execute-examples { run_compose_test "docs_examples" "docs-examples" "$COMPOSE_DIR" } +function test_cmds { + echo "$hash:ONLY_TERM_PARENT=1 docs/examples/bootstrap.sh execute" +} + +function test { + echo_header "docs examples test" + test_cmds | filter_test_cmds | parallelize +} + ############################################################################## # CI failure handling - send Slack notifications instead of blocking the build ############################################################################## @@ -244,7 +263,6 @@ case "$cmd" in run_step "Compile (Noir contracts)" compile run_step "Compile (Solidity)" compile-solidity run_step "TypeScript validation" validate-ts - run_step "Execute examples" execute-examples if [[ ${#FAILED_STEPS[@]} -gt 0 ]]; then send_failure_slack_message @@ -277,6 +295,9 @@ case "$cmd" in fi fi ;; + "hash") + echo "$hash" + ;; compile-circuits) compile-circuits ;; diff --git a/docs/examples/contracts/aave_bridge/Nargo.toml b/docs/examples/contracts/aave_bridge/Nargo.toml new file mode 100644 index 000000000000..5e2d37824460 --- /dev/null +++ b/docs/examples/contracts/aave_bridge/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "aave_bridge" +type = "contract" + +[dependencies] +aztec = { path = "../../../../noir-projects/aztec-nr/aztec" } +token_portal_content_hash_lib = { path = "../../../../noir-projects/noir-contracts/contracts/libs/token_portal_content_hash_lib" } +token = { path = "../../../../noir-projects/noir-contracts/contracts/app/token_contract" } diff --git a/docs/examples/contracts/aave_bridge/src/config.nr b/docs/examples/contracts/aave_bridge/src/config.nr new file mode 100644 index 000000000000..7ea7e5724d58 --- /dev/null +++ b/docs/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/examples/contracts/aave_bridge/src/main.nr b/docs/examples/contracts/aave_bridge/src/main.nr new file mode 100644 index 000000000000..4e9890468d1b --- /dev/null +++ b/docs/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 { + config: PublicImmutable, + } + + #[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/examples/solidity/aave_bridge/AavePortal.sol b/docs/examples/solidity/aave_bridge/AavePortal.sol new file mode 100644 index 000000000000..8a97256b4547 --- /dev/null +++ b/docs/examples/solidity/aave_bridge/AavePortal.sol @@ -0,0 +1,135 @@ +// 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 _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, _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.approve(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.approve(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.approve(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/examples/solidity/aave_bridge/MockAToken.sol b/docs/examples/solidity/aave_bridge/MockAToken.sol new file mode 100644 index 000000000000..9de706f82a3a --- /dev/null +++ b/docs/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/examples/solidity/aave_bridge/MockAavePool.sol b/docs/examples/solidity/aave_bridge/MockAavePool.sol new file mode 100644 index 000000000000..abb0f5b0ff62 --- /dev/null +++ b/docs/examples/solidity/aave_bridge/MockAavePool.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {IERC20} from "@oz/token/ERC20/IERC20.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 { + 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).transferFrom(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 + underlyingToken.transfer(to, totalReturn); + return totalReturn; + } +} +// docs:end:mock_aave_pool diff --git a/docs/examples/solidity/aave_bridge/MockERC20.sol b/docs/examples/solidity/aave_bridge/MockERC20.sol new file mode 100644 index 000000000000..062b0c5905c9 --- /dev/null +++ b/docs/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/examples/ts/aave_bridge/config.yaml b/docs/examples/ts/aave_bridge/config.yaml new file mode 100644 index 000000000000..60f4db13cfd6 --- /dev/null +++ b/docs/examples/ts/aave_bridge/config.yaml @@ -0,0 +1,12 @@ +contracts: + - aave_bridge-AaveBridge + +dependencies: + - "@aztec/aztec.js" + - "@aztec/accounts" + - "@aztec/wallets" + - "@aztec/stdlib" + - "@aztec/foundation" + - "@aztec/ethereum" + - "@aztec/noir-contracts.js" + - "npm:@aztec/viem@2.38.2" diff --git a/docs/examples/ts/aave_bridge/index.ts b/docs/examples/ts/aave_bridge/index.ts new file mode 100644 index 000000000000..760d77cf58dc --- /dev/null +++ b/docs/examples/ts/aave_bridge/index.ts @@ -0,0 +1,401 @@ +import { getInitialTestAccountsData } from "@aztec/accounts/testing"; +import { AztecAddress, EthAddress } from "@aztec/aztec.js/addresses"; +import { SetPublicAuthwitContractInteraction } from "@aztec/aztec.js/authorization"; +import { Fr } from "@aztec/aztec.js/fields"; +import { createAztecNodeClient, waitForNode } from "@aztec/aztec.js/node"; +import { createExtendedL1Client } from "@aztec/ethereum/client"; +import { deployL1Contract } from "@aztec/ethereum/deploy-l1-contract"; +import { sha256ToField } from "@aztec/foundation/crypto/sha256"; +import { + computeL2ToL1MessageHash, + computeSecretHash, +} from "@aztec/stdlib/hash"; +import { computeL2ToL1MembershipWitness } from "@aztec/stdlib/messaging"; +import { EmbeddedWallet } from "@aztec/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/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.createSchnorrAccount( + 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, + contractAddressSalt: Fr.random(), + }); + await AaveBridgeContract.deploy( + aztecWallet, + accountAddress, + EthAddress.ZERO, + ).send({ + from: accountAddress, + contractAddressSalt: Fr.random(), + }); +} +// 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.getProvenBlockNumber(); +while (provenBlockNumber < exitBlockNumber) { + console.log( + ` Waiting... (proven: ${provenBlockNumber}, needed: ${exitBlockNumber})`, + ); + await new Promise((resolve) => setTimeout(resolve, 10000)); + provenBlockNumber = await node.getProvenBlockNumber(); +} +console.log("Block proven!\n"); + +// Compute the membership witness using the message hash and the L2 tx hash +const witness = await computeL2ToL1MembershipWitness(node, msgLeaf, exitReceipt.txHash); +const epoch = witness!.epochNumber; + +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(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: "checkpointNumber", type: "uint256", indexed: true }, + { name: "index", type: "uint256", indexed: false }, + { name: "hash", type: "bytes32", indexed: true }, + { name: "rollingHash", type: "bytes16", indexed: false }, + ], + }, +] 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.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/examples/ts/aave_bridge/yarn.lock b/docs/examples/ts/aave_bridge/yarn.lock new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/examples/ts/aztecjs_runner/run.sh b/docs/examples/ts/aztecjs_runner/run.sh index 9a69652217a1..cb06cff29ac7 100755 --- a/docs/examples/ts/aztecjs_runner/run.sh +++ b/docs/examples/ts/aztecjs_runner/run.sh @@ -10,7 +10,7 @@ # ./run.sh connection # Run specific example # ./run.sh getting_started advanced # Run multiple examples # -# Available examples: connection, getting_started, advanced, authwit, testing, swap, recursive_verification +# Available examples: connection, getting_started, advanced, authwit, testing, swap, aave_bridge, recursive_verification set -euo pipefail @@ -176,6 +176,8 @@ cleanup_project() { # 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=() @@ -187,6 +189,7 @@ else 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 diff --git a/docs/examples/ts/bootstrap.sh b/docs/examples/ts/bootstrap.sh index 606d2e5f3767..302614922013 100755 --- a/docs/examples/ts/bootstrap.sh +++ b/docs/examples/ts/bootstrap.sh @@ -11,6 +11,10 @@ export BUILDER_CLI="$REPO_ROOT/yarn-project/builder/dest/bin/cli.js" # Set parallel flags for concurrent validation export PARALLEL_FLAGS="-j${PARALLELISM:-4} --halt now,fail=1" +# Ensure all yarn.lock files are empty on exit. The per-project cleanup trap +# handles the normal case, but parallel's --halt can kill jobs before their trap runs. +trap 'for lf in */yarn.lock; do [ -f "$lf" ] && > "$lf"; done' EXIT + # Validate config.yaml structure before processing validate_config() { local config_file=$1 diff --git a/docs/examples/ts/recursive_verification/index.ts b/docs/examples/ts/recursive_verification/index.ts index bd14e5dec607..d675db600dda 100644 --- a/docs/examples/ts/recursive_verification/index.ts +++ b/docs/examples/ts/recursive_verification/index.ts @@ -37,7 +37,7 @@ export const setupWallet = async (): Promise => { try { // Create wallet with embedded PXE // The wallet manages accounts and connects to the node - let wallet = await EmbeddedWallet.create(NODE_URL); + 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); diff --git a/docs/netlify.toml b/docs/netlify.toml index 50b670818783..9d3d41270b6e 100644 --- a/docs/netlify.toml +++ b/docs/netlify.toml @@ -800,3 +800,8 @@ # Aztec-nr: user-defined 'offchain_receive' is not allowed from = "/errors/7" to = "/aztec-nr-api/nightly/noir_aztec/messages/processing/offchain/fn.receive.html" + +[[redirects]] + # PXE: incompatible oracle version between contract and PXE + from = "/errors/8" + to = "/developers/docs/foundational-topics/pxe" diff --git a/noir-projects/aztec-nr/aztec/src/capsules/mod.nr b/noir-projects/aztec-nr/aztec/src/capsules/mod.nr index ec8af10da3b2..c9060b7efcc4 100644 --- a/noir-projects/aztec-nr/aztec/src/capsules/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/capsules/mod.nr @@ -10,20 +10,25 @@ pub struct CapsuleArray { /// after the base slot. For example, with base slot 5: the length is at slot 5, the first element (index 0) is at /// slot 6, the second element (index 1) is at slot 7, and so on. base_slot: Field, + /// Scope for capsule isolation. Capsule operations are scoped to the given address, allowing multiple independent + /// namespaces within the same contract. + scope: AztecAddress, } impl CapsuleArray { - /// Returns a CapsuleArray connected to a contract's capsules at a base slot. Array elements are stored in - /// contiguous slots following the base slot, so there should be sufficient space between array base slots to - /// accommodate elements. A reasonable strategy is to make the base slot a hash of a unique value. - pub unconstrained fn at(contract_address: AztecAddress, base_slot: Field) -> Self { - Self { contract_address, base_slot } + /// Returns a CapsuleArray scoped to a specific address. + /// + /// Array elements are stored in contiguous slots + /// following the base slot, so there should be sufficient space between array base slots to accommodate elements. + /// A reasonable strategy is to make the base slot a hash of a unique value. + pub unconstrained fn at(contract_address: AztecAddress, base_slot: Field, scope: AztecAddress) -> Self { + Self { contract_address, base_slot, scope } } /// Returns the number of elements stored in the array. pub unconstrained fn len(self) -> u32 { // An uninitialized array defaults to a length of 0. - capsules::load(self.contract_address, self.base_slot).unwrap_or(0) as u32 + capsules::load(self.contract_address, self.base_slot, self.scope).unwrap_or(0) as u32 } /// Stores a value at the end of the array. @@ -35,11 +40,21 @@ impl CapsuleArray { // The slot corresponding to the index `current_length` is the first slot immediately after the end of the // array, which is where we want to place the new value. - capsules::store(self.contract_address, self.slot_at(current_length), value); + capsules::store( + self.contract_address, + self.slot_at(current_length), + value, + self.scope, + ); // Then we simply update the length. let new_length = current_length + 1; - capsules::store(self.contract_address, self.base_slot, new_length); + capsules::store( + self.contract_address, + self.base_slot, + new_length, + self.scope, + ); } /// Retrieves the value stored in the array at `index`. Throws if the index is out of bounds. @@ -49,7 +64,7 @@ impl CapsuleArray { { assert(index < self.len(), "Attempted to read past the length of a CapsuleArray"); - capsules::load(self.contract_address, self.slot_at(index)).unwrap() + capsules::load(self.contract_address, self.slot_at(index), self.scope).unwrap() } /// Deletes the value stored in the array at `index`. Throws if the index is out of bounds. @@ -67,13 +82,23 @@ impl CapsuleArray { self.slot_at(index + 1), self.slot_at(index), current_length - index - 1, + self.scope, ); } // We can now delete the last element (which has either been copied to the slot immediately before it, or was // the element we meant to delete in the first place) and update the length. - capsules::delete(self.contract_address, self.slot_at(current_length - 1)); - capsules::store(self.contract_address, self.base_slot, current_length - 1); + capsules::delete( + self.contract_address, + self.slot_at(current_length - 1), + self.scope, + ); + capsules::store( + self.contract_address, + self.base_slot, + current_length - 1, + self.scope, + ); } /// Calls a function on each element of the array. @@ -124,10 +149,12 @@ impl CapsuleArray { } mod test { + use crate::protocol::address::AztecAddress; use crate::test::helpers::test_environment::TestEnvironment; use super::CapsuleArray; global SLOT: Field = 1230; + global SCOPE: AztecAddress = AztecAddress { inner: 0xface }; #[test] unconstrained fn empty_array() { @@ -135,7 +162,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array: CapsuleArray = CapsuleArray::at(contract_address, SLOT); + let array: CapsuleArray = CapsuleArray::at(contract_address, SLOT, SCOPE); assert_eq(array.len(), 0); }); } @@ -146,7 +173,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); let _: Field = array.get(0); }); } @@ -157,7 +184,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(5); assert_eq(array.len(), 1); @@ -171,7 +198,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(5); let _ = array.get(1); @@ -184,7 +211,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(5); array.remove(0); @@ -199,7 +226,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(7); array.push(8); @@ -224,7 +251,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(7); array.push(8); @@ -243,7 +270,7 @@ mod test { let env = TestEnvironment::new(); env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(4); array.push(5); @@ -266,7 +293,7 @@ mod test { let env = TestEnvironment::new(); env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(4); array.push(5); @@ -289,7 +316,7 @@ mod test { let env = TestEnvironment::new(); env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(4); array.push(5); @@ -306,7 +333,7 @@ mod test { let env = TestEnvironment::new(); env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(4); array.push(5); @@ -321,4 +348,28 @@ mod test { assert_eq(mock.times_called(), 0); }); } + + #[test] + unconstrained fn different_scopes_are_isolated() { + let env = TestEnvironment::new(); + env.private_context(|context| { + let contract_address = context.this_address(); + let scope_a = AztecAddress { inner: 0xaaa }; + let scope_b = AztecAddress { inner: 0xbbb }; + + let array_a = CapsuleArray::at(contract_address, SLOT, scope_a); + let array_b = CapsuleArray::at(contract_address, SLOT, scope_b); + + array_a.push(10); + array_a.push(20); + array_b.push(99); + + assert_eq(array_a.len(), 2); + assert_eq(array_a.get(0), 10); + assert_eq(array_a.get(1), 20); + + assert_eq(array_b.len(), 1); + assert_eq(array_b.get(0), 99); + }); + } } diff --git a/noir-projects/aztec-nr/aztec/src/context/private_context.nr b/noir-projects/aztec-nr/aztec/src/context/private_context.nr index fd7192f86ca2..edbe8ed5a3cc 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -11,8 +11,8 @@ use crate::{ logs::notify_created_contract_class_log, notes::notify_nullified_note, nullifiers::notify_created_nullifier, - public_call::validate_public_calldata, - tx_phase::{in_revertible_phase, notify_revertible_phase_start}, + public_call::assert_valid_public_call_data, + tx_phase::{is_execution_in_revertible_phase, notify_revertible_phase_start}, }, }; use crate::logging::aztecnr_trace_log_format; @@ -37,7 +37,7 @@ use crate::protocol::{ MAX_KEY_VALIDATION_REQUESTS_PER_CALL, MAX_L2_TO_L1_MSGS_PER_CALL, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, MAX_NOTE_HASHES_PER_CALL, MAX_NULLIFIER_READ_REQUESTS_PER_CALL, MAX_NULLIFIERS_PER_CALL, MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL, MAX_PRIVATE_LOGS_PER_CALL, MAX_TX_LIFETIME, - NULL_MSG_SENDER_CONTRACT_ADDRESS, PRIVATE_LOG_SIZE_IN_FIELDS, + NULL_MSG_SENDER_CONTRACT_ADDRESS, PRIVATE_LOG_CIPHERTEXT_LEN, }, hash::poseidon2_hash, messaging::l2_to_l1_message::L2ToL1Message, @@ -519,7 +519,7 @@ impl PrivateContext { let current_counter = self.side_effect_counter; // Safety: Kernel will validate that the claim is correct by validating the expected counters. - let is_revertible = unsafe { in_revertible_phase(current_counter) }; + let is_revertible = unsafe { is_execution_in_revertible_phase(current_counter) }; if is_revertible { if (self.expected_revertible_side_effect_counter == 0) @@ -775,6 +775,7 @@ impl PrivateContext { // Safety: Kernels verify that the key validation request is valid and below we verify that a request for // the correct public key has been received. let request = unsafe { get_key_validation_request(pk_m_hash, key_index) }; + assert(!request.pk_m.is_infinite, "Infinite public key points are not allowed"); assert_eq(request.pk_m.hash(), pk_m_hash, "Obtained invalid key validation request"); self.key_validation_requests_and_separators.push( @@ -877,22 +878,33 @@ impl PrivateContext { /// about _which_ function has been executed. A tx which leaks such information does not contribute to the privacy /// set of the network. /// - /// * Unlike `emit_raw_note_log`, this log is not tied to any specific note + /// * Unlike `emit_raw_note_log_unsafe`, this log is not tied to any specific note /// /// # Arguments + /// * `tag` - A tag placed at `fields[0]` of the emitted log. Used by recipients and nodes to identify and + /// filter for relevant logs without scanning all of them. /// * `log` - The log data that will be publicly broadcast (so make sure it's already been encrypted before you - /// call this function). Private logs are bounded in size (PRIVATE_LOG_SIZE_IN_FIELDS), to encourage all logs from - /// all smart contracts look identical. - /// * `length` - The actual length of the `log` (measured in number of Fields). Although the input log has a max - /// size of PRIVATE_LOG_SIZE_IN_FIELDS, the latter values of the array might all be 0's for small logs. This - /// `length` should reflect the trimmed length of the array. The protocol's kernel circuits can then append random - /// fields as "padding" after the `length`, so that the logs of this smart contract look indistinguishable from - /// (the same length as) the logs of all other applications. It's up to wallets how much padding to apply, so - /// ideally all wallets should agree on standards for this. - pub fn emit_private_log(&mut self, log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS], length: u32) { + /// call this function). Private logs are bounded in size (`PRIVATE_LOG_CIPHERTEXT_LEN`), to encourage all logs + /// from all smart contracts look identical. + /// * `length` - The actual length of `log` (measured in number of Fields). Although the input log has a max + /// size of `PRIVATE_LOG_CIPHERTEXT_LEN`, the latter values of the array might all be 0's for small logs. This + /// `length` should reflect the trimmed length of the array. The protocol's kernel circuits can then append + /// random fields as "padding" after the `length`, so that the logs of this smart contract look + /// indistinguishable from (the same length as) the logs of all other applications. It's up to wallets how much + /// padding to apply, so ideally all wallets should agree on standards for this. + /// + /// ## Safety + /// + /// The `tag` should be domain-separated (e.g. via [`crate::protocol::hash::compute_log_tag`]) to prevent + /// collisions between logs from different sources. Without domain separation, two unrelated log types that + /// happen to share a raw tag value become indistinguishable. Prefer the higher-level APIs + /// ([`crate::messages::message_delivery::MessageDelivery`] for messages, `self.emit(event)` for events) which + /// handle tagging automatically. + pub fn emit_private_log_unsafe(&mut self, tag: Field, log: [Field; PRIVATE_LOG_CIPHERTEXT_LEN], length: u32) { let counter = self.next_counter(); - let private_log = PrivateLogData { log: PrivateLog::new(log, length), note_hash_counter: 0 }.count(counter); - self.private_logs.push(private_log); + let full_log = [tag].concat(log); + self.private_logs.push(PrivateLogData { log: PrivateLog::new(full_log, length + 1), note_hash_counter: 0 } + .count(counter)); } // TODO: rename. @@ -902,20 +914,31 @@ impl PrivateContext { /// This linkage is important in case the note gets squashed (due to being read later in this same tx), since we /// can then squash the log as well. /// - /// See `emit_private_log` for more info about private log emission. + /// See `emit_private_log_unsafe` for more info about private log emission. /// /// # Arguments + /// * `tag` - A tag placed at `fields[0]`. See `emit_private_log_unsafe`. /// * `log` - The log data as an array of Field elements /// * `length` - The actual length of the `log` (measured in number of Fields). /// * `note_hash_counter` - The side-effect counter that was assigned to the new note_hash when it was pushed to /// this `PrivateContext`. /// /// Important: If your application logic requires the log to always be emitted regardless of note squashing, - /// consider using `emit_private_log` instead, or emitting additional events. + /// consider using `emit_private_log_unsafe` instead, or emitting additional events. /// - pub fn emit_raw_note_log(&mut self, log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS], length: u32, note_hash_counter: u32) { + /// ## Safety + /// + /// Same as [`PrivateContext::emit_private_log_unsafe`]: the `tag` should be domain-separated. + pub fn emit_raw_note_log_unsafe( + &mut self, + tag: Field, + log: [Field; PRIVATE_LOG_CIPHERTEXT_LEN], + length: u32, + note_hash_counter: u32, + ) { let counter = self.next_counter(); - let private_log = PrivateLogData { log: PrivateLog::new(log, length), note_hash_counter }; + let full_log = [tag].concat(log); + let private_log = PrivateLogData { log: PrivateLog::new(full_log, length + 1), note_hash_counter }; self.private_logs.push(private_log.count(counter)); } @@ -1260,7 +1283,7 @@ impl PrivateContext { let is_static_call = is_static_call | self.inputs.call_context.is_static_call; - validate_public_calldata(calldata_hash); + assert_valid_public_call_data(calldata_hash); let msg_sender = if hide_msg_sender { NULL_MSG_SENDER_CONTRACT_ADDRESS @@ -1331,7 +1354,7 @@ impl PrivateContext { ) { let is_static_call = is_static_call | self.inputs.call_context.is_static_call; - validate_public_calldata(calldata_hash); + assert_valid_public_call_data(calldata_hash); let msg_sender = if hide_msg_sender { NULL_MSG_SENDER_CONTRACT_ADDRESS diff --git a/noir-projects/aztec-nr/aztec/src/context/public_context.nr b/noir-projects/aztec-nr/aztec/src/context/public_context.nr index ad9e91c0ada4..8a70842b0bb1 100644 --- a/noir-projects/aztec-nr/aztec/src/context/public_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/public_context.nr @@ -11,6 +11,7 @@ use crate::protocol::{ address::{AztecAddress, EthAddress}, constants::{MAX_U32_VALUE, NULL_MSG_SENDER_CONTRACT_ADDRESS}, traits::{Empty, FromField, Packable, Serialize, ToField}, + utils::writer::Writer, }; /// # PublicContext @@ -96,14 +97,27 @@ impl PublicContext { /// Emits a _public_ log that will be visible onchain to everyone. /// /// # Arguments - /// * `log` - The data to log, must implement Serialize trait + /// * `tag` - A tag placed at `fields[0]` of the emitted log. Nodes index logs by this value, allowing + /// clients to efficiently query for matching logs without scanning all of them. + /// * `log` - The data to log, must implement Serialize trait. /// - pub fn emit_public_log(_self: Self, log: T) + /// ## Safety + /// + /// The `tag` should be domain-separated (e.g. via [`crate::protocol::hash::compute_log_tag`]) to prevent + /// collisions between logs from different sources. Without domain separation, two unrelated log types that + /// happen to share a raw tag value become indistinguishable. Prefer `self.emit(event)` for events, which + /// handles tagging automatically. + pub fn emit_public_log_unsafe(_self: Self, tag: Field, log: T) where T: Serialize, { + // We use a Writer to serialize the log directly after the tag, avoiding an extra O(n) copy that would + // result from serializing first and then prepending the tag. + let mut writer: Writer<1 + ::N> = Writer::new(); + writer.write(tag); + Serialize::stream_serialize(log, &mut writer); // Safety: AVM opcodes are constrained by the AVM itself - unsafe { avm::emit_public_log(Serialize::serialize(log).as_vector()) }; + unsafe { avm::emit_public_log(writer.finish().as_vector()) }; } /// Checks if a given note hash exists in the note hash tree at a particular leaf_index. diff --git a/noir-projects/aztec-nr/aztec/src/context/returns_hash.nr b/noir-projects/aztec-nr/aztec/src/context/returns_hash.nr index 7226e9c4b092..0e073c69e0f4 100644 --- a/noir-projects/aztec-nr/aztec/src/context/returns_hash.nr +++ b/noir-projects/aztec-nr/aztec/src/context/returns_hash.nr @@ -82,7 +82,7 @@ mod test { let hash = hash_args(serialized); - let _ = OracleMock::mock("aztec_prv_loadFromExecutionCache").returns(bad_serialized); + let _ = OracleMock::mock("aztec_prv_getHashPreimage").returns(bad_serialized); assert_eq(ReturnsHash::new(hash).get_preimage(), value); } } diff --git a/noir-projects/aztec-nr/aztec/src/event/event_emission.nr b/noir-projects/aztec-nr/aztec/src/event/event_emission.nr index 845c2f366f6c..38824169d3ac 100644 --- a/noir-projects/aztec-nr/aztec/src/event/event_emission.nr +++ b/noir-projects/aztec-nr/aztec/src/event/event_emission.nr @@ -3,7 +3,7 @@ use crate::{ event::{event_interface::{compute_private_event_commitment, EventInterface}, EventMessage}, oracle::random::random, }; -use crate::protocol::traits::{Serialize, ToField}; +use crate::protocol::{constants::DOM_SEP__EVENT_LOG_TAG, hash::compute_log_tag, traits::{Serialize, ToField}}; /// An event that was emitted in the current contract call. pub struct NewEvent { @@ -39,17 +39,11 @@ pub fn emit_event_in_public(context: PublicContext, event: Event) where Event: EventInterface + Serialize, { - let mut log_content = [0; ::N + 1]; - - let serialized_event = event.serialize(); - for i in 0..serialized_event.len() { - log_content[i] = serialized_event[i]; - } - - // We put the selector in the "last" place, to avoid reading or assigning to an expression in an index - // - // TODO(F-224): change this order. - log_content[serialized_event.len()] = Event::get_event_type_id().to_field(); - - context.emit_public_log(log_content); + // We prepend a domain-separated tag derived from the event type ID so that clients can filter for specific + // events without scanning all public logs. + let log_tag = compute_log_tag( + Event::get_event_type_id().to_field(), + DOM_SEP__EVENT_LOG_TAG, + ); + context.emit_public_log_unsafe(log_tag, event); } diff --git a/noir-projects/aztec-nr/aztec/src/keys/ecdh_shared_secret.nr b/noir-projects/aztec-nr/aztec/src/keys/ecdh_shared_secret.nr index 3a3d2fa87d5c..a1dbd44ed2c3 100644 --- a/noir-projects/aztec-nr/aztec/src/keys/ecdh_shared_secret.nr +++ b/noir-projects/aztec-nr/aztec/src/keys/ecdh_shared_secret.nr @@ -1,4 +1,11 @@ -use crate::protocol::{address::aztec_address::AztecAddress, point::Point, scalar::Scalar, traits::FromField}; +use crate::protocol::{ + address::aztec_address::AztecAddress, + constants::{DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, DOM_SEP__ECDH_FIELD_MASK, DOM_SEP__ECDH_SUBKEY}, + hash::poseidon2_hash_with_separator, + point::Point, + scalar::Scalar, + traits::{FromField, ToField}, +}; use std::{embedded_curve_ops::multi_scalar_mul, ops::Neg}; /// Computes a standard ECDH shared secret: secret * public_key = shared_secret. @@ -6,13 +13,39 @@ use std::{embedded_curve_ops::multi_scalar_mul, ops::Neg}; /// The input secret is known only to one party. The output shared secret can be derived given knowledge of /// `public_key`'s key-pair and the public ephemeral secret, using this same function (with reversed inputs). /// -/// E.g.: Epk = esk * G // ephemeral key-pair Pk = sk * G // recipient key-pair Shared secret S = esk * Pk = sk * Epk +/// E.g.: Epk = esk * G // ephemeral key-pair +/// Pk = sk * G // recipient key-pair +/// Shared secret S = esk * Pk = sk * Epk /// /// See also: https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman pub fn derive_ecdh_shared_secret(secret: Scalar, public_key: Point) -> Point { multi_scalar_mul([public_key], [secret]) } +/// Computes an app-siloed shared secret from a raw ECDH shared secret point and a contract address. +/// +/// `s_app = h(DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, S.x, S.y, contract_address)` +pub(crate) fn compute_app_siloed_shared_secret(shared_secret: Point, contract_address: AztecAddress) -> Field { + poseidon2_hash_with_separator( + [shared_secret.x, shared_secret.y, contract_address.to_field()], + DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, + ) +} + +/// Derives an indexed subkey from an app-siloed shared secret, used for AES key/IV derivation. +/// +/// `s_i = h(DOM_SEP__ECDH_SUBKEY + i, s_app)` +pub(crate) fn derive_shared_secret_subkey(s_app: Field, index: u32) -> Field { + poseidon2_hash_with_separator([s_app], DOM_SEP__ECDH_SUBKEY + index) +} + +/// Derives an indexed field mask from an app-siloed shared secret, used for masking ciphertext fields. +/// +/// `m_i = h(DOM_SEP__ECDH_FIELD_MASK + i, s_app)` +pub(crate) fn derive_shared_secret_field_mask(s_app: Field, index: u32) -> Field { + poseidon2_hash_with_separator([s_app], DOM_SEP__ECDH_FIELD_MASK + index) +} + #[test] unconstrained fn test_consistency_with_typescript() { let secret = Scalar { @@ -81,3 +114,19 @@ unconstrained fn test_shared_secret_computation_from_address_in_both_directions( assert_eq(shared_secret, shared_secret_alt); } + +#[test] +unconstrained fn test_app_siloed_shared_secret_differs_per_contract() { + let secret_a = Scalar { lo: 0x1234, hi: 0x2345 }; + let pk_b = std::embedded_curve_ops::fixed_base_scalar_mul(Scalar { lo: 0x3456, hi: 0x4567 }); + + let shared_secret = derive_ecdh_shared_secret(secret_a, pk_b); + + let contract_a = AztecAddress::from_field(0xAAAA); + let contract_b = AztecAddress::from_field(0xBBBB); + + let s_app_a = compute_app_siloed_shared_secret(shared_secret, contract_a); + let s_app_b = compute_app_siloed_shared_secret(shared_secret, contract_b); + + assert(s_app_a != s_app_b, "app-siloed secrets must differ for different contracts"); +} diff --git a/noir-projects/aztec-nr/aztec/src/keys/getters/mod.nr b/noir-projects/aztec-nr/aztec/src/keys/getters/mod.nr index 6aa7f5bb52dc..d52b43d389e4 100644 --- a/noir-projects/aztec-nr/aztec/src/keys/getters/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/keys/getters/mod.nr @@ -71,7 +71,7 @@ mod test { // partial address random_keys_and_partial_address[12] = 0x236703e2cb00a182e024e98e9f759231b556d25ff19f98896cebb69e9e678cc9; - let _ = OracleMock::mock("aztec_utl_tryGetPublicKeysAndPartialAddress").returns(Option::some( + let _ = OracleMock::mock("aztec_utl_getPublicKeysAndPartialAddress").returns(Option::some( random_keys_and_partial_address, )); let _ = get_public_keys(account); diff --git a/noir-projects/aztec-nr/aztec/src/logging.nr b/noir-projects/aztec-nr/aztec/src/logging.nr index f30b2423c137..34bf4d70bec3 100644 --- a/noir-projects/aztec-nr/aztec/src/logging.nr +++ b/noir-projects/aztec-nr/aztec/src/logging.nr @@ -1,5 +1,8 @@ // Not all log levels are currently used, but we provide the full set so that new call sites can use any level. Because // of that we tag all with `#[allow(dead_code)]` to prevent warnings. +// +// All wrappers resolve function paths at comptime via `resolve_fn` so that the emitted `Quoted` code works both inside +// aztec-nr (where `crate::` = aztec) and inside macro-generated contract code (where `crate::` = the contract). use std::meta::ctstring::AsCtString; @@ -12,43 +15,50 @@ comptime fn log_prefix(msg: str) -> CtString { #[allow(dead_code)] pub(crate) comptime fn aztecnr_fatal_log(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { crate::protocol::logging::fatal_log($msg) } + let f = resolve_fn(quote { crate::protocol::logging::fatal_log }); + quote { $f($msg) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_error_log(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { crate::protocol::logging::error_log($msg) } + let f = resolve_fn(quote { crate::protocol::logging::error_log }); + quote { $f($msg) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_warn_log(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { crate::protocol::logging::warn_log($msg) } + let f = resolve_fn(quote { crate::protocol::logging::warn_log }); + quote { $f($msg) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_info_log(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { crate::protocol::logging::info_log($msg) } + let f = resolve_fn(quote { crate::protocol::logging::info_log }); + quote { $f($msg) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_verbose_log(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { crate::protocol::logging::verbose_log($msg) } + let f = resolve_fn(quote { crate::protocol::logging::verbose_log }); + quote { $f($msg) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_debug_log(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { crate::protocol::logging::debug_log($msg) } + let f = resolve_fn(quote { crate::protocol::logging::debug_log }); + quote { $f($msg) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_trace_log(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { crate::protocol::logging::trace_log($msg) } + let f = resolve_fn(quote { crate::protocol::logging::trace_log }); + quote { $f($msg) } } // --- Format variants (return lambda for runtime args) --- @@ -56,41 +66,53 @@ pub(crate) comptime fn aztecnr_trace_log(msg: str) -> Quoted { #[allow(dead_code)] pub(crate) comptime fn aztecnr_fatal_log_format(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { (|args| crate::protocol::logging::fatal_log_format($msg, args)) } + let f = resolve_fn(quote { crate::protocol::logging::fatal_log_format }); + quote { (|args| $f($msg, args)) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_error_log_format(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { (|args| crate::protocol::logging::error_log_format($msg, args)) } + let f = resolve_fn(quote { crate::protocol::logging::error_log_format }); + quote { (|args| $f($msg, args)) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_warn_log_format(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { (|args| crate::protocol::logging::warn_log_format($msg, args)) } + let f = resolve_fn(quote { crate::protocol::logging::warn_log_format }); + quote { (|args| $f($msg, args)) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_info_log_format(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { (|args| crate::protocol::logging::info_log_format($msg, args)) } + let f = resolve_fn(quote { crate::protocol::logging::info_log_format }); + quote { (|args| $f($msg, args)) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_verbose_log_format(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { (|args| crate::protocol::logging::verbose_log_format($msg, args)) } + let f = resolve_fn(quote { crate::protocol::logging::verbose_log_format }); + quote { (|args| $f($msg, args)) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_debug_log_format(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { (|args| crate::protocol::logging::debug_log_format($msg, args)) } + let f = resolve_fn(quote { crate::protocol::logging::debug_log_format }); + quote { (|args| $f($msg, args)) } } #[allow(dead_code)] pub(crate) comptime fn aztecnr_trace_log_format(msg: str) -> Quoted { let msg = log_prefix(msg); - quote { (|args| crate::protocol::logging::trace_log_format($msg, args)) } + let f = resolve_fn(quote { crate::protocol::logging::trace_log_format }); + quote { (|args| $f($msg, args)) } +} + +// See module-level comment for why this is needed. +comptime fn resolve_fn(path: Quoted) -> TypedExpr { + path.as_expr().unwrap().resolve(Option::none()) } diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index 7b1d85286faf..6161c302e0a1 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -101,8 +101,8 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted { let fn_abi_exports = create_fn_abi_exports(m); // We generate `_compute_note_hash`, `_compute_note_nullifier` (and the deprecated - // `_compute_note_hash_and_nullifier` wrapper), `sync_state` and `process_message` functions only if they are not - // already implemented. If they are implemented we just insert empty quotes. + // `_compute_note_hash_and_nullifier` wrapper) and `sync_state` functions only if they are not already implemented. + // If they are implemented we just insert empty quotes. let contract_library_method_compute_note_hash_and_nullifier = if !m.functions().any(|f| { // Note that we don't test for `_compute_note_hash` or `_compute_note_nullifier` in order to make this simpler // - users must either implement all three or none. @@ -137,11 +137,6 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted { } let offchain_receive_fn_and_abi_export = generate_offchain_receive(); - let process_message_fn_and_abi_export = if !m.functions().any(|f| f.name() == quote { process_message }) { - generate_process_message(process_custom_message_option) - } else { - quote {} - }; let (has_public_init_nullifier_fn, emit_public_init_nullifier_fn_body) = generate_emit_public_init_nullifier(m); let public_dispatch = generate_public_dispatch(m, has_public_init_nullifier_fn); @@ -154,7 +149,6 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted { $contract_library_method_compute_note_hash_and_nullifier $public_dispatch $sync_state_fn_and_abi_export - $process_message_fn_and_abi_export $emit_public_init_nullifier_fn_body $offchain_receive_fn_and_abi_export } @@ -227,7 +221,9 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { /// Generates the `sync_state` utility function that performs message discovery. comptime fn generate_sync_state(process_custom_message_option: Quoted, offchain_inbox_sync_option: Quoted) -> Quoted { quote { - pub struct sync_state_parameters {} + pub struct sync_state_parameters { + pub scope: aztec::protocol::address::AztecAddress, + } #[abi(functions)] pub struct sync_state_abi { @@ -235,7 +231,7 @@ comptime fn generate_sync_state(process_custom_message_option: Quoted, offchain_ } #[aztec::macros::internals_functions_generation::abi_attributes::abi_utility] - unconstrained fn sync_state() { + unconstrained fn sync_state(scope: aztec::protocol::address::AztecAddress) { let address = aztec::context::UtilityContext::new().this_address(); aztec::messages::discovery::do_sync_state( address, @@ -243,47 +239,12 @@ comptime fn generate_sync_state(process_custom_message_option: Quoted, offchain_ _compute_note_nullifier, $process_custom_message_option, $offchain_inbox_sync_option, + scope, ); } } } -/// Generates the `process_message` utility function that processes a single message ciphertext. -comptime fn generate_process_message(process_custom_message_option: Quoted) -> Quoted { - quote { - pub struct process_message_parameters { - pub message_ciphertext: BoundedVec, - pub message_context: aztec::messages::processing::MessageContext, - } - - #[abi(functions)] - pub struct process_message_abi { - parameters: process_message_parameters, - } - - #[aztec::macros::internals_functions_generation::abi_attributes::abi_utility] - unconstrained fn process_message( - message_ciphertext: BoundedVec, - message_context: aztec::messages::processing::MessageContext, - ) { - let address = aztec::context::UtilityContext::new().this_address(); - - aztec::messages::discovery::process_message::process_message_ciphertext( - address, - _compute_note_hash, - _compute_note_nullifier, - $process_custom_message_option, - message_ciphertext, - message_context, - ); - - // At this point, the note is pending validation and storage in the database. We must call - // validate_and_store_enqueued_notes_and_events to complete that process. - aztec::messages::processing::validate_and_store_enqueued_notes_and_events(address); - } - } -} - /// Generates an `offchain_receive` utility function that lets callers add messages to the offchain message inbox. /// /// For more details, see `aztec::messages::processing::offchain::receive`. @@ -303,6 +264,8 @@ comptime fn generate_offchain_receive() -> Quoted { /// Receives offchain messages into this contract's offchain inbox for subsequent processing. /// + /// Each message is routed to the inbox scoped to its `recipient` field. + /// /// For more details, see `aztec::messages::processing::offchain::receive`. /// /// This function is automatically injected by the `#[aztec]` macro. diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr index 7f8887b04315..5362b65772a0 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr @@ -1,3 +1,4 @@ +use crate::logging; use crate::macros::{notes::NOTES, utils::get_trait_impl_method}; /// Generates two contract library methods called `_compute_note_hash` and `_compute_note_nullifier`, plus a @@ -64,9 +65,16 @@ comptime fn generate_contract_library_method_compute_note_hash() -> Quoted { } else { // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we - // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and + // know it we call the correct `unpack` method from the `Packable` trait to obtain the underlying note type, and // compute the note hash (non-siloed). + // We resolve the log format calls here so that the resulting Quoted values can be spliced into the quote + // block below. + let warn_length_mismatch = logging::aztecnr_warn_log_format( + "Packed note length mismatch for note type id {0}: expected {1} fields, got {2}. Skipping note.", + ); + let warn_unknown_note_type = logging::aztecnr_warn_log_format("Unknown note type id {0}. Skipping note."); + let mut if_note_type_id_match_statements_list = @[]; for i in 0..NOTES.len() { let typ = NOTES.get(i); @@ -103,10 +111,7 @@ comptime fn generate_contract_library_method_compute_note_hash() -> Quoted { let expected_len = <$typ as $crate::protocol::traits::Packable>::N; let actual_len = packed_note.len(); if actual_len != expected_len { - aztec::protocol::logging::warn_log_format( - "[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.", - [expected_len as Field, actual_len as Field, note_type_id], - ); + $warn_length_mismatch([note_type_id, expected_len as Field, actual_len as Field]); Option::none() } else { let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); @@ -137,10 +142,7 @@ comptime fn generate_contract_library_method_compute_note_hash() -> Quoted { ) -> Option { $if_note_type_id_match_statements else { - aztec::protocol::logging::warn_log_format( - "[aztec-nr] Unknown note type id {0}. Skipping note.", - [note_type_id], - ); + $warn_unknown_note_type([note_type_id]); Option::none() } } @@ -172,9 +174,16 @@ comptime fn generate_contract_library_method_compute_note_nullifier() -> Quoted } else { // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we - // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and + // know it we call the correct `unpack` method from the `Packable` trait to obtain the underlying note type, and // compute the inner nullifier (non-siloed). + // We resolve the log format calls here so that the resulting Quoted values can be spliced into the quote + // block below. + let warn_length_mismatch = logging::aztecnr_warn_log_format( + "Packed note length mismatch for note type id {0}: expected {1} fields, got {2}. Skipping note.", + ); + let warn_unknown_note_type = logging::aztecnr_warn_log_format("Unknown note type id {0}. Skipping note."); + let mut if_note_type_id_match_statements_list = @[]; for i in 0..NOTES.len() { let typ = NOTES.get(i); @@ -211,10 +220,7 @@ comptime fn generate_contract_library_method_compute_note_nullifier() -> Quoted let expected_len = <$typ as $crate::protocol::traits::Packable>::N; let actual_len = packed_note.len(); if actual_len != expected_len { - aztec::protocol::logging::warn_log_format( - "[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.", - [expected_len as Field, actual_len as Field, note_type_id], - ); + $warn_length_mismatch([note_type_id, expected_len as Field, actual_len as Field]); Option::none() } else { let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); @@ -249,10 +255,7 @@ comptime fn generate_contract_library_method_compute_note_nullifier() -> Quoted ) -> Option { $if_note_type_id_match_statements else { - aztec::protocol::logging::warn_log_format( - "[aztec-nr] Unknown note type id {0}. Skipping note.", - [note_type_id], - ); + $warn_unknown_note_type([note_type_id]); Option::none() } } diff --git a/noir-projects/aztec-nr/aztec/src/macros/events.nr b/noir-projects/aztec-nr/aztec/src/macros/events.nr index 62c71634d32c..a2ff2afabcef 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/events.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/events.nr @@ -71,7 +71,7 @@ pub comptime fn event(s: TypeDefinition) -> Quoted { let serialize_impl = derive_serialize_if_not_implemented(s); - s.add_attribute("abi(events)"); + s.add_abi("events"); quote { $event_interface_impl diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr index 2b1d6b32fa0e..3ef86dc96953 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr @@ -10,13 +10,16 @@ use crate::protocol::{ use std::meta::{ctstring::AsCtString, unquote}; use crate::{ - context::{PrivateContext, PublicContext}, + context::{PrivateContext, PublicContext, UtilityContext}, macros::{ internals_functions_generation::external_functions_registry::get_public_functions, utils::fn_has_noinitcheck, }, nullifier::utils::compute_nullifier_existence_request, - oracle::get_contract_instance::{ - get_contract_instance, get_contract_instance_deployer_avm, get_contract_instance_initialization_hash_avm, + oracle::{ + get_contract_instance::{ + get_contract_instance, get_contract_instance_deployer_avm, get_contract_instance_initialization_hash_avm, + }, + nullifiers::check_nullifier_exists, }, }; @@ -111,6 +114,17 @@ pub fn assert_is_initialized_private(context: &mut PrivateContext) { context.assert_nullifier_exists(nullifier_existence_request); } +/// Asserts that the contract has been initialized, from a utility function's perspective. +/// +/// Only checks the private initialization nullifier in the settled nullifier tree. Since both nullifiers are emitted in +/// the same transaction, the private nullifier's presence in settled state guarantees the public one is also settled. +pub unconstrained fn assert_is_initialized_utility(context: UtilityContext) { + let address = context.this_address(); + let instance = get_contract_instance(address); + let initialization_nullifier = compute_private_initialization_nullifier(address, instance.initialization_hash); + assert(check_nullifier_exists(initialization_nullifier), "Not initialized"); +} + /// Computes the private initialization nullifier for a contract. /// /// Including `init_hash` ensures that an observer who knows only the contract address cannot reconstruct this value diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr index 3965e62210fe..3d03ecefc31d 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr @@ -231,8 +231,6 @@ pub comptime fn only_self(f: FunctionDefinition) { f"The #[only_self] attribute can only be applied to #[external(\"private\")] or #[external(\"public\")] functions - {name} is neither", ); } - - f.add_attribute("noinitcheck"); } /// View functions cannot modify state in any way, including performing contract calls that would in turn modify state. diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr index f50bbc1249d7..55b7ec2ee515 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr @@ -1,6 +1,8 @@ -use crate::macros::utils::module_has_storage; +use crate::macros::utils::{fn_has_noinitcheck, module_has_initializer, module_has_storage}; pub(crate) comptime fn generate_utility_external(f: FunctionDefinition) -> Quoted { + let module_has_initializer = module_has_initializer(f.module()); + // Initialize Storage if module has storage let storage_init = if module_has_storage(f.module()) { quote { @@ -26,10 +28,22 @@ pub(crate) comptime fn generate_utility_external(f: FunctionDefinition) -> Quote }; }; + // Initialization checks are not included in contracts that don't have initializers. + let init_check = if module_has_initializer & !fn_has_noinitcheck(f) { + quote { + aztec::macros::functions::initialization_utils::assert_is_initialized_utility( + self.context, + ); + } + } else { + quote {} + }; + // A quote to be injected at the beginning of the function body. let to_prepend = quote { aztec::oracle::version::assert_compatible_oracle_version(); $contract_self_creation + $init_check }; let original_function_name = f.name(); diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/mod.nr index f6b686128403..58f0290c4e8e 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/mod.nr @@ -25,25 +25,10 @@ comptime fn make_functions_uncallable(functions: [FunctionDefinition // directly and a std::mem::zeroed() to make the compilation fail on the static_assert and not on a missing // return value. let error_message = f"{error_message_template}{name}. See https://docs.aztec.network/errors/6"; - let body = f"{{ std::static_assert(false, \"{error_message}\"); std::mem::zeroed() }}".quoted_contents(); - let body_expr = body.as_expr().expect(f"Body is not an expression: {body}"); - // Prefix all parameter names with "_" to suppress unused variable warnings - let params = function.parameters(); - let prefixed_params = params.map(|(param_name, param_type)| { - let prefixed_name = f"_{param_name}".quoted_contents(); - (prefixed_name, param_type) - }); - - function.set_body(body_expr); - function.set_parameters(prefixed_params); - // We need to add the `contract_library_method` attribute to the function to prevent this function from being - // compiled as an entrypoint function (function that's compiled as its own circuit). - function.add_attribute("contract_library_method"); - - // Contract functions need to have a public return type so we mark it as such to avoid undesired compilation - // errors. - function.set_return_public(true); + // Disabling the function also adds a `contract_library_method` attribute to the function to prevent this + // function from being compiled as an entrypoint function (function that's compiled as its own circuit). + function.disable(error_message); }); } diff --git a/noir-projects/aztec-nr/aztec/src/macros/utils.nr b/noir-projects/aztec-nr/aztec/src/macros/utils.nr index 40cdd20251c9..f7bdd9307d36 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/utils.nr @@ -30,7 +30,13 @@ pub(crate) comptime fn is_fn_initializer(f: FunctionDefinition) -> bool { } pub(crate) comptime fn fn_has_noinitcheck(f: FunctionDefinition) -> bool { - f.has_named_attribute("noinitcheck") + if f.has_named_attribute("noinitcheck") { + true + } else { + // #[only_self] functions automatically skip the initialization check as the check is assumed to be done by the + // calling external function or explicitly skipped. See only_self function docs for more details. + is_fn_only_self(f) + } } pub(crate) comptime fn fn_has_allow_phase_change(f: FunctionDefinition) -> bool { diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index eca4d338ed91..9cbfae253c62 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -103,7 +103,8 @@ pub type CustomMessageHandler = unconstrained fn[Env]( /* msg_type_id */ u64, /* msg_metadata */ u64, /* msg_content */ BoundedVec, -/* message_context */ MessageContext); +/* message_context */ MessageContext, +/* scope */ AztecAddress); /// Synchronizes the contract's private state with the network. /// @@ -119,12 +120,13 @@ pub unconstrained fn do_sync_state( compute_note_nullifier: ComputeNoteNullifier, process_custom_message: Option>, offchain_inbox_sync: Option>, + scope: AztecAddress, ) { aztecnr_debug_log!("Performing state synchronization"); // First we process all private logs, which can contain different kinds of messages e.g. private notes, partial // notes, private events, etc. - let logs = get_private_logs(contract_address); + let logs = get_private_logs(contract_address, scope); logs.for_each(|i, pending_tagged_log: PendingTaggedLog| { if pending_tagged_log.log.len() == 0 { aztecnr_warn_log_format!("Skipping empty log from tx {0}")([pending_tagged_log.context.tx_hash]); @@ -141,6 +143,7 @@ pub unconstrained fn do_sync_state( process_custom_message, message_ciphertext, pending_tagged_log.context, + scope, ); } @@ -152,7 +155,7 @@ pub unconstrained fn do_sync_state( }); if offchain_inbox_sync.is_some() { - let msgs: CapsuleArray = offchain_inbox_sync.unwrap()(contract_address); + let msgs: CapsuleArray = offchain_inbox_sync.unwrap()(contract_address, scope); msgs.for_each(|i, msg| { process_message_ciphertext( contract_address, @@ -161,6 +164,7 @@ pub unconstrained fn do_sync_state( process_custom_message, msg.message_ciphertext, msg.message_context, + scope, ); // The inbox sync returns _a copy_ of messages to process, so we clear them as we do so. This is a // volatile array with the to-process message, not the actual persistent storage of them. @@ -174,11 +178,12 @@ pub unconstrained fn do_sync_state( contract_address, compute_note_hash, compute_note_nullifier, + scope, ); // Finally we validate all notes and events that were found as part of the previous processes, resulting in them // being added to PXE's database and retrievable via oracles (get_notes) and our TS API (PXE::getPrivateEvents). - validate_and_store_enqueued_notes_and_events(contract_address); + validate_and_store_enqueued_notes_and_events(contract_address, scope); } mod test { @@ -195,6 +200,8 @@ mod test { }; use crate::protocol::address::AztecAddress; + global SCOPE: AztecAddress = AztecAddress { inner: 0xcafe }; + #[test] unconstrained fn do_sync_state_does_not_panic_on_empty_logs() { let env = TestEnvironment::new(); @@ -204,7 +211,7 @@ mod test { env.utility_context_at(contract_address, |_| { let base_slot = PENDING_TAGGED_LOG_ARRAY_BASE_SLOT; - let logs: CapsuleArray = CapsuleArray::at(contract_address, base_slot); + let logs: CapsuleArray = CapsuleArray::at(contract_address, base_slot, SCOPE); logs.push(PendingTaggedLog { log: BoundedVec::new(), context: std::mem::zeroed() }); assert_eq(logs.len(), 1); @@ -216,6 +223,7 @@ mod test { dummy_compute_note_nullifier, no_handler, no_inbox_sync, + SCOPE, ); assert_eq(logs.len(), 0); diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr index eab5007b66bb..e785e0072181 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr @@ -128,8 +128,6 @@ pub(crate) unconstrained fn attempt_note_nonce_discovery( } } - aztecnr_debug_log_format!("Found valid nonces for a total of {0} notes")([discovered_notes.len() as Field]); - *discovered_notes } diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr index 37d20096f871..4ef8255e6539 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr @@ -30,15 +30,14 @@ pub(crate) struct DeliveredPendingPartialNote { pub(crate) note_completion_log_tag: Field, pub(crate) note_type_id: Field, pub(crate) packed_private_note_content: BoundedVec, - pub(crate) recipient: AztecAddress, } pub(crate) unconstrained fn process_partial_note_private_msg( contract_address: AztecAddress, - recipient: AztecAddress, msg_metadata: u64, msg_content: BoundedVec, tx_hash: Field, + scope: AztecAddress, ) { let decoded = decode_partial_note_private_message(msg_metadata, msg_content); @@ -53,12 +52,12 @@ pub(crate) unconstrained fn process_partial_note_private_msg( note_completion_log_tag, note_type_id, packed_private_note_content, - recipient, }; CapsuleArray::at( contract_address, DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, + scope, ) .push(pending); } else { @@ -76,10 +75,12 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( contract_address: AztecAddress, compute_note_hash: ComputeNoteHash, compute_note_nullifier: ComputeNoteNullifier, + scope: AztecAddress, ) { let pending_partial_notes = CapsuleArray::at( contract_address, DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, + scope, ); aztecnr_debug_log_format!("{} pending partial notes")([pending_partial_notes.len() as Field]); @@ -87,7 +88,8 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( // Each of the pending partial notes might get completed by a log containing its public values. For performance // reasons, we fetch all of these logs concurrently and then process them one by one, minimizing the amount of time // waiting for the node roundtrip. - let maybe_completion_logs = get_pending_partial_notes_completion_logs(contract_address, pending_partial_notes); + let maybe_completion_logs = + get_pending_partial_notes_completion_logs(contract_address, pending_partial_notes, scope); // Each entry in the maybe completion logs array corresponds to the entry in the pending partial notes array at the // same index. This means we can use the same index as we iterate through the responses to get both the partial @@ -165,7 +167,7 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( discovered_note.note_hash, discovered_note.inner_nullifier, log.tx_hash, - pending_partial_note.recipient, + scope, ); }); diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr index 76a8f810fe66..5737768786ab 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr @@ -10,10 +10,10 @@ use crate::protocol::{address::AztecAddress, traits::ToField}; pub(crate) unconstrained fn process_private_event_msg( contract_address: AztecAddress, - recipient: AztecAddress, msg_metadata: u64, msg_content: BoundedVec, tx_hash: Field, + recipient: AztecAddress, ) { let decoded = decode_private_event_message(msg_metadata, msg_content); diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr index 1070ba4856bf..e56c798c7cca 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr @@ -1,22 +1,24 @@ -use crate::logging::{aztecnr_debug_log_format, aztecnr_warn_log_format}; -use crate::messages::{ - discovery::{ComputeNoteHash, ComputeNoteNullifier, nonce_discovery::attempt_note_nonce_discovery}, - encoding::MAX_MESSAGE_CONTENT_LEN, - logs::note::{decode_private_note_message, MAX_NOTE_PACKED_LEN}, - processing::enqueue_note_for_validation, +use crate::{ + logging::{aztecnr_debug_log_format, aztecnr_warn_log_format}, + messages::{ + discovery::{ComputeNoteHash, ComputeNoteNullifier, nonce_discovery::attempt_note_nonce_discovery}, + encoding::MAX_MESSAGE_CONTENT_LEN, + logs::note::{decode_private_note_message, MAX_NOTE_PACKED_LEN}, + processing::enqueue_note_for_validation, + }, + protocol::{address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX, traits::ToField}, }; -use crate::protocol::{address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX}; pub(crate) unconstrained fn process_private_note_msg( contract_address: AztecAddress, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, - recipient: AztecAddress, compute_note_hash: ComputeNoteHash, compute_note_nullifier: ComputeNoteNullifier, msg_metadata: u64, msg_content: BoundedVec, + recipient: AztecAddress, ) { let decoded = decode_private_note_message(msg_metadata, msg_content); @@ -28,7 +30,6 @@ pub(crate) unconstrained fn process_private_note_msg( tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx, - recipient, compute_note_hash, compute_note_nullifier, owner, @@ -36,6 +37,7 @@ pub(crate) unconstrained fn process_private_note_msg( randomness, note_type_id, packed_note, + recipient, ); } else { aztecnr_warn_log_format!( @@ -53,7 +55,6 @@ pub unconstrained fn attempt_note_discovery( tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, - recipient: AztecAddress, compute_note_hash: ComputeNoteHash, compute_note_nullifier: ComputeNoteNullifier, owner: AztecAddress, @@ -61,6 +62,7 @@ pub unconstrained fn attempt_note_discovery( randomness: Field, note_type_id: Field, packed_note: BoundedVec, + recipient: AztecAddress, ) { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, @@ -75,7 +77,22 @@ pub unconstrained fn attempt_note_discovery( packed_note, ); - aztecnr_debug_log_format!("Discovered {0} notes from a private message")([discovered_notes.len() as Field]); + if discovered_notes.len() == 0 { + // A private note message that results in no discovered notes means none of the computed note hashes matched + // any unique note hash in the transaction. This could indicate a malformed or malicious message (e.g. a sender + // providing bogus note content). + aztecnr_warn_log_format!( + "Discarding private note message from tx {0} for contract {1}: no matching note hash found in the tx", + )( + [tx_hash, contract_address.to_field()], + ); + } else { + aztecnr_debug_log_format!( + "Discovered {0} notes from a private message for contract {1}", + )( + [discovered_notes.len() as Field, contract_address.to_field()], + ); + } discovered_notes.for_each(|discovered_note| { enqueue_note_for_validation( diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr index 1becbd92cecd..026fae34ac15 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr @@ -33,8 +33,9 @@ pub unconstrained fn process_message_ciphertext( process_custom_message: Option>, message_ciphertext: BoundedVec, message_context: MessageContext, + recipient: AztecAddress, ) { - let message_plaintext_option = AES128::decrypt(message_ciphertext, message_context.recipient); + let message_plaintext_option = AES128::decrypt(message_ciphertext, recipient, contract_address); if message_plaintext_option.is_some() { process_message_plaintext( @@ -44,6 +45,7 @@ pub unconstrained fn process_message_ciphertext( process_custom_message, message_plaintext_option.unwrap(), message_context, + recipient, ); } else { aztecnr_warn_log_format!("Could not decrypt message ciphertext from tx {0}, ignoring")([message_context.tx_hash]); @@ -57,6 +59,7 @@ pub(crate) unconstrained fn process_message_plaintext( process_custom_message: Option>, message_plaintext: BoundedVec, message_context: MessageContext, + recipient: AztecAddress, ) { // The first thing to do after decrypting the message is to determine what type of message we're processing. We // have 3 message types: private notes, partial notes and events. @@ -75,31 +78,31 @@ pub(crate) unconstrained fn process_message_plaintext( message_context.tx_hash, message_context.unique_note_hashes_in_tx, message_context.first_nullifier_in_tx, - message_context.recipient, compute_note_hash, compute_note_nullifier, msg_metadata, msg_content, + recipient, ); } else if msg_type_id == PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID { aztecnr_debug_log!("Processing partial note private msg"); process_partial_note_private_msg( contract_address, - message_context.recipient, msg_metadata, msg_content, message_context.tx_hash, + recipient, ); } else if msg_type_id == PRIVATE_EVENT_MSG_TYPE_ID { aztecnr_debug_log!("Processing private event msg"); process_private_event_msg( contract_address, - message_context.recipient, msg_metadata, msg_content, message_context.tx_hash, + recipient, ); } else if msg_type_id < MIN_CUSTOM_MSG_TYPE_ID { // The message type ID falls in the range reserved for aztec.nr built-in types but wasn't matched above. @@ -117,6 +120,7 @@ pub(crate) unconstrained fn process_message_plaintext( msg_metadata, msg_content, message_context, + recipient, ); } else { // A custom message was received but no handler is configured. This likely means the contract emits custom diff --git a/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr b/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr index ba886e82d938..f5327b8484f9 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr @@ -1,14 +1,13 @@ -use crate::protocol::{ - address::AztecAddress, - constants::{DOM_SEP__CIPHERTEXT_FIELD_MASK, DOM_SEP__SYMMETRIC_KEY, DOM_SEP__SYMMETRIC_KEY_2}, - hash::poseidon2_hash_with_separator, - point::Point, - public_keys::AddressPoint, - traits::ToField, -}; +use crate::protocol::{address::AztecAddress, public_keys::AddressPoint, traits::ToField}; use crate::{ - keys::{ecdh_shared_secret::derive_ecdh_shared_secret, ephemeral::generate_positive_ephemeral_key_pair}, + keys::{ + ecdh_shared_secret::{ + compute_app_siloed_shared_secret, derive_ecdh_shared_secret, derive_shared_secret_field_mask, + derive_shared_secret_subkey, + }, + ephemeral::generate_positive_ephemeral_key_pair, + }, logging::aztecnr_warn_log_format, messages::{ encoding::{ @@ -33,75 +32,58 @@ use crate::{ use std::aes128::aes128_encrypt; -/// Computes N close-to-uniformly-random 256 bits from a given ECDH shared_secret. +/// Computes N close-to-uniformly-random 256 bits from a given app-siloed shared secret. /// -/// NEVER re-use the same iv and sym_key. DO NOT call this function more than once with the same shared_secret. +/// NEVER re-use the same iv and sym_key. DO NOT call this function more than once with the same s_app. /// -/// This function is only known to be safe if shared_secret is computed by combining a random ephemeral key with an -/// address point. See big comment within the body of the function. See big comment within the body of the function. -fn extract_many_close_to_uniformly_random_256_bits_from_ecdh_shared_secret_using_poseidon2_unsafe( - shared_secret: Point, -) -> [[u8; 32]; N] { +/// This function is only known to be safe if s_app is derived from combining a random ephemeral key with an +/// address point and a contract address. See big comment within the body of the function. +fn extract_many_close_to_uniformly_random_256_bits_using_poseidon2(s_app: Field) -> [[u8; 32]; N] { /* - * Unsafe because of https://eprint.iacr.org/2010/264.pdf Page 13, Lemma 2 (and the * two - paragraphs below it). + * Unsafe because of https://eprint.iacr.org/2010/264.pdf Page 13, Lemma 2 (and the two paragraphs below it). * - * If you call this function, you need to be careful and aware of how the arg - * `shared_secret` has been derived. + * If you call this function, you need to be careful and aware of how the arg `s_app` has been derived. * - * The paper says that the way you derive aes keys and IVs should be fine with poseidon2 - * (modelled as a RO), as long as you _don't_ use Poseidon2 as a PRG to generate the * two - exponents x & y which multiply to the shared secret S: + * The paper says that the way you derive aes keys and IVs should be fine with poseidon2 (modelled as a RO), + * as long as you _don't_ use Poseidon2 as a PRG to generate the two exponents x & y which multiply to the + * shared secret S: * * S = [x*y]*G. * - * (Otherwise, you would have to "key" poseidon2, i.e. generate a uniformly string K - * which can be public and compute Hash(x) as poseidon(K,x)). - * In that lemma, k would be 2*254=508, and m would be the number of points on the * grumpkin - curve (which is close to r according to the Hasse bound). + * (Otherwise, you would have to "key" poseidon2, i.e. generate a uniformly string K which can be public and + * compute Hash(x) as poseidon(K,x)). + * In that lemma, k would be 2*254=508, and m would be the number of points on the grumpkin curve (which is + * close to r according to the Hasse bound). * - * Our shared secret S is [esk * address_sk] * G, and the question is: * Can we compute hash(S) - using poseidon2 instead of sha256? + * Our shared secret S is [esk * address_sk] * G, and the question is: Can we compute hash(S) using poseidon2 + * instead of sha256? * * Well, esk is random and not generated with poseidon2, so that's good. * What about address_sk? - * Well, address_sk = poseidon2(stuff) + ivsk, so there was some - * discussion about whether address_sk is independent of poseidon2. - * Given that ivsk is random and independent of poseidon2, the address_sk is also + * Well, address_sk = poseidon2(stuff) + ivsk, so there was some discussion about whether address_sk is + * independent of poseidon2. Given that ivsk is random and independent of poseidon2, the address_sk is also * independent of poseidon2. * - * Tl;dr: we believe it's safe to hash S = [esk * address_sk] * G using poseidon2, - * in order to derive a symmetric key. - * - * If you're calling this function for a differently-derived `shared_secret`, be - * careful. + * Tl;dr: we believe it's safe to hash S = [esk * address_sk] * G using poseidon2, in order to derive a + * symmetric key. * + * If you're calling this function for a differently-derived `s_app`, be careful. */ /* The output of this function needs to be 32 random bytes. - * A single field won't give us 32 bytes of entropy. - * So we compute two "random" fields, by poseidon-hashing with two different - * generators. - * We then extract the last 16 (big endian) bytes of each "random" field. - * Note: we use to_be_bytes because it's slightly more efficient. But we have to - * be careful not to take bytes from the "big end", because the "big" byte is - * not uniformly random over the byte: it only has < 6 bits of randomness, because - * it's the big end of a 254-bit field element. + * A single field won't give us 32 bytes of entropy. So we compute two "random" fields, by poseidon-hashing + * with two different indices. We then extract the last 16 (big endian) bytes of each "random" field. + * Note: we use to_be_bytes because it's slightly more efficient. But we have to be careful not to take bytes + * from the "big end", because the "big" byte is not uniformly random over the byte: it only has < 6 bits of + * randomness, because it's the big end of a 254-bit field element. */ let mut all_bytes: [[u8; 32]; N] = std::mem::zeroed(); - // We restrict N to be < 2^8, because of how we compute the domain separator from k below (where k <= N must be 8 - // bits). In practice, it's extremely unlikely that an app will want to compute >= 256 ciphertexts. std::static_assert(N < 256, "N too large"); for k in 0..N { - // We augment the domain separator with the loop index, so that we can generate N lots of randomness. - let k_shift = (k << 8); - let separator_1 = k_shift + DOM_SEP__SYMMETRIC_KEY; - let separator_2 = k_shift + DOM_SEP__SYMMETRIC_KEY_2; - - let rand1: Field = poseidon2_hash_with_separator([shared_secret.x, shared_secret.y], separator_1); - let rand2: Field = poseidon2_hash_with_separator([shared_secret.x, shared_secret.y], separator_2); + let rand1: Field = derive_shared_secret_subkey(s_app, 2 * k); + let rand2: Field = derive_shared_secret_subkey(s_app, 2 * k + 1); let rand1_bytes: [u8; 32] = rand1.to_be_bytes(); let rand2_bytes: [u8; 32] = rand2.to_be_bytes(); @@ -139,11 +121,8 @@ fn derive_aes_symmetric_key_and_iv_from_uniformly_random_256_bits( many_pairs } -pub fn derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe( - shared_secret: Point, -) -> [([u8; 16], [u8; 16]); N] { - let many_random_256_bits: [[u8; 32]; N] = - extract_many_close_to_uniformly_random_256_bits_from_ecdh_shared_secret_using_poseidon2_unsafe(shared_secret); +pub fn derive_aes_symmetric_key_and_iv_from_shared_secret(s_app: Field) -> [([u8; 16], [u8; 16]); N] { + let many_random_256_bits: [[u8; 32]; N] = extract_many_close_to_uniformly_random_256_bits_using_poseidon2(s_app); derive_aes_symmetric_key_and_iv_from_uniformly_random_256_bits(many_random_256_bits) } @@ -218,9 +197,9 @@ impl MessageEncryption for AES128 { /// ``` /// /// **Step 2 -- Pack and mask.** The byte array is split into 31-byte chunks, each stored in one field. A - /// Poseidon2-derived mask (see `derive_field_mask`) is added to each so that the resulting fields appear as - /// uniformly random `Field` values to any observer without knowledge of the shared secret, hiding the fact - /// that the underlying ciphertext consists of 128-bit AES blocks. + /// Poseidon2-derived mask (see `derive_shared_secret_field_mask`) is added to each so that the resulting + /// fields appear as uniformly random `Field` values to any observer without knowledge of the shared secret, + /// hiding the fact that the underlying ciphertext consists of 128-bit AES blocks. /// /// **Step 3 -- Assemble ciphertext.** The ephemeral public key x-coordinate is prepended and random field padding /// is appended to fill to 15 fields: @@ -235,11 +214,15 @@ impl MessageEncryption for AES128 { /// /// ## Key Derivation /// - /// Two (key, IV) pairs are derived from the ECDH shared secret via Poseidon2 hashing with different domain - /// separators: one pair for the body ciphertext and one for the header ciphertext. + /// The raw ECDH shared secret point is first app-siloed into a scalar `s_app` by hashing with the contract + /// address (see + /// [`compute_app_siloed_shared_secret`](crate::keys::ecdh_shared_secret::compute_app_siloed_shared_secret)). + /// Two (key, IV) pairs are then derived from `s_app` via indexed Poseidon2 hashing: one pair for the body + /// ciphertext and one for the header ciphertext. fn encrypt( plaintext: [Field; PlaintextLen], recipient: AztecAddress, + contract_address: AztecAddress, ) -> [Field; MESSAGE_CIPHERTEXT_LEN] { std::static_assert( PlaintextLen <= MESSAGE_PLAINTEXT_LEN, @@ -253,7 +236,7 @@ impl MessageEncryption for AES128 { // Derive ECDH shared secret with recipient using a fresh ephemeral keypair. let (eph_sk, eph_pk) = generate_positive_ephemeral_key_pair(); - let ciphertext_shared_secret = derive_ecdh_shared_secret( + let raw_shared_secret = derive_ecdh_shared_secret( eph_sk, recipient .to_address_point() @@ -281,13 +264,12 @@ impl MessageEncryption for AES128 { .inner, ); - // AES128-CBC encrypt the plaintext bytes. - // It is safe to call the `unsafe` function here, because we know the `shared_secret` was derived using an - // AztecAddress (the recipient). See the block comment at the start of this unsafe target function for more - // info. - let pairs = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe::<2>( - ciphertext_shared_secret, - ); + let s_app = compute_app_siloed_shared_secret(raw_shared_secret, contract_address); + + // It is safe to derive AES keys from `s_app` using Poseidon2 because `s_app` was derived from an ECDH shared + // secret using an AztecAddress (the recipient). See the block comment in + // `extract_many_close_to_uniformly_random_256_bits_using_poseidon2` for more info. + let pairs = derive_aes_symmetric_key_and_iv_from_shared_secret::<2>(s_app); let (body_sym_key, body_iv) = pairs[0]; let (header_sym_key, header_iv) = pairs[1]; @@ -363,7 +345,7 @@ impl MessageEncryption for AES128 { // values let mut offset = 1; for i in 0..message_bytes_as_fields.len() { - let mask = derive_field_mask(ciphertext_shared_secret, i as u32); + let mask = derive_shared_secret_field_mask(s_app, i as u32); ciphertext[offset + i] = message_bytes_as_fields[i] + mask; } offset += message_bytes_as_fields.len(); @@ -381,6 +363,7 @@ impl MessageEncryption for AES128 { unconstrained fn decrypt( ciphertext: BoundedVec, recipient: AztecAddress, + contract_address: AztecAddress, ) -> Option> { // Extract the ephemeral public key x-coordinate and masked fields, returning None for empty ciphertext. if ciphertext.len() > 0 { @@ -395,11 +378,10 @@ impl MessageEncryption for AES128 { // y-coordinate must be positive. This may fail however, as not all x-coordinates are on the curve. In // that case, we simply return `Option::none`. point_from_x_coord_and_sign(eph_pk_x, true).and_then(|eph_pk| { - // Derive shared secret - let ciphertext_shared_secret = get_shared_secret(recipient, eph_pk); + let s_app = get_shared_secret(recipient, eph_pk, contract_address); let unmasked_fields = masked_fields.mapi(|i, field| { - let unmasked = unmask_field(ciphertext_shared_secret, i, field); + let unmasked = unmask_field(s_app, i, field); // If we failed to unmask the field, we are dealing with the random padding. We'll ignore it // later, so we can simply set it to 0 unmasked.unwrap_or(0) @@ -407,9 +389,7 @@ impl MessageEncryption for AES128 { let ciphertext_without_eph_pk_x = bytes_from_fields(unmasked_fields); // Derive symmetric keys: - let pairs = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe::<2>( - ciphertext_shared_secret, - ); + let pairs = derive_aes_symmetric_key_and_iv_from_shared_secret::<2>(s_app); let (body_sym_key, body_iv) = pairs[0]; let (header_sym_key, header_iv) = pairs[1]; @@ -467,8 +447,8 @@ global TWO_POW_248: Field = 2.pow_32(248); /// Removes the Poseidon2-derived mask from a ciphertext field. Returns the unmasked value if it fits in 31 bytes /// (a content field), or `None` if it doesn't (random padding). Unconstrained to prevent accidental use in /// constrained context. -unconstrained fn unmask_field(shared_secret: Point, index: u32, masked: Field) -> Option { - let unmasked = masked - derive_field_mask(shared_secret, index); +unconstrained fn unmask_field(s_app: Field, index: u32, masked: Field) -> Option { + let unmasked = masked - derive_shared_secret_field_mask(s_app, index); if unmasked.lt(TWO_POW_248) { Option::some(unmasked) } else { @@ -476,15 +456,6 @@ unconstrained fn unmask_field(shared_secret: Point, index: u32, masked: Field) - } } -/// Derives a field mask from an ECDH shared secret and field index. Applied only to data fields (those carrying -/// packed message bytes). Padding fields use `random()` instead. -fn derive_field_mask(shared_secret: Point, index: u32) -> Field { - poseidon2_hash_with_separator( - [shared_secret.x, shared_secret.y], - DOM_SEP__CIPHERTEXT_FIELD_MASK + index, - ) -} - /// Produces a random valid address point, i.e. one that is on the curve. This is equivalent to calling /// [`AztecAddress::to_address_point`] on a random valid address. unconstrained fn random_address_point() -> AddressPoint { @@ -506,7 +477,7 @@ unconstrained fn random_address_point() -> AddressPoint { mod test { use crate::{ - keys::ecdh_shared_secret::derive_ecdh_shared_secret, + keys::ecdh_shared_secret::{compute_app_siloed_shared_secret, derive_ecdh_shared_secret}, messages::{ encoding::{HEADER_CIPHERTEXT_SIZE_IN_BYTES, MESSAGE_PLAINTEXT_LEN, MESSAGE_PLAINTEXT_SIZE_IN_BYTES}, encryption::message_encryption::MessageEncryption, @@ -522,7 +493,8 @@ mod test { let env = TestEnvironment::new(); // Message decryption requires oracles that are only available during private execution - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let plaintext = [1, 2, 3]; let recipient = AztecAddress::from_field( @@ -539,18 +511,19 @@ mod test { let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(42); // Encrypt the message - let encrypted_message = BoundedVec::from_array(AES128::encrypt(plaintext, recipient)); + let encrypted_message = BoundedVec::from_array(AES128::encrypt(plaintext, recipient, contract_address)); - // Mock shared secret for deterministic test - let shared_secret = derive_ecdh_shared_secret( + // Compute the same app-siloed shared secret that the oracle would return + let raw_shared_secret = derive_ecdh_shared_secret( EmbeddedCurveScalar::from_field(eph_sk), recipient.to_address_point().unwrap().inner, ); + let s_app = compute_app_siloed_shared_secret(raw_shared_secret, contract_address); - let _ = OracleMock::mock("aztec_utl_getSharedSecret").returns(shared_secret); + let _ = OracleMock::mock("aztec_utl_getSharedSecret").returns(s_app); // Decrypt the message - let decrypted = AES128::decrypt(encrypted_message, recipient).unwrap(); + let decrypted = AES128::decrypt(encrypted_message, recipient, contract_address).unwrap(); // The decryption function spits out a BoundedVec because it's designed to work with messages with unknown // length at compile time. For this reason we need to convert the original input to a BoundedVec. @@ -573,12 +546,18 @@ mod test { let recipient = env.create_light_account(); - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let plaintext = [1, 2, 3]; - let ciphertext = AES128::encrypt(plaintext, recipient); + let ciphertext = AES128::encrypt(plaintext, recipient, contract_address); assert_eq( - AES128::decrypt(BoundedVec::from_array(ciphertext), recipient).unwrap(), + AES128::decrypt( + BoundedVec::from_array(ciphertext), + recipient, + contract_address, + ) + .unwrap(), BoundedVec::from_array(plaintext), ); }); @@ -588,10 +567,9 @@ mod test { unconstrained fn encrypt_to_invalid_address() { // x = 3 is a non-residue for this curve, resulting in an invalid address let invalid_address = AztecAddress { inner: 3 }; + let contract_address = AztecAddress { inner: 42 }; - // We just test that we produced some output and did not crash - the result is gibberish as it is encrypted - // using a public key for which we do not know the private key. - let _ = AES128::encrypt([1, 2, 3, 4], invalid_address); + let _ = AES128::encrypt([1, 2, 3, 4], invalid_address, contract_address); } // Documents the PKCS#7 padding behavior that `encrypt` relies on (see its static_assert). @@ -615,15 +593,21 @@ mod test { let mut env = TestEnvironment::new(); let recipient = env.create_light_account(); - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let mut plaintext = [0; MESSAGE_PLAINTEXT_LEN]; for i in 0..MESSAGE_PLAINTEXT_LEN { plaintext[i] = i as Field; } - let ciphertext = AES128::encrypt(plaintext, recipient); + let ciphertext = AES128::encrypt(plaintext, recipient, contract_address); assert_eq( - AES128::decrypt(BoundedVec::from_array(ciphertext), recipient).unwrap(), + AES128::decrypt( + BoundedVec::from_array(ciphertext), + recipient, + contract_address, + ) + .unwrap(), BoundedVec::from_array(plaintext), ); }); @@ -632,8 +616,9 @@ mod test { #[test(should_fail_with = "Plaintext length exceeds MESSAGE_PLAINTEXT_LEN")] unconstrained fn encrypt_oversized_plaintext() { let address = AztecAddress { inner: 3 }; + let contract_address = AztecAddress { inner: 42 }; let plaintext: [Field; MESSAGE_PLAINTEXT_LEN + 1] = [0; MESSAGE_PLAINTEXT_LEN + 1]; - let _ = AES128::encrypt(plaintext, address); + let _ = AES128::encrypt(plaintext, address, contract_address); } #[test] @@ -652,16 +637,17 @@ mod test { let recipient = env.create_light_account(); - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let plaintext = [1, 2, 3, 4]; - let ciphertext = AES128::encrypt(plaintext, recipient); + let ciphertext = AES128::encrypt(plaintext, recipient, contract_address); // The first field of the ciphertext is the x-coordinate of the ephemeral public key. We set it to a known // non-residue (3), causing `decrypt` to fail to produce a decryption shared secret. let mut bad_ciphertext = BoundedVec::from_array(ciphertext); bad_ciphertext.set(0, 3); - assert(AES128::decrypt(bad_ciphertext, recipient).is_none()); + assert(AES128::decrypt(bad_ciphertext, recipient, contract_address).is_none()); }); } @@ -670,7 +656,10 @@ mod test { let mut env = TestEnvironment::new(); let recipient = env.create_light_account(); - env.private_context(|_| { assert(AES128::decrypt(BoundedVec::new(), recipient).is_none()); }); + env.private_context(|context| { + let contract_address = context.this_address(); + assert(AES128::decrypt(BoundedVec::new(), recipient, contract_address).is_none()); + }); } // Mocks the header AES decrypt oracle to return an empty result. The TS oracle never throws on invalid @@ -680,14 +669,15 @@ mod test { let mut env = TestEnvironment::new(); let recipient = env.create_light_account(); - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let plaintext = [1, 2, 3]; - let ciphertext = BoundedVec::from_array(AES128::encrypt(plaintext, recipient)); + let ciphertext = BoundedVec::from_array(AES128::encrypt(plaintext, recipient, contract_address)); let empty_header = BoundedVec::::new(); - let _ = OracleMock::mock("aztec_utl_tryAes128Decrypt").returns(Option::some(empty_header)).times(1); + let _ = OracleMock::mock("aztec_utl_decryptAes128").returns(Option::some(empty_header)).times(1); - assert(AES128::decrypt(ciphertext, recipient).is_none()); + assert(AES128::decrypt(ciphertext, recipient, contract_address).is_none()); }); } @@ -698,16 +688,17 @@ mod test { let mut env = TestEnvironment::new(); let recipient = env.create_light_account(); - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let plaintext = [1, 2, 3]; - let ciphertext = BoundedVec::from_array(AES128::encrypt(plaintext, recipient)); + let ciphertext = BoundedVec::from_array(AES128::encrypt(plaintext, recipient, contract_address)); let bad_header = BoundedVec::::from_array(encode_header( MESSAGE_PLAINTEXT_SIZE_IN_BYTES + 1, )); - let _ = OracleMock::mock("aztec_utl_tryAes128Decrypt").returns(Option::some(bad_header)).times(1); + let _ = OracleMock::mock("aztec_utl_decryptAes128").returns(Option::some(bad_header)).times(1); - assert(AES128::decrypt(ciphertext, recipient).is_none()); + assert(AES128::decrypt(ciphertext, recipient, contract_address).is_none()); }); } diff --git a/noir-projects/aztec-nr/aztec/src/messages/encryption/message_encryption.nr b/noir-projects/aztec-nr/aztec/src/messages/encryption/message_encryption.nr index 6896a0b9e24e..c47e59a37fb9 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/encryption/message_encryption.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/encryption/message_encryption.nr @@ -34,6 +34,7 @@ pub trait MessageEncryption { fn encrypt( plaintext: [Field; PlaintextLen], recipient: AztecAddress, + contract_address: AztecAddress, ) -> [Field; MESSAGE_CIPHERTEXT_LEN]; /// Decrypts a message ciphertext into its original plaintext. @@ -49,5 +50,6 @@ pub trait MessageEncryption { unconstrained fn decrypt( ciphertext: BoundedVec, recipient: AztecAddress, + contract_address: AztecAddress, ) -> Option>; } diff --git a/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr b/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr index 4dd68c91a33f..0c2711c5f829 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr @@ -1,18 +1,12 @@ use crate::{ messages::{ encoding::{encode_message, MAX_MESSAGE_CONTENT_LEN, MESSAGE_EXPANDED_METADATA_LEN}, - encryption::{aes128::AES128, message_encryption::MessageEncryption}, - logs::utils::prefix_with_tag, msg_type::PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID, }, note::note_interface::NoteType, utils::array, }; -use crate::protocol::{ - address::AztecAddress, - constants::PRIVATE_LOG_SIZE_IN_FIELDS, - traits::{FromField, Packable, ToField}, -}; +use crate::protocol::{address::AztecAddress, traits::{FromField, Packable, ToField}}; /// The number of fields in a private note message content that are not the note's packed representation. pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN: u32 = 3; @@ -25,28 +19,6 @@ pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NOTE_COMPLETION_LOG_TAG_IND pub global MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN: u32 = MAX_MESSAGE_CONTENT_LEN - PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN; -// TODO(#16881): once partial notes support delivery via an offchain message we will most likely want to remove this. -pub fn compute_partial_note_private_content_log( - partial_note_private_content: PartialNotePrivateContent, - owner: AztecAddress, - randomness: Field, - recipient: AztecAddress, - note_completion_log_tag: Field, -) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] -where - PartialNotePrivateContent: NoteType + Packable, -{ - let message_plaintext = encode_partial_note_private_message( - partial_note_private_content, - owner, - randomness, - note_completion_log_tag, - ); - let message_ciphertext = AES128::encrypt(message_plaintext, recipient); - - prefix_with_tag(message_ciphertext, recipient) -} - /// Creates the plaintext for a partial note private message (i.e. one of type [`PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID`]). /// /// This plaintext is meant to be decoded via [`decode_partial_note_private_message`]. diff --git a/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr b/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr index afd9ab550f49..526acc55b4d3 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr @@ -2,63 +2,44 @@ use crate::oracle::notes::{get_next_app_tag_as_sender, get_sender_for_tags}; use crate::protocol::address::AztecAddress; // TODO(#14565): Add constrained tagging -pub(crate) fn prefix_with_tag(log_without_tag: [Field; L], recipient: AztecAddress) -> [Field; L + 1] { +/// Returns the next discovery tag for a private log sent to `recipient`. +/// +/// Private logs are encrypted, so the recipient cannot tell which logs are meant for it just by looking at them. +/// To solve this, sender and recipient derive a shared secret from their keys, and from that secret they produce a +/// sequence of one-time tags (tag_0, tag_1, ...). The recipient scans for these tags because it can compute the same +/// sequence. This function returns the next raw (not domain-separated) tag in the sequence. +pub(crate) fn compute_discovery_tag(recipient: AztecAddress) -> Field { // Safety: we assume that the sender wants for the recipient to find the tagged note, and therefore that they will // cooperate and use the correct tag. Usage of a bad tag will result in the recipient not being able to find the // note automatically. - let tag = unsafe { + unsafe { let sender = get_sender_for_tags().expect( f"Sender for tags is not set when emitting a private log. Set it by calling `set_sender_for_tags(...)`.", ); get_next_app_tag_as_sender(sender, recipient) - }; - - let mut log_with_tag = [0; L + 1]; - - log_with_tag[0] = tag; - for i in 0..log_without_tag.len() { - log_with_tag[i + 1] = log_without_tag[i]; } - - log_with_tag } mod test { use crate::protocol::{address::AztecAddress, traits::FromField}; - use super::prefix_with_tag; + use super::compute_discovery_tag; use std::test::OracleMock; - #[test(should_fail)] + #[test(should_fail_with = "Sender for tags is not set")] unconstrained fn no_tag_sender() { let recipient = AztecAddress::from_field(2); - - let expected_tag = 42; - - // Mock the tagging oracles - note aztec_prv_getSenderForTags returns none let _ = OracleMock::mock("aztec_prv_getSenderForTags").returns(Option::::none()); - let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(expected_tag); - - let log_without_tag = [1, 2, 3]; - let _ = prefix_with_tag(log_without_tag, recipient); + let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(42); + let _ = compute_discovery_tag(recipient); } #[test] - unconstrained fn prefixing_with_tag() { + unconstrained fn returns_oracle_tag() { let sender = AztecAddress::from_field(1); let recipient = AztecAddress::from_field(2); - let expected_tag = 42; - - // Mock the tagging oracles let _ = OracleMock::mock("aztec_prv_getSenderForTags").returns(Option::some(sender)); let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(expected_tag); - - let log_without_tag = [1, 2, 3]; - let log_with_tag = prefix_with_tag(log_without_tag, recipient); - - let expected_result = [expected_tag, 1, 2, 3]; - - // Check tag was prefixed correctly - assert_eq(log_with_tag, expected_result, "Tag was not prefixed correctly"); + assert_eq(compute_discovery_tag(recipient), expected_tag); } } diff --git a/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr b/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr index 20bae1d6059b..8b4a489ffe50 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr @@ -2,12 +2,12 @@ use crate::{ context::PrivateContext, messages::{ encryption::{aes128::AES128, message_encryption::MessageEncryption}, - logs::utils::prefix_with_tag, + logs::utils::compute_discovery_tag, offchain_messages::deliver_offchain_message, }, utils::remove_constraints::remove_constraints_if, }; -use crate::protocol::address::AztecAddress; +use crate::protocol::{address::AztecAddress, constants::DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG, hash::compute_log_tag}; /// Placeholder struct until Noir adds `enum` support. /// @@ -210,17 +210,20 @@ pub fn do_private_message_delivery( // TODO(#14565): Add constrained tagging let _constrained_tagging = delivery_mode == MessageDelivery.ONCHAIN_CONSTRAINED; + let contract_address = context.this_address(); let ciphertext = remove_constraints_if( !constrained_encryption, - || AES128::encrypt(encode_into_message_plaintext(), recipient), + || AES128::encrypt(encode_into_message_plaintext(), recipient, contract_address), ); if deliver_as_offchain_message { deliver_offchain_message(ciphertext, recipient); } else { - // Safety: Currently unsafe. See description of ONCHAIN_CONSTRAINED in MessageDeliveryEnum. TODO(#14565): - // Implement proper constrained tag prefixing to make this truly ONCHAIN_CONSTRAINED - let log_content = prefix_with_tag(ciphertext, recipient); + // TODO(#14565): constrained tagging is not yet implemented. Both modes currently use the unconstrained + // domain separator because the discovery tag always comes from an oracle. Once constrained tagging lands, + // this should branch on `constrained_tagging` to select the appropriate separator. + let discovery_tag = compute_discovery_tag(recipient); + let log_tag = compute_log_tag(discovery_tag, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG); // We forbid this value not being constant to avoid predicating the context calls below, which might result in // the context's arrays having unknown compile time write indices and hence dramatically increasing constraints @@ -234,9 +237,9 @@ pub fn do_private_message_delivery( // // Note that the log always has the same length regardless of `MESSAGE_PLAINTEXT_LEN`, because all message // ciphertexts also have the same length. This prevents accidental privacy leakage via the log length. - context.emit_raw_note_log(log_content, log_content.len(), maybe_note_hash_counter.unwrap()); + context.emit_raw_note_log_unsafe(log_tag, ciphertext, ciphertext.len(), maybe_note_hash_counter.unwrap()); } else { - context.emit_private_log(log_content, log_content.len()); + context.emit_private_log_unsafe(log_tag, ciphertext, ciphertext.len()); } } } diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr index b5337e61e507..98ef074b84e5 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr @@ -12,7 +12,6 @@ pub(crate) struct EventValidationRequest { pub serialized_event: BoundedVec, pub event_commitment: Field, pub tx_hash: Field, - pub recipient: AztecAddress, } mod test { @@ -29,7 +28,6 @@ mod test { serialized_event: BoundedVec::from_array([4, 5]), event_commitment: 6, tx_hash: 7, - recipient: AztecAddress::from_field(8), }; // We define the serialization in Noir and the deserialization in TS. If the deserialization changes from the @@ -46,7 +44,6 @@ mod test { 2, // bounded_vec_len 6, // event_commitment 7, // tx_hash - 8, // recipient ]; assert_eq(request.serialize(), expected_serialization); diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr index b98b4bdeff20..4f456bf6003b 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr @@ -1,4 +1,4 @@ -use crate::protocol::{address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX, traits::{Deserialize, Serialize}}; +use crate::protocol::{constants::MAX_NOTE_HASHES_PER_TX, traits::{Deserialize, Serialize}}; /// Additional information needed to process a message. /// @@ -10,26 +10,11 @@ pub struct MessageContext { pub tx_hash: Field, pub unique_note_hashes_in_tx: BoundedVec, pub first_nullifier_in_tx: Field, - pub recipient: AztecAddress, -} - -/// Transaction context needed to process a message. -/// -/// Like [`MessageContext`], but `MessageTxContext` does not include the recipient. MessageTxContext's are kind of -/// adhoc: they are just the minimal data structure that the contract needs to get from a PXE oracle to prepare -/// offchain messages to be processed. We reify it with a type just because it crosses Noir<->TS boundaries. -/// The contract knows how to pair the context data with a recipient: then it is able to build a `MessageContext` for -/// subsequent processing. -#[derive(Serialize, Deserialize, Eq)] -pub(crate) struct MessageTxContext { - pub tx_hash: Field, - pub unique_note_hashes_in_tx: BoundedVec, - pub first_nullifier_in_tx: Field, } mod test { use crate::messages::processing::MessageContext; - use crate::protocol::{address::AztecAddress, traits::{Deserialize, FromField}}; + use crate::protocol::traits::Deserialize; #[test] unconstrained fn message_context_serialization_matches_typescript() { @@ -37,14 +22,12 @@ mod test { let tx_hash = 123; let unique_note_hashes = BoundedVec::from_array([4, 5]); let first_nullifier = 6; - let recipient = AztecAddress::from_field(789); // Create a MessageContext instance let message_context = MessageContext { tx_hash, unique_note_hashes_in_tx: unique_note_hashes, first_nullifier_in_tx: first_nullifier, - recipient, }; // Expected output generated from TypeScript's `MessageContext.toFields()` @@ -116,7 +99,6 @@ mod test { 0x0000000000000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000002, 0x0000000000000000000000000000000000000000000000000000000000000006, - 0x0000000000000000000000000000000000000000000000000000000000000315, ]; let deserialized = MessageContext::deserialize(serialized_message_context_from_typescript); diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index 6adfbc1d43e9..486436587642 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -3,7 +3,6 @@ pub mod offchain; mod message_context; pub use message_context::MessageContext; -pub(crate) use message_context::MessageTxContext; pub(crate) mod note_validation_request; pub(crate) mod log_retrieval_request; @@ -24,7 +23,12 @@ use crate::{ }, oracle, }; -use crate::protocol::{address::AztecAddress, hash::sha256_to_field, traits::{Deserialize, Serialize}}; +use crate::protocol::{ + address::AztecAddress, + constants::DOM_SEP__NOTE_COMPLETION_LOG_TAG, + hash::{compute_log_tag, sha256_to_field}, + traits::{Deserialize, Serialize}, +}; use event_validation_request::EventValidationRequest; // Base slot for the pending tagged log array to which the fetch_tagged_logs oracle inserts found private logs. @@ -54,14 +58,16 @@ pub struct OffchainMessageWithContext { pub message_context: MessageContext, } -/// Searches for private logs emitted by `contract_address` that might contain messages for one of the local accounts, -/// and stores them in a `CapsuleArray` which is then returned. -pub(crate) unconstrained fn get_private_logs(contract_address: AztecAddress) -> CapsuleArray { +/// Searches for private logs emitted by `contract_address` that might contain messages for the given `scope`. +pub(crate) unconstrained fn get_private_logs( + contract_address: AztecAddress, + scope: AztecAddress, +) -> CapsuleArray { // We will eventually perform log discovery via tagging here, but for now we simply call the `fetchTaggedLogs` // oracle. This makes PXE synchronize tags, download logs and store the pending tagged logs in a capsule array. - oracle::message_processing::fetch_tagged_logs(PENDING_TAGGED_LOG_ARRAY_BASE_SLOT); + oracle::message_processing::fetch_tagged_logs(PENDING_TAGGED_LOG_ARRAY_BASE_SLOT, scope); - CapsuleArray::at(contract_address, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT) + CapsuleArray::at(contract_address, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT, scope) } /// Enqueues a note for validation and storage by PXE. @@ -82,7 +88,7 @@ pub(crate) unconstrained fn get_private_logs(contract_address: AztecAddress) -> /// `owner` is the address used in note hash and nullifier computation, often requiring knowledge of their nullifier /// secret key. /// -/// `recipient` is the account to which the note message was delivered (i.e. the address the message was encrypted to). +/// `scope` is the account to which the note message was delivered (i.e. the address the message was encrypted to). /// This determines which PXE account can see the note - other accounts will not be able to access it (e.g. other /// accounts will not be able to see one another's token balance notes, even in the same PXE) unless authorized. In /// most cases `recipient` equals `owner`, but they can differ in scenarios like delegated discovery. @@ -96,24 +102,28 @@ pub unconstrained fn enqueue_note_for_validation( note_hash: Field, nullifier: Field, tx_hash: Field, - recipient: AztecAddress, + scope: AztecAddress, ) { // We store requests in a `CapsuleArray`, which PXE will later read from and deserialize into its version of the // Noir `NoteValidationRequest` - CapsuleArray::at(contract_address, NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).push( - NoteValidationRequest { - contract_address, - owner, - storage_slot, - randomness, - note_nonce, - packed_note, - note_hash, - nullifier, - tx_hash, - recipient, - }, + CapsuleArray::at( + contract_address, + NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, + scope, ) + .push( + NoteValidationRequest { + contract_address, + owner, + storage_slot, + randomness, + note_nonce, + packed_note, + note_hash, + nullifier, + tx_hash, + }, + ) } /// Enqueues an event for validation and storage by PXE. @@ -135,21 +145,25 @@ pub unconstrained fn enqueue_event_for_validation( serialized_event: BoundedVec, event_commitment: Field, tx_hash: Field, - recipient: AztecAddress, + scope: AztecAddress, ) { // We store requests in a `CapsuleArray`, which PXE will later read from and deserialize into its version of the // Noir `EventValidationRequest` - CapsuleArray::at(contract_address, EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).push( - EventValidationRequest { - contract_address, - event_type_id, - randomness, - serialized_event, - event_commitment, - tx_hash, - recipient, - }, + CapsuleArray::at( + contract_address, + EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, + scope, ) + .push( + EventValidationRequest { + contract_address, + event_type_id, + randomness, + serialized_event, + event_commitment, + tx_hash, + }, + ) } /// Validates and stores all enqueued notes and events. @@ -159,13 +173,14 @@ pub unconstrained fn enqueue_event_for_validation( /// API (PXE::getPrivateEvents). /// /// This automatically clears both validation request queues, so no further work needs to be done by the caller. -pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_address: AztecAddress) { +pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_address: AztecAddress, scope: AztecAddress) { oracle::message_processing::validate_and_store_enqueued_notes_and_events( contract_address, NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, MAX_NOTE_PACKED_LEN as Field, MAX_EVENT_SERIALIZED_LEN as Field, + scope, ); } @@ -174,15 +189,17 @@ pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_addre /// The `message_context_requests_array_base_slot` must point to a CapsuleArray containing tx hashes. /// PXE will store `Option` values into the responses array at /// `message_context_responses_array_base_slot`. -pub unconstrained fn resolve_message_contexts( +pub unconstrained fn get_message_contexts_by_tx_hash( contract_address: AztecAddress, message_context_requests_array_base_slot: Field, message_context_responses_array_base_slot: Field, + scope: AztecAddress, ) { - oracle::message_processing::resolve_message_contexts( + oracle::message_processing::get_message_contexts_by_tx_hash( contract_address, message_context_requests_array_base_slot, message_context_responses_array_base_slot, + scope, ); } @@ -196,8 +213,13 @@ pub unconstrained fn resolve_message_contexts( pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( contract_address: AztecAddress, pending_partial_notes: CapsuleArray, + scope: AztecAddress, ) -> CapsuleArray> { - let log_retrieval_requests = CapsuleArray::at(contract_address, LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT); + let log_retrieval_requests = CapsuleArray::at( + contract_address, + LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT, + scope, + ); // We create a LogRetrievalRequest for each PendingPartialNote in the CapsuleArray. Because we need the indices in // the request array to match the indices in the partial note array, we can't use CapsuleArray::for_each, as that @@ -207,17 +229,26 @@ pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( let pending_partial_notes_count = pending_partial_notes.len(); while i < pending_partial_notes_count { let pending_partial_note = pending_partial_notes.get(i); - log_retrieval_requests.push( - LogRetrievalRequest { contract_address, unsiloed_tag: pending_partial_note.note_completion_log_tag }, + // Partial note completion logs are emitted with a domain-separated tag. To find matching logs, we apply the + // same domain separation to the stored raw tag. + let log_tag = compute_log_tag( + pending_partial_note.note_completion_log_tag, + DOM_SEP__NOTE_COMPLETION_LOG_TAG, ); + log_retrieval_requests.push(LogRetrievalRequest { contract_address, unsiloed_tag: log_tag }); i += 1; } - oracle::message_processing::bulk_retrieve_logs( + oracle::message_processing::get_logs_by_tag( contract_address, LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT, LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT, + scope, ); - CapsuleArray::at(contract_address, LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT) + CapsuleArray::at( + contract_address, + LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT, + scope, + ) } diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr index d845284f2f23..0d7c101eef38 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr @@ -15,7 +15,6 @@ pub(crate) struct NoteValidationRequest { pub note_hash: Field, pub nullifier: Field, pub tx_hash: Field, - pub recipient: AztecAddress, } mod test { @@ -34,7 +33,6 @@ mod test { note_hash: 6, nullifier: 7, tx_hash: 8, - recipient: AztecAddress::from_field(9), }; // We define the serialization in Noir and the deserialization in TS. If the deserialization changes from the @@ -59,7 +57,6 @@ mod test { 0x0000000000000000000000000000000000000000000000000000000000000006, 0x0000000000000000000000000000000000000000000000000000000000000007, 0x0000000000000000000000000000000000000000000000000000000000000008, - 0x0000000000000000000000000000000000000000000000000000000000000009, ]; assert_eq(request.serialize(), expected_serialization); diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr index d33e2a8af2fc..8e62cbfc2b27 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr @@ -3,9 +3,9 @@ use crate::{ context::UtilityContext, messages::{ encoding::MESSAGE_CIPHERTEXT_LEN, - processing::{MessageContext, MessageTxContext, OffchainMessageWithContext, resolve_message_contexts}, + processing::{get_message_contexts_by_tx_hash, MessageContext, OffchainMessageWithContext}, }, - oracle::contract_sync::invalidate_contract_sync_cache, + oracle::contract_sync::set_contract_sync_cache_invalid, protocol::{ address::AztecAddress, constants::MAX_TX_LIFETIME, @@ -54,7 +54,7 @@ global MAX_MSG_TTL: u64 = MAX_TX_LIFETIME + TX_EXPIRATION_TOLERANCE; /// The only current implementation of an `OffchainInboxSync` is [`sync_inbox`], which manages an inbox with expiration /// based eviction and automatic transaction context resolution. pub(crate) type OffchainInboxSync = unconstrained fn[Env]( -/* contract_address */AztecAddress) -> CapsuleArray; +/* contract_address */AztecAddress, /* scope */ AztecAddress) -> CapsuleArray; /// A message delivered via the `offchain_receive` utility function. pub struct OffchainMessage { @@ -93,6 +93,9 @@ struct PendingOffchainMsg { /// calls this function to hand the messages to the contract so they can be processed through the same mechanisms as /// onchain messages. /// +/// Each message is routed to the inbox scoped to its `recipient` field, so messages for different accounts are +/// automatically isolated. +/// /// Messages are processed when their originating transaction is found onchain (providing the context needed to /// validate resulting notes and events). /// @@ -104,7 +107,6 @@ pub unconstrained fn receive( contract_address: AztecAddress, messages: BoundedVec, ) { - let inbox: CapsuleArray = CapsuleArray::at(contract_address, OFFCHAIN_INBOX_SLOT); // May contain duplicates if multiple messages target the same recipient. This is harmless since // cache invalidation on the TS side is idempotent (deleting an already-deleted key is a no-op). let mut scopes: BoundedVec = BoundedVec::new(); @@ -117,6 +119,8 @@ pub unconstrained fn receive( } else { 0 }; + let inbox: CapsuleArray = + CapsuleArray::at(contract_address, OFFCHAIN_INBOX_SLOT, msg.recipient); inbox.push( PendingOffchainMsg { ciphertext: msg.ciphertext, @@ -129,20 +133,24 @@ pub unconstrained fn receive( i += 1; } - invalidate_contract_sync_cache(contract_address, scopes); + set_contract_sync_cache_invalid(contract_address, scopes); } /// Returns offchain-delivered messages to process during sync. /// /// Messages remain in the inbox and are reprocessed on each sync until their originating transaction is no longer at /// risk of being dropped by a reorg. -pub unconstrained fn sync_inbox(address: AztecAddress) -> CapsuleArray { - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); - let context_resolution_requests: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_CONTEXT_REQUESTS_SLOT); - let resolved_contexts: CapsuleArray> = - CapsuleArray::at(address, OFFCHAIN_CONTEXT_RESPONSES_SLOT); +pub unconstrained fn sync_inbox( + contract_address: AztecAddress, + scope: AztecAddress, +) -> CapsuleArray { + let inbox: CapsuleArray = CapsuleArray::at(contract_address, OFFCHAIN_INBOX_SLOT, scope); + let context_resolution_requests: CapsuleArray = + CapsuleArray::at(contract_address, OFFCHAIN_CONTEXT_REQUESTS_SLOT, scope); + let resolved_contexts: CapsuleArray> = + CapsuleArray::at(contract_address, OFFCHAIN_CONTEXT_RESPONSES_SLOT, scope); let ready_to_process: CapsuleArray = - CapsuleArray::at(address, OFFCHAIN_READY_MESSAGES_SLOT); + CapsuleArray::at(contract_address, OFFCHAIN_READY_MESSAGES_SLOT, scope); // Clear any stale ready messages from a previous run. ready_to_process.for_each(|i, _| { ready_to_process.remove(i); }); @@ -161,10 +169,11 @@ pub unconstrained fn sync_inbox(address: AztecAddress) -> CapsuleArray CapsuleArray CapsuleArray, anchor_block_timestamp: u64) -> OffchainMessage { - OffchainMessage { - ciphertext: BoundedVec::new(), - recipient: AztecAddress::from_field(42), - tx_hash, - anchor_block_timestamp, - } + OffchainMessage { ciphertext: BoundedVec::new(), recipient: SCOPE, tx_hash, anchor_block_timestamp } } /// Advances the TXE block timestamp by `offset` seconds and returns the resulting timestamp. @@ -263,9 +261,9 @@ mod test { unconstrained fn empty_inbox_returns_empty_result() { let env = TestEnvironment::new(); env.utility_context(|context| { - let result = sync_inbox(context.this_address()); + let result = sync_inbox(context.this_address(), SCOPE); let inbox: CapsuleArray = - CapsuleArray::at(context.this_address(), OFFCHAIN_INBOX_SLOT); + CapsuleArray::at(context.this_address(), OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); assert_eq(inbox.len(), 0); @@ -288,8 +286,8 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); // context is None, not ready assert_eq(inbox.len(), 0); // expired, removed @@ -312,8 +310,8 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); // context is None, not ready assert_eq(inbox.len(), 1); // not expired, stays @@ -336,8 +334,8 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); // context is None, not ready assert_eq(inbox.len(), 0); // expired, removed @@ -359,8 +357,8 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); // not resolved, not ready assert_eq(inbox.len(), 1); // not expired, stays @@ -392,8 +390,8 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); // all contexts are None // Message 0 expired (anchor=0), message 1 survived (anchor=anchor_ts), @@ -424,15 +422,14 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); // The message should be ready to process since its tx context was resolved. assert_eq(result.len(), 1); let ctx = result.get(0).message_context; assert_eq(ctx.tx_hash, known_tx_hash); - assert_eq(ctx.recipient, AztecAddress::from_field(42)); assert(ctx.first_nullifier_in_tx != 0, "resolved context must have a first nullifier"); // Message stays in inbox (not expired) for potential reorg reprocessing. diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr index 6739fb8d3253..7a85decfb504 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr @@ -12,7 +12,7 @@ pub(crate) struct PendingTaggedLog { mod test { use crate::messages::processing::MessageContext; - use crate::protocol::{address::AztecAddress, traits::{Deserialize, FromField}}; + use crate::protocol::traits::Deserialize; use super::PendingTaggedLog; #[test] @@ -21,14 +21,12 @@ mod test { let tx_hash = 123; let unique_note_hashes = BoundedVec::from_array([4, 5]); let first_nullifier = 6; - let recipient = AztecAddress::from_field(789); let pending_log = PendingTaggedLog { log, context: MessageContext { tx_hash, unique_note_hashes_in_tx: unique_note_hashes, first_nullifier_in_tx: first_nullifier, - recipient, }, }; @@ -119,7 +117,6 @@ mod test { 0x0000000000000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000002, 0x0000000000000000000000000000000000000000000000000000000000000006, - 0x0000000000000000000000000000000000000000000000000000000000000315, ]; let deserialized = PendingTaggedLog::deserialize(serialized_pending_tagged_log_from_typescript); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr index 29044c297770..0622c2b2b6c8 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr @@ -1,5 +1,5 @@ -#[oracle(aztec_utl_tryAes128Decrypt)] -unconstrained fn try_aes128_decrypt_oracle( +#[oracle(aztec_utl_decryptAes128)] +unconstrained fn aes128_decrypt_oracle( ciphertext: BoundedVec, iv: [u8; 16], sym_key: [u8; 16], @@ -14,24 +14,28 @@ unconstrained fn try_aes128_decrypt_oracle( /// Note that we accept ciphertext as a BoundedVec, not as an array. This is because this function is typically used /// when processing logs and at that point we don't have comptime information about the length of the ciphertext as /// the log is not specific to any individual note. +// TODO(F-498): review naming consistency pub unconstrained fn try_aes128_decrypt( ciphertext: BoundedVec, iv: [u8; 16], sym_key: [u8; 16], ) -> Option> { - try_aes128_decrypt_oracle(ciphertext, iv, sym_key) + aes128_decrypt_oracle(ciphertext, iv, sym_key) } mod test { use crate::{ - messages::encryption::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe, + keys::ecdh_shared_secret::compute_app_siloed_shared_secret, + messages::encryption::aes128::derive_aes_symmetric_key_and_iv_from_shared_secret, utils::{array::subarray::subarray, point::point_from_x_coord}, }; + use crate::protocol::address::AztecAddress; use crate::test::helpers::test_environment::TestEnvironment; use super::try_aes128_decrypt; use poseidon::poseidon2::Poseidon2; use std::aes128::aes128_encrypt; + global CONTRACT_ADDRESS: AztecAddress = AztecAddress { inner: 42 }; global TEST_PLAINTEXT_LENGTH: u32 = 10; global TEST_CIPHERTEXT_LENGTH: u32 = 16; global TEST_PADDING_LENGTH: u32 = TEST_CIPHERTEXT_LENGTH - TEST_PLAINTEXT_LENGTH; @@ -41,11 +45,10 @@ mod test { let env = TestEnvironment::new(); env.utility_context(|_| { - let ciphertext_shared_secret = point_from_x_coord(1).unwrap(); + let shared_secret_point = point_from_x_coord(1).unwrap(); + let s_app = compute_app_siloed_shared_secret(shared_secret_point, CONTRACT_ADDRESS); - let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe::<1>( - ciphertext_shared_secret, - )[0]; + let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_shared_secret::<1>(s_app)[0]; let plaintext: [u8; TEST_PLAINTEXT_LENGTH] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; @@ -82,11 +85,10 @@ mod test { // (https://en.wikipedia.org/wiki/Message_authentication_code). We demonstrate this approach in // this test: we compute a MAC, include it in the plaintext, encrypt, and then verify that // decryption with a bad key produces a MAC mismatch. - let ciphertext_shared_secret = point_from_x_coord(1).unwrap(); + let shared_secret_point = point_from_x_coord(1).unwrap(); + let s_app = compute_app_siloed_shared_secret(shared_secret_point, CONTRACT_ADDRESS); - let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe::<1>( - ciphertext_shared_secret, - )[0]; + let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_shared_secret::<1>(s_app)[0]; let mac_preimage = 0x42; let mac = Poseidon2::hash([mac_preimage], 1); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr b/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr index 93b3b07d67b5..d813f2ab8392 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr @@ -2,39 +2,52 @@ use crate::protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}; /// Stores arbitrary information in a per-contract non-volatile database, which can later be retrieved with `load`. If /// data was already stored at this slot, it is overwritten. -pub unconstrained fn store(contract_address: AztecAddress, slot: Field, value: T) +// TODO(F-498): review naming consistency +pub unconstrained fn store(contract_address: AztecAddress, slot: Field, value: T, scope: AztecAddress) where T: Serialize, { let serialized = value.serialize(); - store_oracle(contract_address, slot, serialized); + set_capsule_oracle(contract_address, slot, serialized, scope); } -/// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns Option::none() -/// if nothing was stored at the given slot. -pub unconstrained fn load(contract_address: AztecAddress, slot: Field) -> Option +/// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns +/// Option::none() if nothing was stored at the given slot. +// TODO(F-498): review naming consistency +pub unconstrained fn load(contract_address: AztecAddress, slot: Field, scope: AztecAddress) -> Option where T: Deserialize, { - let serialized_option = load_oracle(contract_address, slot, ::N); + let serialized_option = get_capsule_oracle(contract_address, slot, ::N, scope); serialized_option.map(|arr| Deserialize::deserialize(arr)) } /// Deletes data in the per-contract non-volatile database. Does nothing if no data was present. -pub unconstrained fn delete(contract_address: AztecAddress, slot: Field) { - delete_oracle(contract_address, slot); +pub unconstrained fn delete(contract_address: AztecAddress, slot: Field, scope: AztecAddress) { + delete_oracle(contract_address, slot, scope); } /// Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data /// structures by avoiding repeated calls to `loadCapsule` and `storeCapsule`. Supports overlapping source and /// destination regions (which will result in the overlapped source values being overwritten). All copied slots must /// exist in the database (i.e. have been stored and not deleted) -pub unconstrained fn copy(contract_address: AztecAddress, src_slot: Field, dst_slot: Field, num_entries: u32) { - copy_oracle(contract_address, src_slot, dst_slot, num_entries); +pub unconstrained fn copy( + contract_address: AztecAddress, + src_slot: Field, + dst_slot: Field, + num_entries: u32, + scope: AztecAddress, +) { + copy_oracle(contract_address, src_slot, dst_slot, num_entries, scope); } -#[oracle(aztec_utl_storeCapsule)] -unconstrained fn store_oracle(contract_address: AztecAddress, slot: Field, values: [Field; N]) {} +#[oracle(aztec_utl_setCapsule)] +unconstrained fn set_capsule_oracle( + contract_address: AztecAddress, + slot: Field, + values: [Field; N], + scope: AztecAddress, +) {} /// We need to pass in `array_len` (the value of N) as a parameter to tell the oracle how many fields the response must /// have. @@ -43,18 +56,25 @@ unconstrained fn store_oracle(contract_address: AztecAddress, slot: /// require for the oracle resolver to know the shape of T (e.g. if T were a struct of 3 u32 values then the expected /// response shape would be 3 single items, whereas it were a struct containing `u32, [Field;10], u32` then the /// expected shape would be single, array, single.). Instead, we return the serialization and deserialize in Noir. -#[oracle(aztec_utl_loadCapsule)] -unconstrained fn load_oracle( +#[oracle(aztec_utl_getCapsule)] +unconstrained fn get_capsule_oracle( contract_address: AztecAddress, slot: Field, array_len: u32, + scope: AztecAddress, ) -> Option<[Field; N]> {} #[oracle(aztec_utl_deleteCapsule)] -unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field) {} +unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field, scope: AztecAddress) {} #[oracle(aztec_utl_copyCapsule)] -unconstrained fn copy_oracle(contract_address: AztecAddress, src_slot: Field, dst_slot: Field, num_entries: u32) {} +unconstrained fn copy_oracle( + contract_address: AztecAddress, + src_slot: Field, + dst_slot: Field, + num_entries: u32, + scope: AztecAddress, +) {} mod test { // These tests are sort of redundant since we already test the oracle implementation directly in TypeScript, but @@ -68,6 +88,7 @@ mod test { use crate::protocol::{address::AztecAddress, traits::{FromField, ToField}}; global SLOT: Field = 1; + global SCOPE: AztecAddress = AztecAddress { inner: 0xcafe }; #[test] unconstrained fn stores_and_loads() { @@ -76,9 +97,9 @@ mod test { let contract_address = context.this_address(); let value = MockStruct::new(5, 6); - store(contract_address, SLOT, value); + store(contract_address, SLOT, value, SCOPE); - assert_eq(load(contract_address, SLOT).unwrap(), value); + assert_eq(load(contract_address, SLOT, SCOPE).unwrap(), value); }); } @@ -89,12 +110,12 @@ mod test { let contract_address = context.this_address(); let value = MockStruct::new(5, 6); - store(contract_address, SLOT, value); + store(contract_address, SLOT, value, SCOPE); let new_value = MockStruct::new(7, 8); - store(contract_address, SLOT, new_value); + store(contract_address, SLOT, new_value, SCOPE); - assert_eq(load(contract_address, SLOT).unwrap(), new_value); + assert_eq(load(contract_address, SLOT, SCOPE).unwrap(), new_value); }); } @@ -104,7 +125,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let loaded_value: Option = load(contract_address, SLOT); + let loaded_value: Option = load(contract_address, SLOT, SCOPE); assert_eq(loaded_value, Option::none()); }); } @@ -116,10 +137,10 @@ mod test { let contract_address = context.this_address(); let value = MockStruct::new(5, 6); - store(contract_address, SLOT, value); - delete(contract_address, SLOT); + store(contract_address, SLOT, value, SCOPE); + delete(contract_address, SLOT, SCOPE); - let loaded_value: Option = load(contract_address, SLOT); + let loaded_value: Option = load(contract_address, SLOT, SCOPE); assert_eq(loaded_value, Option::none()); }); } @@ -130,8 +151,8 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - delete(contract_address, SLOT); - let loaded_value: Option = load(contract_address, SLOT); + delete(contract_address, SLOT, SCOPE); + let loaded_value: Option = load(contract_address, SLOT, SCOPE); assert_eq(loaded_value, Option::none()); }); } @@ -145,16 +166,16 @@ mod test { let src = 5; let values = [MockStruct::new(5, 6), MockStruct::new(7, 8), MockStruct::new(9, 10)]; - store(contract_address, src, values[0]); - store(contract_address, src + 1, values[1]); - store(contract_address, src + 2, values[2]); + store(contract_address, src, values[0], SCOPE); + store(contract_address, src + 1, values[1], SCOPE); + store(contract_address, src + 2, values[2], SCOPE); let dst = 10; - copy(contract_address, src, dst, 3); + copy(contract_address, src, dst, 3, SCOPE); - assert_eq(load(contract_address, dst).unwrap(), values[0]); - assert_eq(load(contract_address, dst + 1).unwrap(), values[1]); - assert_eq(load(contract_address, dst + 2).unwrap(), values[2]); + assert_eq(load(contract_address, dst, SCOPE).unwrap(), values[0]); + assert_eq(load(contract_address, dst + 1, SCOPE).unwrap(), values[1]); + assert_eq(load(contract_address, dst + 2, SCOPE).unwrap(), values[2]); }); } @@ -167,21 +188,21 @@ mod test { let src = 1; let values = [MockStruct::new(5, 6), MockStruct::new(7, 8), MockStruct::new(9, 10)]; - store(contract_address, src, values[0]); - store(contract_address, src + 1, values[1]); - store(contract_address, src + 2, values[2]); + store(contract_address, src, values[0], SCOPE); + store(contract_address, src + 1, values[1], SCOPE); + store(contract_address, src + 2, values[2], SCOPE); let dst = 2; - copy(contract_address, src, dst, 3); + copy(contract_address, src, dst, 3, SCOPE); - assert_eq(load(contract_address, dst).unwrap(), values[0]); - assert_eq(load(contract_address, dst + 1).unwrap(), values[1]); - assert_eq(load(contract_address, dst + 2).unwrap(), values[2]); + assert_eq(load(contract_address, dst, SCOPE).unwrap(), values[0]); + assert_eq(load(contract_address, dst + 1, SCOPE).unwrap(), values[1]); + assert_eq(load(contract_address, dst + 2, SCOPE).unwrap(), values[2]); // src[1] and src[2] should have been overwritten since they are also dst[0] and dst[1] - assert_eq(load(contract_address, src).unwrap(), values[0]); // src[0] (unchanged) - assert_eq(load(contract_address, src + 1).unwrap(), values[0]); // dst[0] - assert_eq(load(contract_address, src + 2).unwrap(), values[1]); // dst[1] + assert_eq(load(contract_address, src, SCOPE).unwrap(), values[0]); // src[0] (unchanged) + assert_eq(load(contract_address, src + 1, SCOPE).unwrap(), values[0]); // dst[0] + assert_eq(load(contract_address, src + 2, SCOPE).unwrap(), values[1]); // dst[1] }); } @@ -194,21 +215,21 @@ mod test { let src = 2; let values = [MockStruct::new(5, 6), MockStruct::new(7, 8), MockStruct::new(9, 10)]; - store(contract_address, src, values[0]); - store(contract_address, src + 1, values[1]); - store(contract_address, src + 2, values[2]); + store(contract_address, src, values[0], SCOPE); + store(contract_address, src + 1, values[1], SCOPE); + store(contract_address, src + 2, values[2], SCOPE); let dst = 1; - copy(contract_address, src, dst, 3); + copy(contract_address, src, dst, 3, SCOPE); - assert_eq(load(contract_address, dst).unwrap(), values[0]); - assert_eq(load(contract_address, dst + 1).unwrap(), values[1]); - assert_eq(load(contract_address, dst + 2).unwrap(), values[2]); + assert_eq(load(contract_address, dst, SCOPE).unwrap(), values[0]); + assert_eq(load(contract_address, dst + 1, SCOPE).unwrap(), values[1]); + assert_eq(load(contract_address, dst + 2, SCOPE).unwrap(), values[2]); // src[0] and src[1] should have been overwritten since they are also dst[1] and dst[2] - assert_eq(load(contract_address, src).unwrap(), values[1]); // dst[1] - assert_eq(load(contract_address, src + 1).unwrap(), values[2]); // dst[2] - assert_eq(load(contract_address, src + 2).unwrap(), values[2]); // src[2] (unchanged) + assert_eq(load(contract_address, src, SCOPE).unwrap(), values[1]); // dst[1] + assert_eq(load(contract_address, src + 1, SCOPE).unwrap(), values[2]); // dst[2] + assert_eq(load(contract_address, src + 2, SCOPE).unwrap(), values[2]); // src[2] (unchanged) }); } @@ -218,7 +239,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - copy(contract_address, SLOT, SLOT, 1); + copy(contract_address, SLOT, SLOT, 1, SCOPE); }); } @@ -230,7 +251,7 @@ mod test { let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); let value = MockStruct::new(5, 6); - store(other_contract_address, SLOT, value); + store(other_contract_address, SLOT, value, SCOPE); }); } @@ -241,7 +262,7 @@ mod test { let contract_address = context.this_address(); let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); - let _: Option = load(other_contract_address, SLOT); + let _: Option = load(other_contract_address, SLOT, SCOPE); }); } @@ -252,7 +273,7 @@ mod test { let contract_address = context.this_address(); let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); - delete(other_contract_address, SLOT); + delete(other_contract_address, SLOT, SCOPE); }); } @@ -263,7 +284,7 @@ mod test { let contract_address = context.this_address(); let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); - copy(other_contract_address, SLOT, SLOT, 0); + copy(other_contract_address, SLOT, SLOT, 0, SCOPE); }); } } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/contract_sync.nr b/noir-projects/aztec-nr/aztec/src/oracle/contract_sync.nr index 98017107ab0e..8c9198bf4b11 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/contract_sync.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/contract_sync.nr @@ -1,7 +1,7 @@ use crate::protocol::address::AztecAddress; -#[oracle(aztec_utl_invalidateContractSyncCache)] -unconstrained fn invalidate_contract_sync_cache_oracle( +#[oracle(aztec_utl_setContractSyncCacheInvalid)] +unconstrained fn set_contract_sync_cache_invalid_oracle( contract_address: AztecAddress, scopes: BoundedVec, ) {} @@ -10,9 +10,9 @@ unconstrained fn invalidate_contract_sync_cache_oracle( /// /// Call this after writing data (e.g. offchain messages) that the contract's `sync_state` function needs to discover. /// Without invalidation, the sync cache would skip re-running `sync_state` until the next block. -pub unconstrained fn invalidate_contract_sync_cache( +pub unconstrained fn set_contract_sync_cache_invalid( contract_address: AztecAddress, scopes: BoundedVec, ) { - invalidate_contract_sync_cache_oracle(contract_address, scopes); + set_contract_sync_cache_invalid_oracle(contract_address, scopes); } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/execution_cache.nr b/noir-projects/aztec-nr/aztec/src/oracle/execution_cache.nr index 1a56f7724fa5..aab0d16d555c 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/execution_cache.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/execution_cache.nr @@ -1,20 +1,22 @@ /// Stores values represented as slice in execution cache to be later obtained by its hash. +// TODO(F-498): review naming consistency pub fn store(values: [Field; N], hash: Field) { // Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to // call. When loading the values, however, the caller must check that the values are indeed the preimage. - unsafe { store_in_execution_cache_oracle_wrapper(values, hash) }; + unsafe { set_hash_preimage_oracle_wrapper(values, hash) }; } -unconstrained fn store_in_execution_cache_oracle_wrapper(values: [Field; N], hash: Field) { - store_in_execution_cache_oracle(values, hash); +unconstrained fn set_hash_preimage_oracle_wrapper(values: [Field; N], hash: Field) { + set_hash_preimage_oracle(values, hash); } +// TODO(F-498): review naming consistency pub unconstrained fn load(hash: Field) -> [Field; N] { - load_from_execution_cache_oracle(hash) + get_hash_preimage_oracle(hash) } -#[oracle(aztec_prv_storeInExecutionCache)] -unconstrained fn store_in_execution_cache_oracle(_values: [Field; N], _hash: Field) {} +#[oracle(aztec_prv_setHashPreimage)] +unconstrained fn set_hash_preimage_oracle(_values: [Field; N], _hash: Field) {} -#[oracle(aztec_prv_loadFromExecutionCache)] -unconstrained fn load_from_execution_cache_oracle(_hash: Field) -> [Field; N] {} +#[oracle(aztec_prv_getHashPreimage)] +unconstrained fn get_hash_preimage_oracle(_hash: Field) -> [Field; N] {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/keys.nr b/noir-projects/aztec-nr/aztec/src/oracle/keys.nr index 3c234ca34524..6834a04975ac 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/keys.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/keys.nr @@ -4,17 +4,19 @@ use crate::protocol::{ public_keys::{IvpkM, NpkM, OvpkM, PublicKeys, TpkM}, }; +// TODO(F-498): review naming consistency pub unconstrained fn get_public_keys_and_partial_address(address: AztecAddress) -> (PublicKeys, PartialAddress) { try_get_public_keys_and_partial_address(address).expect(f"Public keys not registered for account {address}") } -#[oracle(aztec_utl_tryGetPublicKeysAndPartialAddress)] -unconstrained fn try_get_public_keys_and_partial_address_oracle(_address: AztecAddress) -> Option<[Field; 13]> {} +#[oracle(aztec_utl_getPublicKeysAndPartialAddress)] +unconstrained fn get_public_keys_and_partial_address_oracle(_address: AztecAddress) -> Option<[Field; 13]> {} +// TODO(F-498): review naming consistency pub unconstrained fn try_get_public_keys_and_partial_address( address: AztecAddress, ) -> Option<(PublicKeys, PartialAddress)> { - try_get_public_keys_and_partial_address_oracle(address).map(|result: [Field; 13]| { + get_public_keys_and_partial_address_oracle(address).map(|result: [Field; 13]| { let keys = PublicKeys { npk_m: NpkM { inner: Point { x: result[0], y: result[1], is_infinite: result[2] != 0 } }, ivpk_m: IvpkM { inner: Point { x: result[3], y: result[4], is_infinite: result[5] != 0 } }, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index ee36e2c2486a..ac66b939dcaf 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -2,12 +2,13 @@ use crate::protocol::address::AztecAddress; /// Finds new private logs that may have been sent to all registered accounts in PXE in the current contract and makes /// them available for later processing in Noir by storing them in a capsule array. -pub unconstrained fn fetch_tagged_logs(pending_tagged_log_array_base_slot: Field) { - fetch_tagged_logs_oracle(pending_tagged_log_array_base_slot); +// TODO(F-498): review naming consistency +pub unconstrained fn fetch_tagged_logs(pending_tagged_log_array_base_slot: Field, scope: AztecAddress) { + get_pending_tagged_logs_oracle(pending_tagged_log_array_base_slot, scope); } -#[oracle(aztec_utl_fetchTaggedLogs)] -unconstrained fn fetch_tagged_logs_oracle(pending_tagged_log_array_base_slot: Field) {} +#[oracle(aztec_utl_getPendingTaggedLogs)] +unconstrained fn get_pending_tagged_logs_oracle(pending_tagged_log_array_base_slot: Field, scope: AztecAddress) {} // This must be a single oracle and not one for notes and one for events because the entire point is to validate all // notes and events in one go, minimizing node round-trips. @@ -17,6 +18,7 @@ pub(crate) unconstrained fn validate_and_store_enqueued_notes_and_events( event_validation_requests_array_base_slot: Field, max_note_packed_len: Field, max_event_serialized_len: Field, + scope: AztecAddress, ) { validate_and_store_enqueued_notes_and_events_oracle( contract_address, @@ -24,6 +26,7 @@ pub(crate) unconstrained fn validate_and_store_enqueued_notes_and_events( event_validation_requests_array_base_slot, max_note_packed_len, max_event_serialized_len, + scope, ); } @@ -34,42 +37,49 @@ unconstrained fn validate_and_store_enqueued_notes_and_events_oracle( event_validation_requests_array_base_slot: Field, max_note_packed_len: Field, max_event_serialized_len: Field, + scope: AztecAddress, ) {} -pub(crate) unconstrained fn bulk_retrieve_logs( +pub(crate) unconstrained fn get_logs_by_tag( contract_address: AztecAddress, log_retrieval_requests_array_base_slot: Field, log_retrieval_responses_array_base_slot: Field, + scope: AztecAddress, ) { - bulk_retrieve_logs_oracle( + get_logs_by_tag_oracle( contract_address, log_retrieval_requests_array_base_slot, log_retrieval_responses_array_base_slot, + scope, ); } -#[oracle(aztec_utl_bulkRetrieveLogs)] -unconstrained fn bulk_retrieve_logs_oracle( +#[oracle(aztec_utl_getLogsByTag)] +unconstrained fn get_logs_by_tag_oracle( contract_address: AztecAddress, log_retrieval_requests_array_base_slot: Field, log_retrieval_responses_array_base_slot: Field, + scope: AztecAddress, ) {} -pub(crate) unconstrained fn resolve_message_contexts( +pub(crate) unconstrained fn get_message_contexts_by_tx_hash( contract_address: AztecAddress, message_context_requests_array_base_slot: Field, message_context_responses_array_base_slot: Field, + scope: AztecAddress, ) { - resolve_message_contexts_oracle( + get_message_contexts_by_tx_hash_oracle( contract_address, message_context_requests_array_base_slot, message_context_responses_array_base_slot, + scope, ); } -#[oracle(aztec_utl_utilityResolveMessageContexts)] -unconstrained fn resolve_message_contexts_oracle( +#[oracle(aztec_utl_getMessageContextsByTxHash)] +unconstrained fn get_message_contexts_by_tx_hash_oracle( contract_address: AztecAddress, message_context_requests_array_base_slot: Field, message_context_responses_array_base_slot: Field, + scope: AztecAddress, ) {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/nullifiers.nr b/noir-projects/aztec-nr/aztec/src/oracle/nullifiers.nr index 7c8e64a85574..a083cf372ed1 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/nullifiers.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/nullifiers.nr @@ -33,9 +33,10 @@ unconstrained fn is_nullifier_pending_oracle(_inner_nullifier: Field, _contract_ /// nullifier, but a `false` value should not be relied upon since other transactions may emit this nullifier before /// the current transaction is included in a block. While this might seem of little use at first, certain design /// patterns benefit from this abstraction (see e.g. `PrivateMutable`). +// TODO(F-498): review naming consistency pub unconstrained fn check_nullifier_exists(inner_nullifier: Field) -> bool { - check_nullifier_exists_oracle(inner_nullifier) + does_nullifier_exist_oracle(inner_nullifier) } -#[oracle(aztec_utl_checkNullifierExists)] -unconstrained fn check_nullifier_exists_oracle(_inner_nullifier: Field) -> bool {} +#[oracle(aztec_utl_doesNullifierExist)] +unconstrained fn does_nullifier_exist_oracle(_inner_nullifier: Field) -> bool {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr b/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr index 055aa3048d69..396451d0559e 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr @@ -2,17 +2,17 @@ /// /// The check is unconstrained and the only purpose of it is to fail early in case of calldata overflow or a bug in /// calldata hashing. -pub(crate) fn validate_public_calldata(calldata_hash: Field) { +pub(crate) fn assert_valid_public_call_data(calldata_hash: Field) { // Safety: This oracle call returns nothing: we only call it for its side effects (validating the calldata). // It is therefore always safe to call. unsafe { - validate_public_calldata_wrapper(calldata_hash) + assert_valid_public_call_data_oracle_wrapper(calldata_hash) } } -unconstrained fn validate_public_calldata_wrapper(calldata_hash: Field) { - validate_public_calldata_oracle(calldata_hash) +unconstrained fn assert_valid_public_call_data_oracle_wrapper(calldata_hash: Field) { + assert_valid_public_call_data_oracle(calldata_hash) } -#[oracle(aztec_prv_validatePublicCalldata)] -unconstrained fn validate_public_calldata_oracle(_calldata_hash: Field) {} +#[oracle(aztec_prv_assertValidPublicCalldata)] +unconstrained fn assert_valid_public_call_data_oracle(_calldata_hash: Field) {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr index a040afdb98da..10580939f5de 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr @@ -1,16 +1,32 @@ -use crate::protocol::{address::aztec_address::AztecAddress, point::Point}; +use crate::protocol::address::aztec_address::AztecAddress; +use crate::protocol::point::Point; -// TODO(#12656): return an app-siloed secret + document this #[oracle(aztec_utl_getSharedSecret)] -unconstrained fn get_shared_secret_oracle(address: AztecAddress, ephPk: Point) -> Point {} +unconstrained fn get_shared_secret_oracle( + address: AztecAddress, + ephPk: Point, + contract_address: AztecAddress, +) -> Field {} /// Returns an app-siloed shared secret between `address` and someone who knows the secret key behind an ephemeral -/// public key `ephPk`. The app-siloing means that contracts cannot retrieve secrets that belong to other contracts, -/// and therefore cannot e.g. decrypt their messages. This is an important security consideration given that both the -/// `address` and `ephPk` are public information. +/// public key `ephPk`. /// -/// The shared secret `S` is computed as: `let S = (ivsk + h) * ephPk` where `ivsk + h` is the 'preaddress' i.e. the -/// preimage of the address, also called the address secret. TODO(#12656): app-silo this secret -pub unconstrained fn get_shared_secret(address: AztecAddress, ephPk: Point) -> Point { - get_shared_secret_oracle(address, ephPk) +/// The returned value is a Field `s_app`, computed as: +/// +/// ```text +/// S = address_secret * ephPk (raw ECDH point) +/// s_app = h(DOM_SEP, S.x, S.y, contract) (app-siloed scalar) +/// ``` +/// +/// where `contract` is the address of the calling contract. The oracle host validates this matches its execution +/// context. +/// +/// Without app-siloing, a malicious contract could call this oracle with public information (address, ephPk) and +/// obtain the same raw secret as the legitimate contract, enabling cross-contract decryption. By including the +/// contract address in the hash, each contract receives a different `s_app`, preventing this attack. +/// +/// Callers derive indexed subkeys from `s_app` via +/// [`derive_shared_secret_subkey`](crate::keys::ecdh_shared_secret::derive_shared_secret_subkey). +pub unconstrained fn get_shared_secret(address: AztecAddress, ephPk: Point, contract_address: AztecAddress) -> Field { + get_shared_secret_oracle(address, ephPk, contract_address) } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/storage.nr b/noir-projects/aztec-nr/aztec/src/oracle/storage.nr index fef8abe759c8..80b8297420c4 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/storage.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/storage.nr @@ -1,21 +1,23 @@ use crate::protocol::{abis::block_header::BlockHeader, address::AztecAddress, traits::{Hash, Packable, ToField}}; -#[oracle(aztec_utl_storageRead)] -unconstrained fn storage_read_oracle( +#[oracle(aztec_utl_getFromPublicStorage)] +unconstrained fn get_from_public_storage_oracle( block_hash: Field, address: Field, storage_slot: Field, length: u32, ) -> [Field; N] {} +// TODO(F-498): review naming consistency pub unconstrained fn raw_storage_read( block_hash_to_read_from: Field, address: AztecAddress, storage_slot: Field, ) -> [Field; N] { - storage_read_oracle(block_hash_to_read_from, address.to_field(), storage_slot, N) + get_from_public_storage_oracle(block_hash_to_read_from, address.to_field(), storage_slot, N) } +// TODO(F-498): review naming consistency pub unconstrained fn storage_read(header_to_read_from: BlockHeader, address: AztecAddress, storage_slot: Field) -> T where T: Packable, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/tx_phase.nr b/noir-projects/aztec-nr/aztec/src/oracle/tx_phase.nr index 207e2fb26030..baa45f2b21f0 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/tx_phase.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/tx_phase.nr @@ -9,8 +9,8 @@ pub(crate) fn notify_revertible_phase_start(counter: u32) { } /// Returns whether a side effect counter falls in the revertible phase of the transaction. -pub(crate) unconstrained fn in_revertible_phase(current_counter: u32) -> bool { - in_revertible_phase_oracle(current_counter) +pub(crate) unconstrained fn is_execution_in_revertible_phase(current_counter: u32) -> bool { + is_execution_in_revertible_phase_oracle(current_counter) } unconstrained fn notify_revertible_phase_start_oracle_wrapper(counter: u32) { @@ -20,5 +20,5 @@ unconstrained fn notify_revertible_phase_start_oracle_wrapper(counter: u32) { #[oracle(aztec_prv_notifyRevertiblePhaseStart)] unconstrained fn notify_revertible_phase_start_oracle(_counter: u32) {} -#[oracle(aztec_prv_inRevertiblePhase)] -unconstrained fn in_revertible_phase_oracle(current_counter: u32) -> bool {} +#[oracle(aztec_prv_isExecutionInRevertiblePhase)] +unconstrained fn is_execution_in_revertible_phase_oracle(current_counter: u32) -> bool {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/version.nr b/noir-projects/aztec-nr/aztec/src/oracle/version.nr index bb733bb42fae..0e9c3ce1a270 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/version.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/version.nr @@ -4,7 +4,7 @@ /// /// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is /// called and if the oracle version is incompatible an error is thrown. -pub global ORACLE_VERSION: Field = 18; +pub global ORACLE_VERSION: Field = 21; /// Asserts that the version of the oracle is compatible with the version expected by the contract. pub fn assert_compatible_oracle_version() { @@ -30,7 +30,7 @@ mod test { assert_compatible_oracle_version_oracle(ORACLE_VERSION); } - #[test(should_fail_with = "Incompatible oracle version. TXE is using version")] + #[test(should_fail_with = "Incompatible aztec cli version:")] unconstrained fn incompatible_oracle_version() { let arbitrary_incorrect_version = 318183437; assert_compatible_oracle_version_oracle(arbitrary_incorrect_version); diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr index ccef7768325d..1e6d20443abd 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr @@ -944,14 +944,17 @@ impl TestEnvironment { // as events are properly scoped by recipient, but notes use a global scope instead. We therefore simply set // the zero address as the recipient if one is not supplied, which PXE accepts as a scope despite this not // being a registered account. - let message_context = MessageContext { - tx_hash, - unique_note_hashes_in_tx, - first_nullifier_in_tx: nullifiers_in_tx.get(0), - recipient: recipient.unwrap_or(AztecAddress::zero()), - }; + let message_context = + MessageContext { tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx: nullifiers_in_tx.get(0) }; + + // The scope controls which PXE account can see discovered notes/events. We use the zero address as the + // default scope if no recipient is supplied, which PXE accepts despite this not being a registered account. + // + // TODO(F-489, @mverzilli): we should revisit how we treat scope in test env, it seems we're treating it too + // implicitly. + let scope = recipient.unwrap_or(AztecAddress::zero()); - // Both private and utility functions perform message processing . We do it in an utility context here as that + // Both private and utility functions perform message processing. We do it in an utility context here as that // one is more lightweight and does not create new blocks, which also allows for `discover_note` and // `discover_event` to be called repeatedly with multiple messages from the same transaction. self.utility_context_opts(UtilityContextOptions { contract_address }, |context| { @@ -962,9 +965,10 @@ impl TestEnvironment { process_custom_message, message_plaintext, message_context, + scope, ); - validate_and_store_enqueued_notes_and_events(context.this_address()); + validate_and_store_enqueued_notes_and_events(context.this_address(), scope); }); } } diff --git a/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr b/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr index 14cc838857f1..e1651f527d76 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr @@ -173,7 +173,7 @@ mod test { // Mock the oracle to return a non-zero hint/packed value let value_packed = MockStruct { a: 1, b: 1 }.pack(); - let _ = OracleMock::mock("aztec_utl_storageRead") + let _ = OracleMock::mock("aztec_utl_getFromPublicStorage") .with_params((block_header.hash(), address.to_field(), STORAGE_SLOT, value_packed.len())) .returns(value_packed) .times(1); diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index 0c9ea6fcd8e8..d075a6e5491d 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -3,13 +3,19 @@ use aztec::{ history::nullifier::assert_nullifier_existed_by, keys::getters::{get_nhk_app, get_public_keys, try_get_public_keys}, macros::notes::custom_note, - messages::logs::partial_note::compute_partial_note_private_content_log, + messages::{ + logs::partial_note::encode_partial_note_private_message, + message_delivery::{do_private_message_delivery, MessageDelivery}, + }, note::{note_interface::{NoteHash, NoteType}, utils::compute_note_nullifier}, oracle::random::random, protocol::{ address::AztecAddress, - constants::{DOM_SEP__NOTE_HASH, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, PRIVATE_LOG_SIZE_IN_FIELDS}, - hash::{compute_siloed_nullifier, poseidon2_hash_with_separator}, + constants::{ + DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__NOTE_HASH, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, + PRIVATE_LOG_CIPHERTEXT_LEN, + }, + hash::{compute_log_tag, compute_siloed_nullifier, poseidon2_hash_with_separator}, traits::{Deserialize, FromField, Hash, Packable, Serialize, ToField}, }, }; @@ -116,17 +122,13 @@ impl UintNote { // - other contracts cannot impersonate us and emit logs with the same tag due to public log siloing let private_log_content = UintPartialNotePrivateLogContent {}; - let encrypted_log = compute_partial_note_private_content_log( - private_log_content, - owner, - randomness, + do_private_message_delivery( + context, + || encode_partial_note_private_message(private_log_content, owner, randomness, commitment), + Option::none(), recipient, - commitment, + MessageDelivery.ONCHAIN_UNCONSTRAINED, ); - // Regardless of the original content size, the log is padded with random bytes up to - // `PRIVATE_LOG_SIZE_IN_FIELDS` to prevent leaking information about the actual size. - let length = encrypted_log.len(); - context.emit_private_log(encrypted_log, length); let partial_note = PartialUintNote { commitment }; @@ -170,7 +172,7 @@ pub struct PartialUintNote { } // docs:end:partial_uint_note_def -global NOTE_COMPLETION_LOG_LENGTH: u32 = 3; +global NOTE_COMPLETION_PAYLOAD_LENGTH: u32 = 2; impl PartialUintNote { /// Completes the partial note, creating a new note that can be used like any other UintNote. @@ -193,11 +195,12 @@ impl PartialUintNote { // We need to do two things: // - emit a public log containing the public fields (the storage slot and value). The contract will later find - // it by searching for the expected tag (which is simply the partial note commitment). + // it by searching for the domain-separated commitment as the tag. // - insert the completion note hash (i.e. the hash of the note) into the note hash tree. This is typically // only done in private to hide the preimage of the hash that is inserted, but completed partial notes are // inserted in public as the public values are provided and the note hash computed. - context.emit_public_log(self.compute_note_completion_log(storage_slot, value)); + let log_tag = compute_log_tag(self.commitment, DOM_SEP__NOTE_COMPLETION_LOG_TAG); + context.emit_public_log_unsafe(log_tag, [storage_slot, value.to_field()]); context.push_note_hash(self.compute_complete_note_hash(storage_slot, value)); } @@ -224,15 +227,13 @@ impl PartialUintNote { // We need to do two things: // - emit an unencrypted log containing the public fields (the storage slot and value) via the private log - // channel. The contract will later find it by searching for the expected tag (which is simply the partial note - // commitment). + // channel. The contract will later find it by searching for the domain-separated commitment as the tag. // - insert the completion note hash (i.e. the hash of the note) into the note hash tree. This is typically // only done in private to hide the preimage of the hash that is inserted, but completed partial notes are // inserted in public as the public values are provided and the note hash computed. - context.emit_private_log( - self.compute_note_completion_log_padded_for_private_log(storage_slot, value), - NOTE_COMPLETION_LOG_LENGTH, - ); + let log_tag = compute_log_tag(self.commitment, DOM_SEP__NOTE_COMPLETION_LOG_TAG); + let padded_payload = self.compute_note_completion_payload_padded_for_private_log(storage_slot, value); + context.emit_private_log_unsafe(log_tag, padded_payload, NOTE_COMPLETION_PAYLOAD_LENGTH); context.push_note_hash(self.compute_complete_note_hash(storage_slot, value)); } @@ -248,20 +249,13 @@ impl PartialUintNote { ) } - fn compute_note_completion_log(self, storage_slot: Field, value: u128) -> [Field; NOTE_COMPLETION_LOG_LENGTH] { - // The first field of this log must be the tag that the recipient of the partial note private field logs - // expects, which is equal to the partial note commitment. - [self.commitment, storage_slot, value.to_field()] - } - - fn compute_note_completion_log_padded_for_private_log( - self, + fn compute_note_completion_payload_padded_for_private_log( + _self: Self, storage_slot: Field, value: u128, - ) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] { - let note_completion_log = self.compute_note_completion_log(storage_slot, value); - let padding = [0; PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_COMPLETION_LOG_LENGTH]; - note_completion_log.concat(padding) + ) -> [Field; PRIVATE_LOG_CIPHERTEXT_LEN] { + let payload = [storage_slot, value.to_field()]; + payload.concat([0; PRIVATE_LOG_CIPHERTEXT_LEN - NOTE_COMPLETION_PAYLOAD_LENGTH]) } // docs:start:compute_complete_note_hash diff --git a/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr b/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr index a2dad7d1e64d..0542cdf24f09 100644 --- a/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr +++ b/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr @@ -2,13 +2,19 @@ use aztec::{ context::{PrivateContext, PublicContext}, keys::getters::{get_nhk_app, get_public_keys, try_get_public_keys}, macros::notes::custom_note, - messages::logs::partial_note::compute_partial_note_private_content_log, + messages::{ + logs::partial_note::encode_partial_note_private_message, + message_delivery::{do_private_message_delivery, MessageDelivery}, + }, note::{note_interface::{NoteHash, NoteType}, utils::compute_note_nullifier}, oracle::random::random, protocol::{ address::AztecAddress, - constants::{DOM_SEP__NOTE_HASH, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT}, - hash::poseidon2_hash_with_separator, + constants::{ + DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__NOTE_HASH, + DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, + }, + hash::{compute_log_tag, poseidon2_hash_with_separator}, traits::{Deserialize, Hash, Packable, Serialize, ToField}, }, }; @@ -117,17 +123,20 @@ impl NFTNote { // - other contracts cannot impersonate us and emit logs with the same tag due to public log siloing let private_log_content = NFTPartialNotePrivateLogContent {}; - let encrypted_log = compute_partial_note_private_content_log( - private_log_content, - owner, - randomness, + do_private_message_delivery( + context, + || { + encode_partial_note_private_message( + private_log_content, + owner, + randomness, + commitment, + ) + }, + Option::none(), recipient, - commitment, + MessageDelivery.ONCHAIN_UNCONSTRAINED, ); - // Regardless of the original content size, the log is padded with random bytes up to - // `PRIVATE_LOG_SIZE_IN_FIELDS` to prevent leaking information about the actual size. - let length = encrypted_log.len(); - context.emit_private_log(encrypted_log, length); let partial_note = PartialNFTNote { commitment }; @@ -195,11 +204,12 @@ impl PartialNFTNote { // We need to do two things: // - emit a public log containing the public fields (the storage slot and token id). The contract will later - // find it by searching for the expected tag (which is simply the partial note commitment). + // find it by searching for the domain-separated commitment as the tag. // - insert the completion note hash (i.e. the hash of the note) into the note hash tree. This is typically // only done in private to hide the preimage of the hash that is inserted, but completed partial notes are // inserted in public as the public values are provided and the note hash computed. - context.emit_public_log(self.compute_note_completion_log(storage_slot, token_id)); + let log_tag = compute_log_tag(self.commitment, DOM_SEP__NOTE_COMPLETION_LOG_TAG); + context.emit_public_log_unsafe(log_tag, [storage_slot, token_id]); context.push_note_hash(self.compute_complete_note_hash(storage_slot, token_id)); } @@ -215,13 +225,6 @@ impl PartialNFTNote { ) } - fn compute_note_completion_log(self, storage_slot: Field, token_id: Field) -> [Field; 3] { - // The first field of this log must be the tag that the recipient of the partial note private field logs - // expects, which is equal to the partial note commitment. The storage slot is included as the first public - // value so that note discovery can extract it. - [self.commitment, storage_slot, token_id] - } - fn compute_complete_note_hash(self, storage_slot: Field, token_id: Field) -> Field { // Here we finalize the note hash by including the (public) storage slot and token id into the partial note // commitment. Note that we use the same separator as we used for the first round of poseidon - this is not diff --git a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr index 15d680e0909c..5fb0faa05d34 100644 --- a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr @@ -277,7 +277,7 @@ pub contract TokenBlacklist { tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, - recipient: AztecAddress, + scope: AztecAddress, ) { let note = TransparentNote { amount, secret_hash }; let storage_slot = TokenBlacklist::storage_layout().pending_shields.slot; @@ -291,7 +291,6 @@ pub contract TokenBlacklist { tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx, - recipient, _compute_note_hash, _compute_note_nullifier, AztecAddress::zero(), @@ -299,10 +298,11 @@ pub contract TokenBlacklist { TRANSPARENT_NOTE_RANDOMNESS, note_type_id, packed_note, + scope, ); // At this point, the note is pending validation and storage in the database. We must call // validate_and_store_enqueued_notes_and_events to complete that process. - validate_and_store_enqueued_notes_and_events(contract_address); + validate_and_store_enqueued_notes_and_events(contract_address, scope); } } diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr index 762b984311b2..bd63e645f549 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr @@ -2,27 +2,40 @@ use crate::protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}; /// Stores arbitrary information in a per-contract non-volatile database, which can later be retrieved with `load`. If /// data was already stored at this slot, it is overwritten. -pub unconstrained fn store(contract_address: AztecAddress, slot: Field, value: T) +pub unconstrained fn store( + contract_address: AztecAddress, + slot: Field, + value: T, + scope: AztecAddress, +) where T: Serialize, { let serialized = value.serialize(); - store_oracle(contract_address, slot, serialized); + store_oracle(contract_address, slot, serialized, scope); } -/// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns Option::none() -/// if nothing was stored at the given slot. -pub unconstrained fn load(contract_address: AztecAddress, slot: Field) -> Option +/// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns +/// Option::none() if nothing was stored at the given slot. +pub unconstrained fn load( + contract_address: AztecAddress, + slot: Field, + scope: AztecAddress, +) -> Option where T: Deserialize, { - let serialized_option = load_oracle(contract_address, slot, ::N); + let serialized_option = load_oracle(contract_address, slot, ::N, scope); serialized_option.map(|arr| Deserialize::deserialize(arr)) } /// Deletes data in the per-contract non-volatile database. Does nothing if no data was present. -pub unconstrained fn delete(contract_address: AztecAddress, slot: Field) { - delete_oracle(contract_address, slot); +pub unconstrained fn delete( + contract_address: AztecAddress, + slot: Field, + scope: AztecAddress, +) { + delete_oracle(contract_address, slot, scope); } /// Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data @@ -34,15 +47,18 @@ pub unconstrained fn copy( src_slot: Field, dst_slot: Field, num_entries: u32, + scope: AztecAddress, ) { - copy_oracle(contract_address, src_slot, dst_slot, num_entries); + copy_oracle(contract_address, src_slot, dst_slot, num_entries, scope); } -#[oracle(aztec_utl_storeCapsule)] +#[oracle(aztec_utl_setCapsule)] +// TODO(F-498): review naming consistency unconstrained fn store_oracle( contract_address: AztecAddress, slot: Field, values: [Field; N], + scope: AztecAddress, ) {} /// We need to pass in `array_len` (the value of N) as a parameter to tell the oracle how many fields the response must @@ -52,15 +68,17 @@ unconstrained fn store_oracle( /// require for the oracle resolver to know the shape of T (e.g. if T were a struct of 3 u32 values then the expected /// response shape would be 3 single items, whereas it were a struct containing `u32, [Field;10], u32` then the /// expected shape would be single, array, single.). Instead, we return the serialization and deserialize in Noir. -#[oracle(aztec_utl_loadCapsule)] +// TODO(F-498): review naming consistency +#[oracle(aztec_utl_getCapsule)] unconstrained fn load_oracle( contract_address: AztecAddress, slot: Field, array_len: u32, + scope: AztecAddress, ) -> Option<[Field; N]> {} #[oracle(aztec_utl_deleteCapsule)] -unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field) {} +unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field, scope: AztecAddress) {} #[oracle(aztec_utl_copyCapsule)] unconstrained fn copy_oracle( @@ -68,4 +86,5 @@ unconstrained fn copy_oracle( src_slot: Field, dst_slot: Field, num_entries: u32, + scope: AztecAddress, ) {} diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/execution_cache.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/execution_cache.nr index 5ba553baa659..ccca9fdc1d31 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/execution_cache.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/execution_cache.nr @@ -16,8 +16,10 @@ pub unconstrained fn load(hash: Field) -> [Field; N] { load_from_execution_cache_oracle(hash) } -#[oracle(aztec_prv_storeInExecutionCache)] +// TODO(F-498): review naming consistency +#[oracle(aztec_prv_setHashPreimage)] unconstrained fn store_in_execution_cache_oracle(_values: [Field; N], _hash: Field) {} -#[oracle(aztec_prv_loadFromExecutionCache)] +// TODO(F-498): review naming consistency +#[oracle(aztec_prv_getHashPreimage)] unconstrained fn load_from_execution_cache_oracle(_hash: Field) -> [Field; N] {} diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/nullifiers.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/nullifiers.nr index 1f9f5de98a02..c950f029dcaa 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/nullifiers.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/nullifiers.nr @@ -42,5 +42,6 @@ pub unconstrained fn check_nullifier_exists(inner_nullifier: Field) -> bool { check_nullifier_exists_oracle(inner_nullifier) } -#[oracle(aztec_utl_checkNullifierExists)] +// TODO(F-498): review naming consistency +#[oracle(aztec_utl_doesNullifierExist)] unconstrained fn check_nullifier_exists_oracle(_inner_nullifier: Field) -> bool {} diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/public_call.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/public_call.nr index 055aa3048d69..8c1faf9a9379 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/public_call.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/public_call.nr @@ -14,5 +14,6 @@ unconstrained fn validate_public_calldata_wrapper(calldata_hash: Field) { validate_public_calldata_oracle(calldata_hash) } -#[oracle(aztec_prv_validatePublicCalldata)] +// TODO(F-498): review naming consistency +#[oracle(aztec_prv_assertValidPublicCalldata)] unconstrained fn validate_public_calldata_oracle(_calldata_hash: Field) {} diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/storage.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/storage.nr index b91bf4c6866a..145be1482f42 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/storage.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/storage.nr @@ -4,7 +4,8 @@ use crate::protocol::{ traits::{Hash, Packable, ToField}, }; -#[oracle(aztec_utl_storageRead)] +// TODO(F-498): review naming consistency +#[oracle(aztec_utl_getFromPublicStorage)] unconstrained fn storage_read_oracle( block_hash: Field, address: Field, diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/tx_phase.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/tx_phase.nr index 207e2fb26030..33a35efdef9a 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/tx_phase.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/tx_phase.nr @@ -20,5 +20,6 @@ unconstrained fn notify_revertible_phase_start_oracle_wrapper(counter: u32) { #[oracle(aztec_prv_notifyRevertiblePhaseStart)] unconstrained fn notify_revertible_phase_start_oracle(_counter: u32) {} -#[oracle(aztec_prv_inRevertiblePhase)] +// TODO(F-498): review naming consistency +#[oracle(aztec_prv_isExecutionInRevertiblePhase)] unconstrained fn in_revertible_phase_oracle(current_counter: u32) -> bool {} diff --git a/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr b/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr index 8f3b64bdea67..a8989c57e0cc 100644 --- a/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr @@ -25,6 +25,7 @@ pub contract ContractClassRegistry { use aztec::{ oracle::capsules, protocol::{ + address::AztecAddress, constants::{ CONTRACT_CLASS_REGISTRY_BYTECODE_CAPSULE_SLOT, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS, @@ -78,6 +79,7 @@ pub contract ContractClassRegistry { capsules::load( context.this_address(), CONTRACT_CLASS_REGISTRY_BYTECODE_CAPSULE_SLOT, + AztecAddress::zero(), ) .unwrap() }; diff --git a/noir-projects/noir-contracts/contracts/test/abi_types_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/abi_types_contract/src/main.nr index 2952925403c6..df9a4145a001 100644 --- a/noir-projects/noir-contracts/contracts/test/abi_types_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/abi_types_contract/src/main.nr @@ -7,7 +7,14 @@ use aztec::macros::aztec; #[aztec] pub contract AbiTypes { - use aztec::{macros::functions::external, protocol::traits::{Deserialize, Serialize}}; + use aztec::{ + macros::functions::external, + protocol::{ + abis::function_selector::FunctionSelector, + address::EthAddress, + traits::{Deserialize, Serialize}, + }, + }; #[derive(Serialize, Deserialize, Eq)] pub struct CustomStruct { @@ -17,6 +24,11 @@ pub contract AbiTypes { pub z: i64, } + #[derive(Serialize, Deserialize, Eq)] + pub struct WrappedField { + pub inner: Field, + } + #[external("public")] fn return_public_parameters( a: bool, @@ -49,4 +61,19 @@ pub contract AbiTypes { ) -> (bool, Field, u64, i64, CustomStruct) { (a, b, c, d, e) } + + #[external("utility")] + unconstrained fn return_eth_address(addr: EthAddress) -> EthAddress { + addr + } + + #[external("utility")] + unconstrained fn return_function_selector(selector: FunctionSelector) -> FunctionSelector { + selector + } + + #[external("utility")] + unconstrained fn return_wrapped_field(wrapped: WrappedField) -> WrappedField { + wrapped + } } diff --git a/noir-projects/noir-contracts/contracts/test/avm_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/avm_test_contract/src/main.nr index 75327bf38420..0453a6e7116d 100644 --- a/noir-projects/noir-contracts/contracts/test/avm_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/avm_test_contract/src/main.nr @@ -605,15 +605,18 @@ pub contract AvmTest { #[contract_library_method] fn _emit_public_log(context: PublicContext) { - context.emit_public_log(/*message=*/ [10, 20, 30]); - context.emit_public_log(/*message=*/ "Hello, world!"); + context.emit_public_log_unsafe(0, /*message=*/ [10, 20, 30]); + context.emit_public_log_unsafe(0, /*message=*/ "Hello, world!"); let s: CompressedString<2, 44> = CompressedString::from_string("A long time ago, in a galaxy far far away..."); - context.emit_public_log(/*message=*/ s); - context.emit_public_log(/*message=*/ [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, - ]); // Large log + context.emit_public_log_unsafe(0, /*message=*/ s); + context.emit_public_log_unsafe( + 0, + /*message=*/ [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + ], + ); // Large log } #[external("public")] @@ -684,7 +687,7 @@ pub contract AvmTest { #[external("public")] fn n_new_public_logs(num: u32) { for i in 0..num { - self.context.emit_public_log(/*message=*/ [i as Field]); + self.context.emit_public_log_unsafe(0, /*message=*/ [i as Field]); } } diff --git a/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr index c48ddf69888e..42ad349a3889 100644 --- a/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr @@ -17,7 +17,7 @@ pub contract Benchmarking { constants::{ CONTRACT_CLASS_LOG_SIZE_IN_FIELDS, MAX_L2_TO_L1_MSGS_PER_CALL, MAX_NOTE_HASHES_PER_CALL, MAX_NULLIFIERS_PER_CALL, MAX_PRIVATE_LOGS_PER_CALL, - PRIVATE_LOG_SIZE_IN_FIELDS, + PRIVATE_LOG_CIPHERTEXT_LEN, }, }, state_vars::{Map, Owned, PrivateSet, PublicMutable}, @@ -61,7 +61,7 @@ pub contract Benchmarking { // Emits a public log. #[external("public")] fn broadcast(owner: AztecAddress) { - self.context.emit_public_log(self.storage.balances.at(owner).read()); + self.context.emit_public_log_unsafe(0, self.storage.balances.at(owner).read()); } // Does a bunch of heavy compute @@ -110,11 +110,11 @@ pub contract Benchmarking { let random_seed = unsafe { random() }; for i in 0..MAX_PRIVATE_LOGS_PER_CALL { - let mut log = [0; PRIVATE_LOG_SIZE_IN_FIELDS]; - for j in 0..PRIVATE_LOG_SIZE_IN_FIELDS { - log[i] = random_seed + (i * MAX_PRIVATE_LOGS_PER_CALL + j) as Field; + let mut log = [0; PRIVATE_LOG_CIPHERTEXT_LEN]; + for j in 0..PRIVATE_LOG_CIPHERTEXT_LEN { + log[j] = random_seed + (i * MAX_PRIVATE_LOGS_PER_CALL + j) as Field; } - self.context.emit_private_log(log, PRIVATE_LOG_SIZE_IN_FIELDS); + self.context.emit_private_log_unsafe(0, log, PRIVATE_LOG_CIPHERTEXT_LEN); } } diff --git a/noir-projects/noir-contracts/contracts/test/child_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/child_contract/src/main.nr index 1a5ae2c167e3..d8ed6010ae59 100644 --- a/noir-projects/noir-contracts/contracts/test/child_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/child_contract/src/main.nr @@ -42,7 +42,7 @@ pub contract Child { #[external("public")] fn pub_set_value(new_value: Field) -> Field { self.storage.current_value.write(new_value); - self.context.emit_public_log(new_value); + self.context.emit_public_log_unsafe(0, new_value); new_value } @@ -71,7 +71,7 @@ pub contract Child { fn pub_inc_value(new_value: Field) -> Field { let old_value = self.storage.current_value.read(); self.storage.current_value.write(old_value + new_value); - self.context.emit_public_log(new_value); + self.context.emit_public_log_unsafe(0, new_value); new_value } @@ -80,13 +80,13 @@ pub contract Child { fn set_value_twice_with_nested_first() { let _result = self.call_self.pub_set_value(10); self.storage.current_value.write(20); - self.context.emit_public_log(20); + self.context.emit_public_log_unsafe(0, 20); } #[external("public")] fn set_value_twice_with_nested_last() { self.storage.current_value.write(20); - self.context.emit_public_log(20); + self.context.emit_public_log_unsafe(0, 20); let _result = self.call_self.pub_set_value(10); } @@ -95,6 +95,6 @@ pub contract Child { self.call_self.set_value_twice_with_nested_first(); self.call_self.set_value_twice_with_nested_last(); self.storage.current_value.write(20); - self.context.emit_public_log(20); + self.context.emit_public_log_unsafe(0, 20); } } diff --git a/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr index 99abd3d0b473..6ec9821e5192 100644 --- a/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr @@ -32,6 +32,7 @@ unconstrained fn handle_multi_log_message( msg_metadata: u64, msg_content: BoundedVec, message_context: MessageContext, + scope: AztecAddress, ) { if msg_type_id == MULTI_LOG_MSG_TYPE_ID { let part_number = MultiLog::::unpack_part_number(msg_metadata); @@ -41,7 +42,11 @@ unconstrained fn handle_multi_log_message( let slot = poseidon2_hash([MULTI_LOG_PARTS_SEPARATOR, message_id]); // Load (or initialize) the pending event and place this part at its index. - let mut multi_log: MultiLog = capsules::load(contract_address, slot) + let mut multi_log: MultiLog = capsules::load( + contract_address, + slot, + scope, + ) .unwrap_or(MultiLog { parts: [BoundedVec::new(); NUM_MULTI_LOG_PARTS] }); multi_log.parts[part_number] = msg_content; @@ -70,7 +75,7 @@ unconstrained fn handle_multi_log_message( } } - capsules::delete(contract_address, slot); + capsules::delete(contract_address, slot, scope); let event_type_id = EventSelector::from_field(event_type_id_field); let event_commitment = compute_private_serialized_event_commitment( @@ -86,10 +91,10 @@ unconstrained fn handle_multi_log_message( serialized_event, event_commitment, message_context.tx_hash, - message_context.recipient, + scope, ); } else { - capsules::store(contract_address, slot, multi_log); + capsules::store(contract_address, slot, multi_log, scope); } } else { panic(f"Unknown message type id: {msg_type_id}"); diff --git a/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr index 2120f72e0fa5..85558367aefe 100644 --- a/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr @@ -4,17 +4,8 @@ use aztec::macros::aztec; #[aztec] pub contract InitTest { - use aztec::macros::{ - functions::{external, initializer, noinitcheck, only_self, view}, - storage::storage, - }; + use aztec::macros::functions::{external, initializer, noinitcheck, only_self, view}; use aztec::protocol::address::AztecAddress; - use aztec::state_vars::{Map, PublicMutable}; - - #[storage] - struct Storage { - public_values: Map, Context>, - } #[external("private")] #[initializer] @@ -64,14 +55,17 @@ pub contract InitTest { fn priv_no_init_check(owner: AztecAddress, value: Field) {} #[external("public")] - fn pub_init_check(owner: AztecAddress, value: Field) { - self.storage.public_values.at(owner).write(value); - } + fn pub_init_check(owner: AztecAddress, value: Field) {} #[external("public")] #[noinitcheck] #[view] fn pub_no_init_check(owner: AztecAddress) -> pub Field { - self.storage.public_values.at(owner).read() + 1 + } + + #[external("utility")] + unconstrained fn utility_init_check(owner: AztecAddress) -> Field { + 2 } } diff --git a/noir-projects/noir-contracts/contracts/test/no_constructor_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/no_constructor_contract/src/main.nr index 9395ae288ea1..b62c2c8ab851 100644 --- a/noir-projects/noir-contracts/contracts/test/no_constructor_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/no_constructor_contract/src/main.nr @@ -20,8 +20,8 @@ pub contract NoConstructor { /// Arbitrary public method used to test that publishing for public execution works for a contract with no constructor. #[external("public")] - fn emit_public(value: Field) { - self.context.emit_public_log(/*message=*/ value); + fn emit_public(tag: Field, value: Field) { + self.context.emit_public_log_unsafe(tag, /*message=*/ value); } /// Arbitrary function used to test that we can call private functions on a contract with diff --git a/noir-projects/noir-contracts/contracts/test/static_child_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/static_child_contract/src/main.nr index 2ec353639904..aea7f59909d1 100644 --- a/noir-projects/noir-contracts/contracts/test/static_child_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/static_child_contract/src/main.nr @@ -37,7 +37,7 @@ pub contract StaticChild { #[external("public")] fn pub_set_value(new_value: Field) -> Field { self.storage.current_value.write(new_value); - self.context.emit_public_log(new_value); + self.context.emit_public_log_unsafe(0, new_value); new_value } @@ -79,7 +79,7 @@ pub contract StaticChild { fn pub_inc_value(new_value: Field) -> Field { let old_value = self.storage.current_value.read(); self.storage.current_value.write(old_value + new_value); - self.context.emit_public_log(new_value); + self.context.emit_public_log_unsafe(0, new_value); new_value } @@ -89,7 +89,7 @@ pub contract StaticChild { fn pub_illegal_inc_value(new_value: Field) -> Field { let old_value = self.storage.current_value.read(); self.storage.current_value.write(old_value + new_value); - self.context.emit_public_log(new_value); + self.context.emit_public_log_unsafe(0, new_value); new_value } } diff --git a/noir-projects/noir-contracts/contracts/test/storage_proof_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/storage_proof_test_contract/src/main.nr index 9703c7303752..437294dc7a2d 100644 --- a/noir-projects/noir-contracts/contracts/test/storage_proof_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/storage_proof_test_contract/src/main.nr @@ -57,7 +57,12 @@ contract StorageProofTest { let address_capsule_key = compute_address_capsule_key(eth_storage_root, address); // Safety: We'll check that the address inside the hint matches. let hinted_account = unsafe { - Account::deserialize(capsules::load(self.address, address_capsule_key).unwrap()) + Account::deserialize(capsules::load( + self.address, + address_capsule_key, + AztecAddress::zero(), + ) + .unwrap()) }; assert_eq(hinted_account.address, address.to_field().to_be_bytes::<20>()); @@ -68,6 +73,7 @@ contract StorageProofTest { <(u32, [Node; MAX_ACCOUNT_PROOF_LENGTH]) as Deserialize>::deserialize(capsules::load( self.address, account_proof_capsule_key, + AztecAddress::zero(), ) .unwrap()) }; @@ -86,9 +92,12 @@ contract StorageProofTest { // Safety: This is a hint for path verification. let hinted_storage_proof_length = unsafe { - u32::deserialize( - capsules::load(self.address, storage_proof_capsule_key).unwrap(), + u32::deserialize(capsules::load( + self.address, + storage_proof_capsule_key, + AztecAddress::zero(), ) + .unwrap()) }; assert(hinted_storage_proof_length > 0, "Storage proof length must be greater than 0"); @@ -134,7 +143,12 @@ contract StorageProofTest { if i < num_nodes_to_process { // Safety: This is a hint for the path verification. hinted_nodes[i] = unsafe { - Node::deserialize(capsules::load(self.address, node_capsule_key).unwrap()) + Node::deserialize(capsules::load( + self.address, + node_capsule_key, + AztecAddress::zero(), + ) + .unwrap()) }; } } diff --git a/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr index d1767fe7a1eb..bc952c1943b0 100644 --- a/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr @@ -31,7 +31,7 @@ pub contract Test { protocol::{ abis::function_selector::FunctionSelector, address::{AztecAddress, EthAddress}, - constants::{PRIVATE_LOG_SIZE_IN_FIELDS, MAX_PUBLIC_LOG_SIZE_IN_FIELDS}, + constants::{PRIVATE_LOG_CIPHERTEXT_LEN, MAX_PUBLIC_LOG_SIZE_IN_FIELDS}, traits::{Hash, Packable, Serialize}, }, // Event related @@ -325,7 +325,12 @@ pub contract Test { // For testing non-note encrypted logs #[external("private")] - fn emit_array_as_encrypted_log(fields: [Field; 5], owner: AztecAddress, nest: bool) { + fn emit_array_as_encrypted_log( + tag: Field, + fields: [Field; 5], + owner: AztecAddress, + nest: bool, + ) { let event = ExampleEvent { value0: fields[0], value1: fields[1], @@ -339,25 +344,27 @@ pub contract Test { // this contract has reached max number of functions, so using this one fn // to test nested and non nested encrypted logs if nest { - self.call_self.emit_array_as_encrypted_log([0, 0, 0, 0, 0], owner, false); + self.call_self.emit_array_as_encrypted_log(tag, [0, 0, 0, 0, 0], owner, false); // Emit a log with non-encrypted content for testing purpose. - let leaky_log = event.serialize().concat([0; PRIVATE_LOG_SIZE_IN_FIELDS - 5]); - self.context.emit_private_log(leaky_log, 5); + let leaky_log = event.serialize().concat([0; PRIVATE_LOG_CIPHERTEXT_LEN - 5]); + self.context.emit_private_log_unsafe(tag, leaky_log, 5); } } #[external("public")] fn emit_public(value: Field) { - self.context.emit_public_log(/*message=*/ value); - self.context.emit_public_log(/*message=*/ [10, 20, 30]); - self.context.emit_public_log(/*message=*/ "Hello, world!"); + self.context.emit_public_log_unsafe(0, /*message=*/ value); + self.context.emit_public_log_unsafe(0, /*message=*/ [10, 20, 30]); + self.context.emit_public_log_unsafe(0, /*message=*/ "Hello, world!"); } #[external("public")] fn emit_full_size_public_log(value_offset: Field) { - let log_fields = [0; MAX_PUBLIC_LOG_SIZE_IN_FIELDS].mapi(|i, _| value_offset + i as Field); - self.context.emit_public_log(log_fields); + // -1 because emit_public_log_unsafe prepends the tag, so the emitted log is N + 1 fields. + let log_fields = + [0; MAX_PUBLIC_LOG_SIZE_IN_FIELDS - 1].mapi(|i, _| value_offset + i as Field); + self.context.emit_public_log_unsafe(0, log_fields); } #[external("private")] diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index afb70aee5f82..87b490d41b8c 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -674,6 +674,12 @@ pub global DOM_SEP__SILOED_NULLIFIER: u32 = 57496191; /// This is not technically a protocol constant as message nullifiers are computed by each contract. pub global DOM_SEP__MESSAGE_NULLIFIER: u32 = 3754509616; +/// Domain separator for event log tags. Used by [`crate::hash::compute_log_tag`]. +pub global DOM_SEP__EVENT_LOG_TAG: u32 = 926040838; +/// Domain separator for partial note completion log tags. Used by [`crate::hash::compute_log_tag`]. +pub global DOM_SEP__NOTE_COMPLETION_LOG_TAG: u32 = 3372669888; +/// Domain separator for unconstrained message delivery log tags. Used by [`crate::hash::compute_log_tag`]. +pub global DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG: u32 = 1485357192; /// Domain separator for private log tags. /// /// Used by [`crate::hash::compute_siloed_private_log_first_field`]. @@ -736,9 +742,9 @@ pub global DOM_SEP__AUTHWIT_INNER: u32 = 221354163; pub global DOM_SEP__AUTHWIT_OUTER: u32 = 3283595782; pub global DOM_SEP__AUTHWIT_NULLIFIER: u32 = 1239150694; -pub global DOM_SEP__SYMMETRIC_KEY: u32 = 3882206064; -pub global DOM_SEP__SYMMETRIC_KEY_2: u32 = 4129434989; -pub global DOM_SEP__CIPHERTEXT_FIELD_MASK: u32 = 1870492847; +pub global DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET: u32 = 1707851664; +pub global DOM_SEP__ECDH_SUBKEY: u32 = 4277646631; +pub global DOM_SEP__ECDH_FIELD_MASK: u32 = 190532684; pub global DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT: u32 = 623934423; diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr index 6d25e3d14ced..62bef3010c19 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr @@ -6,22 +6,24 @@ use crate::{ CONTRACT_CLASS_REGISTRY_PRIVATE_FUNCTION_BROADCASTED_MAGIC_VALUE, CONTRACT_CLASS_REGISTRY_UTILITY_FUNCTION_BROADCASTED_MAGIC_VALUE, CONTRACT_INSTANCE_PUBLISHED_MAGIC_VALUE, CONTRACT_INSTANCE_UPDATED_MAGIC_VALUE, - DOM_SEP__AUTHWIT_INNER, DOM_SEP__AUTHWIT_NULLIFIER, DOM_SEP__AUTHWIT_OUTER, - DOM_SEP__BLOCK_HEADER_HASH, DOM_SEP__CIPHERTEXT_FIELD_MASK, DOM_SEP__CONTRACT_ADDRESS_V1, - DOM_SEP__CONTRACT_CLASS_ID, DOM_SEP__EVENT_COMMITMENT, DOM_SEP__FUNCTION_ARGS, + DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, DOM_SEP__AUTHWIT_INNER, DOM_SEP__AUTHWIT_NULLIFIER, + DOM_SEP__AUTHWIT_OUTER, DOM_SEP__BLOCK_HEADER_HASH, DOM_SEP__CONTRACT_ADDRESS_V1, + DOM_SEP__CONTRACT_CLASS_ID, DOM_SEP__ECDH_FIELD_MASK, DOM_SEP__ECDH_SUBKEY, + DOM_SEP__EVENT_COMMITMENT, DOM_SEP__EVENT_LOG_TAG, DOM_SEP__FUNCTION_ARGS, DOM_SEP__INITIALIZATION_NULLIFIER, DOM_SEP__INITIALIZER, DOM_SEP__IVSK_M, - DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__NHK_M, DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE, - DOM_SEP__NOTE_NULLIFIER, DOM_SEP__OVSK_M, DOM_SEP__PARTIAL_ADDRESS, - DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, DOM_SEP__PRIVATE_FUNCTION_LEAF, - DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER, DOM_SEP__PRIVATE_LOG_FIRST_FIELD, - DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS, DOM_SEP__PUBLIC_BYTECODE, - DOM_SEP__PUBLIC_CALLDATA, DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER, - DOM_SEP__PUBLIC_KEYS_HASH, DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, - DOM_SEP__PUBLIC_TX_HASH, DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD, - DOM_SEP__SILOED_NOTE_HASH, DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, - DOM_SEP__SYMMETRIC_KEY, DOM_SEP__SYMMETRIC_KEY_2, DOM_SEP__TSK_M, DOM_SEP__TX_NULLIFIER, - DOM_SEP__TX_REQUEST, DOM_SEP__UNIQUE_NOTE_HASH, NULL_MSG_SENDER_CONTRACT_ADDRESS, - SIDE_EFFECT_MASKING_ADDRESS, TX_START_PREFIX, + DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__NHK_M, DOM_SEP__NOTE_COMPLETION_LOG_TAG, + DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE, DOM_SEP__NOTE_NULLIFIER, DOM_SEP__OVSK_M, + DOM_SEP__PARTIAL_ADDRESS, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, + DOM_SEP__PRIVATE_FUNCTION_LEAF, DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER, + DOM_SEP__PRIVATE_LOG_FIRST_FIELD, DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS, + DOM_SEP__PUBLIC_BYTECODE, DOM_SEP__PUBLIC_CALLDATA, + DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER, DOM_SEP__PUBLIC_KEYS_HASH, + DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, DOM_SEP__PUBLIC_TX_HASH, + DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD, DOM_SEP__SILOED_NOTE_HASH, + DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, DOM_SEP__TSK_M, + DOM_SEP__TX_NULLIFIER, DOM_SEP__TX_REQUEST, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG, + DOM_SEP__UNIQUE_NOTE_HASH, NULL_MSG_SENDER_CONTRACT_ADDRESS, SIDE_EFFECT_MASKING_ADDRESS, + TX_START_PREFIX, }, hash::poseidon2_hash_bytes, traits::{FromField, ToField}, @@ -135,7 +137,7 @@ impl HashedValueTester::new(); + let mut tester = HashedValueTester::<56, 49>::new(); // ----------------- // Domain separators @@ -159,6 +161,15 @@ fn hashed_values_match_derived() { DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, "public_storage_map_slot", ); + tester.assert_dom_sep_matches_derived(DOM_SEP__EVENT_LOG_TAG, "event_log_tag"); + tester.assert_dom_sep_matches_derived( + DOM_SEP__NOTE_COMPLETION_LOG_TAG, + "note_completion_log_tag", + ); + tester.assert_dom_sep_matches_derived( + DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG, + "unconstrained_msg_log_tag", + ); tester.assert_dom_sep_matches_derived(DOM_SEP__MESSAGE_NULLIFIER, "message_nullifier"); tester.assert_dom_sep_matches_derived(DOM_SEP__PRIVATE_FUNCTION_LEAF, "private_function_leaf"); tester.assert_dom_sep_matches_derived(DOM_SEP__PUBLIC_BYTECODE, "public_bytecode"); @@ -182,9 +193,12 @@ fn hashed_values_match_derived() { tester.assert_dom_sep_matches_derived(DOM_SEP__AUTHWIT_INNER, "authwit_inner"); tester.assert_dom_sep_matches_derived(DOM_SEP__AUTHWIT_OUTER, "authwit_outer"); tester.assert_dom_sep_matches_derived(DOM_SEP__AUTHWIT_NULLIFIER, "authwit_nullifier"); - tester.assert_dom_sep_matches_derived(DOM_SEP__SYMMETRIC_KEY, "symmetric_key"); - tester.assert_dom_sep_matches_derived(DOM_SEP__SYMMETRIC_KEY_2, "symmetric_key_2"); - tester.assert_dom_sep_matches_derived(DOM_SEP__CIPHERTEXT_FIELD_MASK, "ciphertext_field_mask"); + tester.assert_dom_sep_matches_derived( + DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, + "app_siloed_ecdh_shared_secret", + ); + tester.assert_dom_sep_matches_derived(DOM_SEP__ECDH_SUBKEY, "ecdh_subkey"); + tester.assert_dom_sep_matches_derived(DOM_SEP__ECDH_FIELD_MASK, "ecdh_field_mask"); tester.assert_dom_sep_matches_derived( DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, "partial_note_validity_commitment", diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr b/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr index 2fb1b80513d8..a9642b1937b5 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr @@ -133,6 +133,10 @@ pub fn create_protocol_nullifier(tx_request: TxRequest) -> Scoped Field { + poseidon2_hash_with_separator([raw_tag], dom_sep) +} + pub fn compute_siloed_private_log_first_field( contract_address: AztecAddress, field: Field, diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/logging.nr b/noir-projects/noir-protocol-circuits/crates/types/src/logging.nr index 30afa0713bb2..19d717947907 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/logging.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/logging.nr @@ -83,6 +83,9 @@ unconstrained fn log_oracle_wrapper( log_oracle(log_level, msg, N, args); } +// While the length parameter might seem unnecessary given that we have N, we keep it around because at the AVM +// bytecode level we want to support non-comptime-known lengths for such opcodes, even if Noir code will not generally +// take that route. The AVM transpiler maps this oracle to the DEBUGLOG opcode, which reads the fields size from memory. #[oracle(aztec_utl_log)] unconstrained fn log_oracle( log_level: u8, diff --git a/spartan/.gitignore b/spartan/.gitignore index 2c250795da22..4aabc98ecfbd 100644 --- a/spartan/.gitignore +++ b/spartan/.gitignore @@ -27,6 +27,7 @@ environments/* !environments/staging.local.env !environments/testnet-canary.env !environments/testnet.env +!environments/mainnet.env !environments/tps-scenario.env !environments/kind-minimal.env !environments/kind-provers.env diff --git a/spartan/bootstrap.sh b/spartan/bootstrap.sh index ddf9cf6280a8..6a9bab0b0379 100755 --- a/spartan/bootstrap.sh +++ b/spartan/bootstrap.sh @@ -146,7 +146,7 @@ function network_tests { function network_bench_cmds { local high_value_tps=0.1 - local low_value_tps_list=(0.1 0.2 0.5 1) + local low_value_tps_list=(0.1 0.2 0.5 1 2) for low_value_tps in "${low_value_tps_list[@]}"; do local low_label=${low_value_tps/./_} diff --git a/spartan/environments/ignition-fisherman.env b/spartan/environments/ignition-fisherman.env index d3d11e9b277b..07e8095e3509 100644 --- a/spartan/environments/ignition-fisherman.env +++ b/spartan/environments/ignition-fisherman.env @@ -30,6 +30,7 @@ OTEL_COLLECTOR_ENDPOINT=REPLACE_WITH_GCP_SECRET SNAPSHOT_BUCKET_DIRECTORY=${SNAPSHOT_BUCKET_DIRECTORY:-ignition-sepolia} BLOB_BUCKET_DIRECTORY=${BLOB_BUCKET_DIRECTORY:-ignition-sepolia/blobs} +BLOB_FILE_STORE_URLS="," ETHERSCAN_API_KEY="" R2_ACCESS_KEY_ID=REPLACE_WITH_GCP_SECRET diff --git a/spartan/environments/mainnet.env b/spartan/environments/mainnet.env new file mode 100644 index 000000000000..b4669e59912c --- /dev/null +++ b/spartan/environments/mainnet.env @@ -0,0 +1,47 @@ +NETWORK=${NETWORK:-mainnet} +L1_NETWORK=${L1_NETWORK:-mainnet} +ETHEREUM_CHAIN_ID=${ETHEREUM_CHAIN_ID:-1} + +GCP_REGION=us-west1-a +CLUSTER=aztec-gke-public +NAMESPACE=${NAMESPACE:-mainnet} + +CREATE_ROLLUP_CONTRACTS=false +VERIFY_CONTRACTS=false +DEPLOY_INTERNAL_BOOTNODE=false +VALIDATOR_REPLICAS=0 +RPC_REPLICAS=1 +PROVER_REPLICAS=0 + +FISHERMAN_MODE=true +FISHERMAN_MNEMONIC_START_INDEX=1 +PROVER_NODE_DISABLE_PROOF_PUBLISH=true + +LOG_LEVEL=info +FISHERMAN_LOG_LEVEL=info +USE_NETWORK_CONFIG=true + +PROVER_FAILED_PROOF_STORE=gs://aztec-develop/mainnet/failed-proofs +L1_TX_FAILED_STORE=gs://aztec-develop/mainnet/failed-l1-txs + +ETHEREUM_RPC_URLS=REPLACE_WITH_GCP_SECRET +ETHEREUM_CONSENSUS_HOST_URLS=REPLACE_WITH_GCP_SECRET +ETHEREUM_CONSENSUS_HOST_API_KEYS=REPLACE_WITH_GCP_SECRET +ETHEREUM_CONSENSUS_HOST_API_KEY_HEADERS=REPLACE_WITH_GCP_SECRET +LABS_INFRA_MNEMONIC=REPLACE_WITH_GCP_SECRET +OTEL_COLLECTOR_ENDPOINT=REPLACE_WITH_GCP_SECRET +# intentionally left blank +FUNDING_PRIVATE_KEY="" +ROLLUP_DEPLOYMENT_PRIVATE_KEY="" + +SNAPSHOT_BUCKET_DIRECTORY=${SNAPSHOT_BUCKET_DIRECTORY:-mainnet} + +BLOB_BUCKET_DIRECTORY=${BLOB_BUCKET_DIRECTORY:-mainnet/blobs} +BLOB_FILE_STORE_URLS="," + +TX_FILE_STORE_ENABLED=true +TX_FILE_STORE_BUCKET_DIRECTORY=${TX_FILE_STORE_BUCKET_DIRECTORY:-mainnet/txs} +TX_COLLECTION_FILE_STORE_URLS="https://aztec-labs-snapshots.com/${TX_FILE_STORE_BUCKET_DIRECTORY}" + +R2_ACCESS_KEY_ID=REPLACE_WITH_GCP_SECRET +R2_SECRET_ACCESS_KEY=REPLACE_WITH_GCP_SECRET diff --git a/spartan/environments/next-net.env b/spartan/environments/next-net.env index 196aa3169b6e..5d46d171f9d6 100644 --- a/spartan/environments/next-net.env +++ b/spartan/environments/next-net.env @@ -18,6 +18,7 @@ ETHERSCAN_API_KEY=REPLACE_WITH_GCP_SECRET DEPLOY_INTERNAL_BOOTNODE=true STORE_SNAPSHOT_URL= BLOB_BUCKET_DIRECTORY=${BLOB_BUCKET_DIRECTORY:-next-net/blobs} +BLOB_FILE_STORE_URLS="," TX_FILE_STORE_ENABLED=true TX_FILE_STORE_BUCKET_DIRECTORY=${TX_FILE_STORE_BUCKET_DIRECTORY:-next-net/txs} TX_COLLECTION_FILE_STORE_URLS="https://aztec-labs-snapshots.com/${TX_FILE_STORE_BUCKET_DIRECTORY}" diff --git a/spartan/environments/staging-ignition.env b/spartan/environments/staging-ignition.env index 5ae4843aa4ec..f1d267365995 100644 --- a/spartan/environments/staging-ignition.env +++ b/spartan/environments/staging-ignition.env @@ -23,6 +23,7 @@ VERIFY_CONTRACTS=true ETHERSCAN_API_KEY=REPLACE_WITH_GCP_SECRET SNAPSHOT_BUCKET_DIRECTORY=${SNAPSHOT_BUCKET_DIRECTORY:-staging-ignition} BLOB_BUCKET_DIRECTORY=${BLOB_BUCKET_DIRECTORY:-staging-ignition/blobs} +BLOB_FILE_STORE_URLS="," R2_ACCESS_KEY_ID=REPLACE_WITH_GCP_SECRET R2_SECRET_ACCESS_KEY=REPLACE_WITH_GCP_SECRET BOT_TRANSFERS_REPLICAS=0 diff --git a/spartan/environments/staging-public.env b/spartan/environments/staging-public.env index fa67490c7688..924e1df966a1 100644 --- a/spartan/environments/staging-public.env +++ b/spartan/environments/staging-public.env @@ -17,6 +17,7 @@ ETHERSCAN_API_KEY=REPLACE_WITH_GCP_SECRET DEPLOY_INTERNAL_BOOTNODE=true SNAPSHOT_BUCKET_DIRECTORY=${SNAPSHOT_BUCKET_DIRECTORY:-staging-public} BLOB_BUCKET_DIRECTORY=${BLOB_BUCKET_DIRECTORY:-staging-public/blobs} +BLOB_FILE_STORE_URLS="," TX_FILE_STORE_ENABLED=true TX_FILE_STORE_BUCKET_DIRECTORY=${TX_FILE_STORE_BUCKET_DIRECTORY:-staging-public/txs} TX_COLLECTION_FILE_STORE_URLS="https://aztec-labs-snapshots.com/${TX_FILE_STORE_BUCKET_DIRECTORY}" diff --git a/spartan/environments/testnet.env b/spartan/environments/testnet.env index 6b5638c202f1..bab2e6a04261 100644 --- a/spartan/environments/testnet.env +++ b/spartan/environments/testnet.env @@ -50,6 +50,7 @@ USE_NETWORK_CONFIG=${USE_NETWORK_CONFIG:-true} SNAPSHOT_BUCKET_DIRECTORY=${SNAPSHOT_BUCKET_DIRECTORY:-testnet} BLOB_BUCKET_DIRECTORY=${BLOB_BUCKET_DIRECTORY:-testnet/blobs} +BLOB_FILE_STORE_URLS="," TX_FILE_STORE_ENABLED=true TX_FILE_STORE_BUCKET_DIRECTORY=${TX_FILE_STORE_BUCKET_DIRECTORY:-testnet/txs} TX_COLLECTION_FILE_STORE_URLS="https://aztec-labs-snapshots.com/${TX_FILE_STORE_BUCKET_DIRECTORY}" diff --git a/spartan/environments/tps-scenario.env b/spartan/environments/tps-scenario.env index 688fa0c6a4e5..b208c5c0fec3 100644 --- a/spartan/environments/tps-scenario.env +++ b/spartan/environments/tps-scenario.env @@ -69,6 +69,7 @@ AZTEC_LOCAL_EJECTION_THRESHOLD=90000000000000000000 SEQ_MAX_TX_PER_CHECKPOINT=15 # approx 0.2 TPS SEQ_MIN_TX_PER_BLOCK=1 +SEQ_BUILD_CHECKPOINT_IF_EMPTY=true # Override L1 tx utils bump percentages for scenario tests VALIDATOR_L1_PRIORITY_FEE_BUMP_PERCENTAGE=0 diff --git a/spartan/scripts/deploy_network.sh b/spartan/scripts/deploy_network.sh index 9304a94d384f..e168592f3cee 100755 --- a/spartan/scripts/deploy_network.sh +++ b/spartan/scripts/deploy_network.sh @@ -618,6 +618,7 @@ PROVER_L1_PRIORITY_FEE_BUMP_PERCENTAGE = ${PROVER_L1_PRIORITY_FEE_BUMP_PERCENTAG PROVER_L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE = ${PROVER_L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE:-null} BLOB_ALLOW_EMPTY_SOURCES = ${BLOB_ALLOW_EMPTY_SOURCES:-false} BLOB_FILE_STORE_UPLOAD_URL = ${BLOB_FILE_STORE_UPLOAD_URL_TF} +BLOB_FILE_STORE_URLS = "${BLOB_FILE_STORE_URLS:-}" TX_FILE_STORE_ENABLED = ${TX_FILE_STORE_ENABLED} TX_FILE_STORE_URL = ${TX_FILE_STORE_URL_TF} TX_COLLECTION_FILE_STORE_URLS = "${TX_COLLECTION_FILE_STORE_URLS}" diff --git a/spartan/terraform/cloudflare/main.tf b/spartan/terraform/cloudflare/main.tf index b1c12316ee9c..0d73a8d90d64 100644 --- a/spartan/terraform/cloudflare/main.tf +++ b/spartan/terraform/cloudflare/main.tf @@ -37,6 +37,35 @@ resource "cloudflare_r2_custom_domain" "aztec_labs_snapshots_com" { enabled = true } +# Do not cache 404s +resource "cloudflare_ruleset" "cache_settings" { + zone_id = var.R2_ZONE_ID + kind = "zone" + name = "R2 cache settings" + phase = "http_request_cache_settings" + + rules = [ + { + ref = "no_cache_404" + description = "Do not cache 404 responses for R2 custom domain" + expression = "(http.host eq \"${var.DOMAIN}\")" + action = "set_cache_settings" + action_parameters = { + cache = true + edge_ttl = { + mode = "respect_origin" + status_code_ttl = [ + { + status_code = 404 + value = 0 + } + ] + } + } + } + ] +} + locals { top_level_folders = toset([ "devnet", diff --git a/spartan/terraform/deploy-aztec-infra/main.tf b/spartan/terraform/deploy-aztec-infra/main.tf index 0d8ac042099b..0ba6fa7ceeac 100644 --- a/spartan/terraform/deploy-aztec-infra/main.tf +++ b/spartan/terraform/deploy-aztec-infra/main.tf @@ -573,6 +573,7 @@ locals { "node.env.P2P_DROP_TX_CHANCE" = var.P2P_DROP_TX_CHANCE "node.env.WS_NUM_HISTORIC_CHECKPOINTS" = var.WS_NUM_HISTORIC_CHECKPOINTS "node.env.TX_COLLECTION_FILE_STORE_URLS" = var.TX_COLLECTION_FILE_STORE_URLS + "node.env.BLOB_FILE_STORE_URLS" = var.BLOB_FILE_STORE_URLS } boot_node_host_path = "node.env.BOOT_NODE_HOST" bootstrap_nodes_path = "node.env.BOOTSTRAP_NODES" diff --git a/spartan/terraform/deploy-aztec-infra/values/blob-sink.yaml b/spartan/terraform/deploy-aztec-infra/values/blob-sink.yaml index 04ffca33f75a..7970519894a4 100644 --- a/spartan/terraform/deploy-aztec-infra/values/blob-sink.yaml +++ b/spartan/terraform/deploy-aztec-infra/values/blob-sink.yaml @@ -3,6 +3,17 @@ node: env: OTEL_SERVICE_NAME: "blob-sink" + preStartScript: | + if [ -n "${BOOT_NODE_HOST:-}" ]; then + until curl --silent --head --fail "${BOOT_NODE_HOST}/status" > /dev/null; do + echo "Waiting for boot node..." + sleep 1 + done + echo "Boot node is ready!" + + export BOOTSTRAP_NODES=$(curl -X POST -H "content-type: application/json" --data '{"method": "bootstrap_getEncodedEnr"}' $BOOT_NODE_HOST | jq -r .result) + fi + startCmd: - --node - --archiver diff --git a/spartan/terraform/deploy-aztec-infra/variables.tf b/spartan/terraform/deploy-aztec-infra/variables.tf index ec754d70839c..f1f1fafdcfa6 100644 --- a/spartan/terraform/deploy-aztec-infra/variables.tf +++ b/spartan/terraform/deploy-aztec-infra/variables.tf @@ -768,6 +768,12 @@ variable "BLOB_FILE_STORE_UPLOAD_URL" { default = null } +variable "BLOB_FILE_STORE_URLS" { + description = "Comma-separated URLs for reading blobs from filestore. Set to ',' to disable." + type = string + default = "" +} + variable "TX_FILE_STORE_ENABLED" { description = "Whether to enable uploading transactions to file storage" type = bool diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index f51448cf39d7..f7547a5159ba 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -642,6 +642,30 @@ describe('aztec node', () => { expect(result).toBe(snapshotMerkleTreeOps); }); }); + + describe('getBlockHashMembershipWitness', () => { + let initialHeader: BlockHeader; + + beforeEach(() => { + lastBlockNumber = BlockNumber(5); + initialHeader = BlockHeader.empty({ + globalVariables: GlobalVariables.empty({ blockNumber: BlockNumber.ZERO }), + }); + merkleTreeOps.getInitialHeader.mockReturnValue(initialHeader); + }); + + it('returns undefined when reference block is the initial block hash', async () => { + // The initial block (block 0) has an empty archive — no block hashes exist in it. + // getBlockHashMembershipWitness computes referenceBlockNumber - 1, which would be 0 - 1 = -1. + // This should return undefined (empty archive has no witnesses) rather than crashing. + const initialHash = await initialHeader.hash(); + const initialBlockHash = new BlockHash(initialHash); + const someBlockHash = BlockHash.random(); + + const result = await node.getBlockHashMembershipWitness(initialBlockHash, someBlockHash); + expect(result).toBeUndefined(); + }); + }); }); describe('simulatePublicCalls', () => { diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 2836f700a4c1..eec304ba23b7 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1083,6 +1083,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { // which is the archive tree root BEFORE the anchor block was added (i.e. the state after block N-1). // So we need the world state at block N-1, not block N, to produce a sibling path matching that root. const referenceBlockNumber = await this.resolveBlockNumber(referenceBlock); + if (referenceBlockNumber === BlockNumber.ZERO) { + // Block 0 (the initial block) has an empty archive, so no membership witness can exist. + return undefined; + } const committedDb = await this.getWorldState(BlockNumber(referenceBlockNumber - 1)); const [pathAndIndex] = await committedDb.findSiblingPaths(MerkleTreeId.ARCHIVE, [blockHash]); return pathAndIndex === undefined diff --git a/yarn-project/aztec.js/src/api/contract.ts b/yarn-project/aztec.js/src/api/contract.ts index 4443cae6d60c..b8cd3953e84c 100644 --- a/yarn-project/aztec.js/src/api/contract.ts +++ b/yarn-project/aztec.js/src/api/contract.ts @@ -10,16 +10,9 @@ * or can be queried via `simulate()`. * * ```ts - * // Deploy and get the contract instance directly (default behavior) - * const contract = await Contract.deploy(wallet, MyContractArtifact, [...constructorArgs]).send({ from: accountAddress }); + * // Deploy and get the contract, receipt, and instance + * const { contract, receipt, instance } = await Contract.deploy(wallet, MyContractArtifact, [...constructorArgs]).send({ from: accountAddress }); * console.log(`Contract deployed at ${contract.address}`); - * - * // Or get the full receipt with contract and instance - * const receipt = await Contract.deploy(wallet, MyContractArtifact, [...constructorArgs]).send({ - * from: accountAddress, - * wait: { returnReceipt: true } - * }); - * console.log(`Contract deployed at ${receipt.contract.address}`); * ``` * * ```ts @@ -74,8 +67,6 @@ export { type DeployOptions, type DeployResultMined, type DeployReturn, - type DeployTxReceipt, - type DeployWaitOptions, type DeployInteractionWaitOptions, DeployMethod, type RequestDeployOptions, diff --git a/yarn-project/aztec.js/src/api/events.ts b/yarn-project/aztec.js/src/api/events.ts index a683718a8ee8..9e616c0822f9 100644 --- a/yarn-project/aztec.js/src/api/events.ts +++ b/yarn-project/aztec.js/src/api/events.ts @@ -1,4 +1,6 @@ -import { type EventMetadataDefinition, EventSelector, decodeFromAbi } from '@aztec/stdlib/abi'; +import { DomainSeparator } from '@aztec/constants'; +import { type EventMetadataDefinition, decodeFromAbi } from '@aztec/stdlib/abi'; +import { computeLogTag } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { PublicEvent, PublicEventFilter } from '../wallet/wallet.js'; @@ -28,27 +30,26 @@ export async function getPublicEvents( eventMetadataDef: EventMetadataDefinition, filter: PublicEventFilter, ): Promise> { + // Public events are tagged with a domain-separated hash of their event type ID, so we compute + // the same hash here to filter for logs of the requested event type. + const logTag = await computeLogTag(eventMetadataDef.eventSelector.toField(), DomainSeparator.EVENT_LOG_TAG); + const { logs, maxLogsHit } = await node.getPublicLogs({ fromBlock: filter.fromBlock ? Number(filter.fromBlock) : undefined, toBlock: filter.toBlock ? Number(filter.toBlock) : undefined, txHash: filter.txHash, contractAddress: filter.contractAddress, afterLog: filter.afterLog, + tag: logTag, }); const events: PublicEvent[] = []; for (const log of logs) { - const logFields = log.log.getEmittedFields(); - // Event selector is at the last position of the emitted fields - const logEventSelector = EventSelector.fromField(logFields[logFields.length - 1]); - - if (!logEventSelector.equals(eventMetadataDef.eventSelector)) { - continue; - } + const logFieldsWithoutTag = log.log.getEmittedFieldsWithoutTag(); events.push({ - event: decodeFromAbi([eventMetadataDef.abiType], log.log.fields) as T, + event: decodeFromAbi([eventMetadataDef.abiType], logFieldsWithoutTag) as T, metadata: { l2BlockNumber: log.id.blockNumber, l2BlockHash: log.id.blockHash, diff --git a/yarn-project/aztec.js/src/contract/batch_call.test.ts b/yarn-project/aztec.js/src/contract/batch_call.test.ts index 189ba8734436..39f65adf2e96 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.test.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.test.ts @@ -1,8 +1,11 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; +import { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { + Capsule, ExecutionPayload, + HashedValues, OFFCHAIN_MESSAGE_IDENTIFIER, type OffchainEffect, TxSimulationResult, @@ -405,5 +408,27 @@ describe('BatchCall', () => { expect(result.calls[1]).toEqual(payload.calls[0]); expect(mockPaymentMethod.getExecutionPayload).toHaveBeenCalledTimes(1); }); + + it('should propagate authWitnesses, capsules, and extraHashedArgs into the execution payload', async () => { + const contractAddress = await AztecAddress.random(); + const payload = createPrivateExecutionPayload('func', [Fr.random()], contractAddress); + + const authWitness = AuthWitness.random(); + const capsule = new Capsule(await AztecAddress.random(), Fr.random(), [Fr.random()]); + const extraHashedArgs = [HashedValues.random()]; + + batchCall = new BatchCall(wallet, [payload], extraHashedArgs); + // Inject authWitnesses and capsules into the interaction (as BaseContractInteraction exposes these) + (batchCall as any).authWitnesses = [authWitness]; + (batchCall as any).capsules = [capsule]; + + const result = await batchCall.request(); + + expect(result.calls).toHaveLength(1); + expect(result.calls[0]).toEqual(payload.calls[0]); + expect(result.authWitnesses).toContainEqual(authWitness); + expect(result.capsules).toContainEqual(capsule); + expect(result.extraHashedArgs).toContainEqual(extraHashedArgs[0]); + }); }); }); diff --git a/yarn-project/aztec.js/src/contract/batch_call.ts b/yarn-project/aztec.js/src/contract/batch_call.ts index d64b13e58ecf..596e3f6d1d99 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.ts @@ -1,5 +1,11 @@ import { type FunctionCall, FunctionType, decodeFromAbi } from '@aztec/stdlib/abi'; -import { ExecutionPayload, TxSimulationResult, UtilityExecutionResult, mergeExecutionPayloads } from '@aztec/stdlib/tx'; +import { + ExecutionPayload, + HashedValues, + TxSimulationResult, + UtilityExecutionResult, + mergeExecutionPayloads, +} from '@aztec/stdlib/tx'; import type { BatchedMethod, Wallet } from '../wallet/wallet.js'; import { BaseContractInteraction } from './base_contract_interaction.js'; @@ -19,6 +25,7 @@ export class BatchCall extends BaseContractInteraction { constructor( wallet: Wallet, protected interactions: (BaseContractInteraction | ExecutionPayload)[], + private extraHashedArgs: HashedValues[] = [], ) { super(wallet); } @@ -34,9 +41,18 @@ export class BatchCall extends BaseContractInteraction { const feeExecutionPayload = options.fee?.paymentMethod ? await options.fee.paymentMethod.getExecutionPayload() : undefined; + const { authWitnesses, capsules } = options; + + // Propagates the included authwitnesses, capsules, and extraHashedArgs potentially baked into the interaction + const initialExecutionPayload = new ExecutionPayload( + [], + this.authWitnesses.concat(authWitnesses ?? []), + this.capsules.concat(capsules ?? []), + this.extraHashedArgs, + ); const finalExecutionPayload = feeExecutionPayload - ? mergeExecutionPayloads([feeExecutionPayload, ...requests]) - : mergeExecutionPayloads([...requests]); + ? mergeExecutionPayloads([initialExecutionPayload, feeExecutionPayload, ...requests]) + : mergeExecutionPayloads([initialExecutionPayload, ...requests]); return finalExecutionPayload; } diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index 5e6b278615fc..3f520e9155ee 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -30,7 +30,6 @@ import { type SimulationInteractionFeeOptions, type SimulationResult, type TxSendResultImmediate, - type TxSendResultMined, extractOffchainOutput, toProfileOptions, toSendOptions, @@ -38,22 +37,13 @@ import { } from './interaction_options.js'; import type { WaitOpts } from './wait_opts.js'; -/** - * Wait options specific to deployment transactions. - * Extends WaitOpts with a flag to return the full receipt instead of just the contract. - */ -export type DeployWaitOptions = WaitOpts & { - /** If true, return the full DeployTxReceipt instead of just the contract. Defaults to false. */ - returnReceipt?: boolean; -}; - /** * Type for wait options in deployment interactions. * - NO_WAIT symbol: Don't wait, return TxHash immediately - * - DeployWaitOptions: Wait with custom options + * - WaitOpts: Wait with custom options * - undefined: Wait with default options */ -export type DeployInteractionWaitOptions = NoWait | DeployWaitOptions | undefined; +export type DeployInteractionWaitOptions = NoWait | WaitOpts | undefined; /** * Options for deploying a contract on the Aztec network. @@ -96,7 +86,7 @@ export type DeployOptions = /** * Options for waiting for the transaction to be mined. * - undefined (default): wait with default options and return the contract instance - * - DeployWaitOptions: wait with custom options and return contract or receipt based on returnReceipt flag + * - WaitOpts: wait with custom options * - NO_WAIT: return TxHash immediately without waiting */ wait?: W; @@ -120,40 +110,20 @@ export type SimulateDeployOptions = Omit & { includeMetadata?: boolean; }; -/** Receipt for a deployment transaction with the deployed contract instance. */ -export type DeployTxReceipt = TxReceipt & { - /** Type-safe wrapper around the deployed contract instance, linked to the deployment wallet */ - contract: TContract; - /** The deployed contract instance with address and metadata. */ - instance: ContractInstanceWithAddress; -}; - -/** Wait options that request a full receipt instead of just the contract instance. */ -type WaitWithReturnReceipt = { - /** Request the full receipt instead of just the contract instance. */ - returnReceipt: true; -}; - -/** - * Represents the result type of deploying a contract. - * - If wait is NO_WAIT, returns TxHash immediately. - * - If wait has returnReceipt: true, returns DeployTxReceipt after waiting. - * - Otherwise (undefined or DeployWaitOptions without returnReceipt), returns TContract after waiting. - */ /** Result of deploying a contract when waiting for mining (default case). */ export type DeployResultMined = { /** The deployed contract instance. */ contract: TContract; + /** The deployed contract instance with address and metadata. */ + instance: ContractInstanceWithAddress; /** The deploy transaction receipt. */ - receipt: DeployTxReceipt; + receipt: TxReceipt; } & OffchainOutput; /** Conditional return type for deploy based on wait options. */ export type DeployReturn = W extends NoWait ? TxSendResultImmediate - : W extends WaitWithReturnReceipt - ? TxSendResultMined> - : DeployResultMined; + : DeployResultMined; /** * Contract interaction for deployment. @@ -234,20 +204,13 @@ export class DeployMethod extends } /** - * Converts DeployOptions to SendOptions, stripping out the returnReceipt flag if present. - * @param options - Deploy options with wait parameter - * @returns Send options with wait parameter + * Converts DeployOptions to SendOptions. + * @param options - Deploy options with wait parameter. */ protected convertDeployOptionsToSendOptions( options: DeployOptions, - // eslint-disable-next-line jsdoc/require-jsdoc - ): SendOptions { - return { - ...toSendOptions({ - ...options, - wait: options.wait as any, - }), - } as any; + ): SendOptions { + return toSendOptions({ ...options, wait: options.wait as any }) as any; } /** @@ -354,7 +317,7 @@ export class DeployMethod extends * By default, waits for the transaction to be mined and returns the deployed contract instance. * * @param options - An object containing various deployment options such as contractAddressSalt and from. - * @returns TxHash (if wait is NO_WAIT), TContract (if wait is undefined or doesn't have returnReceipt), or DeployTxReceipt (if wait.returnReceipt is true) + * @returns TxHash (if wait is NO_WAIT), or DeployResultMined with contract, receipt, and instance (otherwise) */ // Overload for when wait is not specified at all - returns the contract public override send(options: DeployOptionsWithoutWait): Promise>; @@ -383,12 +346,7 @@ export class DeployMethod extends const instance = await this.getInstance(options); const contract = this.postDeployCtor(instance, this.wallet) as TContract; - // Return full receipt if requested, otherwise just the contract - if (options.wait && typeof options.wait === 'object' && options.wait.returnReceipt) { - return { receipt: { ...receipt, contract, instance }, ...offchainOutput }; - } - - return { contract, receipt, ...offchainOutput }; + return { contract, receipt, instance, ...offchainOutput }; } /** diff --git a/yarn-project/aztec.js/src/wallet/deploy_account_method.ts b/yarn-project/aztec.js/src/wallet/deploy_account_method.ts index 73f57bb46c33..c77b45f8c338 100644 --- a/yarn-project/aztec.js/src/wallet/deploy_account_method.ts +++ b/yarn-project/aztec.js/src/wallet/deploy_account_method.ts @@ -23,7 +23,6 @@ import { NO_FROM, type ProfileInteractionOptions, } from '../contract/interaction_options.js'; -import type { WaitOpts } from '../contract/wait_opts.js'; import type { FeePaymentMethod } from '../fee/fee_payment_method.js'; import { AccountEntrypointMetaPaymentMethod } from './account_entrypoint_meta_payment_method.js'; import type { ProfileOptions, SendOptions, SimulateOptions, Wallet } from './index.js'; @@ -170,8 +169,7 @@ export class DeployAccountMethod exte protected override convertDeployOptionsToSendOptions( options: DeployOptions, - // eslint-disable-next-line jsdoc/require-jsdoc - ): SendOptions { + ): SendOptions { return super.convertDeployOptionsToSendOptions(this.injectContractAddressIntoScopes(options)); } diff --git a/yarn-project/aztec/scripts/aztec.sh b/yarn-project/aztec/scripts/aztec.sh index 16e64d62cd4f..8ec2ff401e86 100755 --- a/yarn-project/aztec/scripts/aztec.sh +++ b/yarn-project/aztec/scripts/aztec.sh @@ -23,7 +23,7 @@ case $cmd in # Attempt to compile, no-op if there are no changes node --no-warnings "$script_dir/../dest/bin/index.js" compile - export LOG_LEVEL="${LOG_LEVEL:-"error;trace:contract_log"}" + export LOG_LEVEL="${LOG_LEVEL:-"error;trace:contract"}" aztec start --txe --port 8081 & server_pid=$! trap 'kill $server_pid &>/dev/null || true' EXIT diff --git a/yarn-project/bb-prover/src/prover/client/bb_private_kernel_prover.ts b/yarn-project/bb-prover/src/prover/client/bb_private_kernel_prover.ts index 70f691a6508c..157b1e282840 100644 --- a/yarn-project/bb-prover/src/prover/client/bb_private_kernel_prover.ts +++ b/yarn-project/bb-prover/src/prover/client/bb_private_kernel_prover.ts @@ -286,16 +286,28 @@ export abstract class BBPrivateKernelProver implements PrivateKernelProver { executionSteps.map(step => step.functionName), ); - const [proof] = await backend.prove( + // Use compressed prove path to get both proof fields and compressed proof bytes + const result = await backend.prove( executionSteps.map(step => ungzip(serializeWitness(step.witness))), executionSteps.map(step => step.vk), + { compress: true }, ); this.log.info(`Generated ClientIVC proof`, { eventName: 'client-ivc-proof-generation', duration: timer.ms(), - proofSize: proof.length, + proofSize: result.proofFields.length, + compressedSize: result.compressedProof?.length, }); - return ChonkProofWithPublicInputs.fromBufferArray(proof); + + // Create ChonkProofWithPublicInputs from the flat field elements + const proofWithPubInputs = ChonkProofWithPublicInputs.fromBufferArray(result.proofFields); + + // Attach compressed proof bytes to the ChonkProof (without public inputs). + // The compressed bytes are for the full proof WITH public inputs from bb; + // when deserializing, the decompressor will strip them to match CHONK_PROOF_LENGTH. + proofWithPubInputs.compressedProof = result.compressedProof ? Buffer.from(result.compressedProof) : undefined; + + return proofWithPubInputs; } public async computeGateCountForCircuit(_bytecode: Buffer, _circuitName: string): Promise { diff --git a/yarn-project/bot/src/amm_bot.ts b/yarn-project/bot/src/amm_bot.ts index 21461c775d90..a8d414759a94 100644 --- a/yarn-project/bot/src/amm_bot.ts +++ b/yarn-project/bot/src/amm_bot.ts @@ -87,7 +87,7 @@ export class AmmBot extends BaseBot { authWitnesses: [swapAuthwit], }); - const opts = await this.getSendMethodOpts(swapExactTokensInteraction); + const opts = this.getSendMethodOpts(); this.log.verbose(`Sending transaction`, logCtx); this.log.info(`Tx. Balances: ${jsonStringify(balances)}`, { ...logCtx, balances }); diff --git a/yarn-project/bot/src/base_bot.ts b/yarn-project/bot/src/base_bot.ts index d50e4248fadc..723385a62bde 100644 --- a/yarn-project/bot/src/base_bot.ts +++ b/yarn-project/bot/src/base_bot.ts @@ -1,5 +1,5 @@ import { AztecAddress } from '@aztec/aztec.js/addresses'; -import { BatchCall, ContractFunctionInteraction, type SendInteractionOptions } from '@aztec/aztec.js/contracts'; +import type { SendInteractionOptions } from '@aztec/aztec.js/contracts'; import { createLogger } from '@aztec/aztec.js/log'; import { waitForTx } from '@aztec/aztec.js/node'; import { TxHash, TxReceipt, TxStatus } from '@aztec/aztec.js/tx'; @@ -56,27 +56,19 @@ export abstract class BaseBot { return Promise.resolve(); } - protected async getSendMethodOpts( - interaction: ContractFunctionInteraction | BatchCall, - ): Promise { + protected getSendMethodOpts(): SendInteractionOptions { const { l2GasLimit, daGasLimit, minFeePadding } = this.config; this.wallet.setMinFeePadding(minFeePadding); - let gasSettings; - if (l2GasLimit !== undefined && l2GasLimit > 0 && daGasLimit !== undefined && daGasLimit > 0) { - gasSettings = { gasLimits: Gas.from({ l2Gas: l2GasLimit, daGas: daGasLimit }) }; - this.log.verbose(`Using gas limits ${l2GasLimit} L2 gas ${daGasLimit} DA gas`); - } else { - this.log.verbose(`Estimating gas for transaction`); - ({ estimatedGas: gasSettings } = await interaction.simulate({ - fee: { estimateGas: true }, - from: this.defaultAccountAddress, - })); - } + const gasSettings = + l2GasLimit !== undefined && l2GasLimit > 0 && daGasLimit !== undefined && daGasLimit > 0 + ? { gasLimits: Gas.from({ l2Gas: l2GasLimit, daGas: daGasLimit }) } + : undefined; + return { from: this.defaultAccountAddress, - fee: { gasSettings }, + ...(gasSettings ? { fee: { gasSettings } } : {}), }; } } diff --git a/yarn-project/bot/src/bot.ts b/yarn-project/bot/src/bot.ts index c2128b2a219e..f4bda93d7b9a 100644 --- a/yarn-project/bot/src/bot.ts +++ b/yarn-project/bot/src/bot.ts @@ -70,10 +70,7 @@ export class Bot extends BaseBot { ); const batch = new BatchCall(wallet, calls); - const opts = await this.getSendMethodOpts(batch); - - this.log.verbose(`Simulating transaction with ${calls.length}`, logCtx); - await batch.simulate({ from: this.defaultAccountAddress }); + const opts = this.getSendMethodOpts(); this.log.verbose(`Sending transaction`, logCtx); const { txHash } = await batch.send({ ...opts, wait: NO_WAIT }); diff --git a/yarn-project/bot/src/config.ts b/yarn-project/bot/src/config.ts index 3efc3c977348..95ae4173a4f0 100644 --- a/yarn-project/bot/src/config.ts +++ b/yarn-project/bot/src/config.ts @@ -69,9 +69,9 @@ export type BotConfig = { maxPendingTxs: number; /** Whether to flush after sending each 'setup' transaction */ flushSetupTransactions: boolean; - /** L2 gas limit for the tx (empty to have the bot trigger an estimate gas). */ + /** L2 gas limit for the tx (empty to let the bot's wallet estimate). */ l2GasLimit: number | undefined; - /** DA gas limit for the tx (empty to have the bot trigger an estimate gas). */ + /** DA gas limit for the tx (empty to let the bot's wallet estimate). */ daGasLimit: number | undefined; /** Token contract to use */ contract: SupportedTokenContracts; @@ -243,12 +243,12 @@ export const botConfigMappings: ConfigMappingsType = { }, l2GasLimit: { env: 'BOT_L2_GAS_LIMIT', - description: 'L2 gas limit for the tx (empty to have the bot trigger an estimate gas).', + description: "L2 gas limit for the tx (empty to let the bot's wallet estimate).", ...optionalNumberConfigHelper(), }, daGasLimit: { env: 'BOT_DA_GAS_LIMIT', - description: 'DA gas limit for the tx (empty to have the bot trigger an estimate gas).', + description: "DA gas limit for the tx (empty to let the bot's wallet estimate).", ...optionalNumberConfigHelper(), }, contract: { diff --git a/yarn-project/bot/src/cross_chain_bot.ts b/yarn-project/bot/src/cross_chain_bot.ts index 2a3ab8848ddb..ab5416753b2c 100644 --- a/yarn-project/bot/src/cross_chain_bot.ts +++ b/yarn-project/bot/src/cross_chain_bot.ts @@ -137,7 +137,7 @@ export class CrossChainBot extends BaseBot { } const batch = new BatchCall(this.wallet, calls); - const opts = await this.getSendMethodOpts(batch); + const opts = this.getSendMethodOpts(); this.log.verbose(`Sending cross-chain batch with ${calls.length} calls`, logCtx); const { txHash } = await batch.send({ ...opts, wait: NO_WAIT }); diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index b317b0142278..0ced68e14182 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -29,7 +29,6 @@ import { PrivateTokenContract } from '@aztec/noir-contracts.js/PrivateToken'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { GasFees, GasSettings } from '@aztec/stdlib/gas'; import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import { EmbeddedWallet } from '@aztec/wallets/embedded'; @@ -224,21 +223,14 @@ export class BotFactory { const paymentMethod = new FeeJuicePaymentMethodWithClaim(accountManager.address, claim); const deployMethod = await accountManager.getDeployMethod(); - const maxFeesPerGas = (await this.aztecNode.getCurrentMinFees()).mul(1 + this.config.minFeePadding); - - const { estimatedGas } = await deployMethod.simulate({ - from: NO_FROM, - fee: { estimateGas: true, paymentMethod }, - }); - const gasSettings = GasSettings.from({ ...estimatedGas!, maxFeesPerGas, maxPriorityFeesPerGas: GasFees.empty() }); await this.withNoMinTxsPerBlock(async () => { const { txHash } = await deployMethod.send({ from: NO_FROM, - fee: { gasSettings, paymentMethod }, + fee: { paymentMethod }, wait: NO_WAIT, }); - this.log.info(`Sent tx for account deployment with hash ${txHash.toString()}`, { gasSettings }); + this.log.info(`Sent tx for account deployment with hash ${txHash.toString()}`); return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); }); this.log.info(`Account deployed at ${address}`); @@ -304,9 +296,8 @@ export class BotFactory { await deploy.register(); } else { this.log.info(`Deploying token contract at ${address.toString()}`); - const { estimatedGas } = await deploy.simulate({ ...deployOpts, fee: { estimateGas: true } }); - const { txHash } = await deploy.send({ ...deployOpts, fee: { gasSettings: estimatedGas }, wait: NO_WAIT }); - this.log.info(`Sent tx for token setup with hash ${txHash.toString()}`, { estimatedGas }); + const { txHash } = await deploy.send({ ...deployOpts, wait: NO_WAIT }); + this.log.info(`Sent tx for token setup with hash ${txHash.toString()}`); await this.withNoMinTxsPerBlock(async () => { await waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); return token; @@ -347,18 +338,11 @@ export class BotFactory { this.log.info(`AMM deployed at ${amm.address}`); const setMinterInteraction = lpToken.methods.set_minter(amm.address, true); - const { estimatedGas: setMinterGas } = await setMinterInteraction.simulate({ - from: deployer, - fee: { estimateGas: true }, - }); const { receipt: minterReceipt } = await setMinterInteraction.send({ from: deployer, - fee: { gasSettings: setMinterGas }, wait: { timeout: this.config.txMinedWaitSeconds }, }); - this.log.info(`Set LP token minter to AMM txHash=${minterReceipt.txHash.toString()}`, { - estimatedGas: setMinterGas, - }); + this.log.info(`Set LP token minter to AMM txHash=${minterReceipt.txHash.toString()}`); this.log.info(`Liquidity token initialized`); return amm; @@ -430,17 +414,12 @@ export class BotFactory { token0.methods.mint_to_private(liquidityProvider, MINT_BALANCE), token1.methods.mint_to_private(liquidityProvider, MINT_BALANCE), ]); - const { estimatedGas: mintGas } = await mintBatch.simulate({ - from: liquidityProvider, - fee: { estimateGas: true }, - }); const { receipt: mintReceipt } = await mintBatch.send({ from: liquidityProvider, - fee: { gasSettings: mintGas }, wait: { timeout: this.config.txMinedWaitSeconds }, }); - this.log.info(`Sent mint tx: ${mintReceipt.txHash.toString()}`, { estimatedGas: mintGas }); + this.log.info(`Sent mint tx: ${mintReceipt.txHash.toString()}`); const addLiquidityInteraction = amm.methods.add_liquidity( amount0Max, @@ -449,21 +428,13 @@ export class BotFactory { amount1Min, authwitNonce, ); - const { estimatedGas: addLiquidityGas } = await addLiquidityInteraction.simulate({ - from: liquidityProvider, - fee: { estimateGas: true }, - authWitnesses: [token0Authwit, token1Authwit], - }); const { receipt: addLiquidityReceipt } = await addLiquidityInteraction.send({ from: liquidityProvider, - fee: { gasSettings: addLiquidityGas }, authWitnesses: [token0Authwit, token1Authwit], wait: { timeout: this.config.txMinedWaitSeconds }, }); - this.log.info(`Sent tx to add liquidity to the AMM: ${addLiquidityReceipt.txHash.toString()}`, { - estimatedGas: addLiquidityGas, - }); + this.log.info(`Sent tx to add liquidity to the AMM: ${addLiquidityReceipt.txHash.toString()}`); this.log.info(`Liquidity added`); const [newT0Bal, newT1Bal, newLPBal] = await getPrivateBalances(); @@ -484,10 +455,9 @@ export class BotFactory { this.log.info(`Contract ${name} at ${address.toString()} already deployed`); await deploy.register(); } else { - const { estimatedGas } = await deploy.simulate({ ...deployOpts, fee: { estimateGas: true } }); - this.log.info(`Deploying contract ${name} at ${address.toString()}`, { estimatedGas }); + this.log.info(`Deploying contract ${name} at ${address.toString()}`); await this.withNoMinTxsPerBlock(async () => { - const { txHash } = await deploy.send({ ...deployOpts, fee: { gasSettings: estimatedGas }, wait: NO_WAIT }); + const { txHash } = await deploy.send({ ...deployOpts, wait: NO_WAIT }); this.log.info(`Sent contract ${name} setup tx with hash ${txHash.toString()}`); return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); }); @@ -532,15 +502,13 @@ export class BotFactory { // PrivateToken's mint accesses contract-level private storage vars (admin, total_supply). const additionalScopes = isStandardToken ? undefined : [token.address]; const mintBatch = new BatchCall(token.wallet, calls); - const { estimatedGas } = await mintBatch.simulate({ from: minter, fee: { estimateGas: true }, additionalScopes }); await this.withNoMinTxsPerBlock(async () => { const { txHash } = await mintBatch.send({ from: minter, additionalScopes, - fee: { gasSettings: estimatedGas }, wait: NO_WAIT, }); - this.log.info(`Sent token mint tx with hash ${txHash.toString()}`, { estimatedGas }); + this.log.info(`Sent token mint tx with hash ${txHash.toString()}`); return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); }); } diff --git a/yarn-project/constants/src/constants.gen.ts b/yarn-project/constants/src/constants.gen.ts index e92cccb37013..f4b9ed89ccad 100644 --- a/yarn-project/constants/src/constants.gen.ts +++ b/yarn-project/constants/src/constants.gen.ts @@ -512,6 +512,9 @@ export enum DomainSeparator { NOTE_NULLIFIER = 50789342, SILOED_NULLIFIER = 57496191, MESSAGE_NULLIFIER = 3754509616, + EVENT_LOG_TAG = 926040838, + NOTE_COMPLETION_LOG_TAG = 3372669888, + UNCONSTRAINED_MSG_LOG_TAG = 1485357192, PRIVATE_LOG_FIRST_FIELD = 2769976252, PUBLIC_LEAF_SLOT = 1247650290, PUBLIC_STORAGE_MAP_SLOT = 4015149901, @@ -537,9 +540,9 @@ export enum DomainSeparator { AUTHWIT_INNER = 221354163, AUTHWIT_OUTER = 3283595782, AUTHWIT_NULLIFIER = 1239150694, - SYMMETRIC_KEY = 3882206064, - SYMMETRIC_KEY_2 = 4129434989, - CIPHERTEXT_FIELD_MASK = 1870492847, + APP_SILOED_ECDH_SHARED_SECRET = 1707851664, + ECDH_SUBKEY = 4277646631, + ECDH_FIELD_MASK = 190532684, PARTIAL_NOTE_VALIDITY_COMMITMENT = 623934423, INITIALIZATION_NULLIFIER = 1653084894, PUBLIC_INITIALIZATION_NULLIFIER = 3342006647, diff --git a/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts b/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts index ef092df34035..ac234af7c3a9 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts @@ -92,7 +92,7 @@ describe('Deployment benchmark', () => { if (process.env.SANITY_CHECKS) { // Ensure we paid a fee - const { receipt } = await deploymentInteraction.send({ ...options, wait: { returnReceipt: true } }); + const { receipt } = await deploymentInteraction.send({ ...options }); expect(receipt.transactionFee!).toBeGreaterThan(0n); } }); diff --git a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts index 5f8a9b8ec908..fdd1d29c130d 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts @@ -250,11 +250,14 @@ export class ClientFlowsBenchmark { async applyDeployBananaToken() { this.logger.info('Applying banana token deployment'); - const { - receipt: { contract: bananaCoin, instance: bananaCoinInstance }, - } = await BananaCoin.deploy(this.adminWallet, this.adminAddress, 'BC', 'BC', 18n).send({ + const { contract: bananaCoin, instance: bananaCoinInstance } = await BananaCoin.deploy( + this.adminWallet, + this.adminAddress, + 'BC', + 'BC', + 18n, + ).send({ from: this.adminAddress, - wait: { returnReceipt: true }, }); this.logger.info(`BananaCoin deployed at ${bananaCoin.address}`); this.bananaCoin = bananaCoin; @@ -263,11 +266,14 @@ export class ClientFlowsBenchmark { async applyDeployCandyBarToken() { this.logger.info('Applying candy bar token deployment'); - const { - receipt: { contract: candyBarCoin, instance: candyBarCoinInstance }, - } = await TokenContract.deploy(this.adminWallet, this.adminAddress, 'CBC', 'CBC', 18n).send({ + const { contract: candyBarCoin, instance: candyBarCoinInstance } = await TokenContract.deploy( + this.adminWallet, + this.adminAddress, + 'CBC', + 'CBC', + 18n, + ).send({ from: this.adminAddress, - wait: { returnReceipt: true }, }); this.logger.info(`CandyBarCoin deployed at ${candyBarCoin.address}`); this.candyBarCoin = candyBarCoin; @@ -280,11 +286,12 @@ export class ClientFlowsBenchmark { expect((await this.context.wallet.getContractMetadata(feeJuiceContract.address)).isContractPublished).toBe(true); const bananaCoin = this.bananaCoin; - const { - receipt: { contract: bananaFPC, instance: bananaFPCInstance }, - } = await FPCContract.deploy(this.adminWallet, bananaCoin.address, this.adminAddress).send({ + const { contract: bananaFPC, instance: bananaFPCInstance } = await FPCContract.deploy( + this.adminWallet, + bananaCoin.address, + this.adminAddress, + ).send({ from: this.adminAddress, - wait: { returnReceipt: true }, }); this.logger.info(`BananaPay deployed at ${bananaFPC.address}`); @@ -348,20 +355,21 @@ export class ClientFlowsBenchmark { public async applyDeployAmm() { this.logger.info('Applying AMM deployment'); - const { - receipt: { contract: liquidityToken, instance: liquidityTokenInstance }, - } = await TokenContract.deploy(this.adminWallet, this.adminAddress, 'LPT', 'LPT', 18n).send({ + const { contract: liquidityToken, instance: liquidityTokenInstance } = await TokenContract.deploy( + this.adminWallet, + this.adminAddress, + 'LPT', + 'LPT', + 18n, + ).send({ from: this.adminAddress, - wait: { returnReceipt: true }, }); - const { - receipt: { contract: amm, instance: ammInstance }, - } = await AMMContract.deploy( + const { contract: amm, instance: ammInstance } = await AMMContract.deploy( this.adminWallet, this.bananaCoin.address, this.candyBarCoin.address, liquidityToken.address, - ).send({ from: this.adminAddress, wait: { returnReceipt: true } }); + ).send({ from: this.adminAddress }); this.logger.info(`AMM deployed at ${amm.address}`); await liquidityToken.methods.set_minter(amm.address, true).send({ from: this.adminAddress }); this.liquidityToken = liquidityToken; diff --git a/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts b/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts index f75f4d011751..7cf37f32763a 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts @@ -86,7 +86,7 @@ describe('Deployment benchmark', () => { if (process.env.SANITY_CHECKS) { // Ensure we paid a fee - const { receipt } = await deploymentInteraction.send({ ...options, wait: { returnReceipt: true } }); + const { receipt } = await deploymentInteraction.send({ ...options }); expect(receipt.transactionFee!).toBeGreaterThan(0n); } }); diff --git a/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts b/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts index c46f17ccf444..d4fcd63bb74d 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts @@ -34,9 +34,8 @@ describe('Storage proof benchmark', () => { await t.applyFPCSetup(); await t.applyDeploySponsoredFPC(); - const { receipt: deployed } = await StorageProofTestContract.deploy(t.adminWallet).send({ + const deployed = await StorageProofTestContract.deploy(t.adminWallet).send({ from: t.adminAddress, - wait: { returnReceipt: true }, }); storageProofContract = deployed.contract; storageProofInstance = deployed.instance; diff --git a/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts b/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts index 3f60809d3f8f..31b9667d0190 100644 --- a/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts @@ -68,11 +68,8 @@ describe('Aztec persistence', () => { owner = initialFundedAccounts[0]; ownerAddress = owner.address; - const { - receipt: { contract, instance }, - } = await TokenBlacklistContract.deploy(wallet, ownerAddress).send({ + const { contract, instance } = await TokenBlacklistContract.deploy(wallet, ownerAddress).send({ from: ownerAddress, - wait: { returnReceipt: true }, }); contractInstance = instance; contractAddress = contract.address; diff --git a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts index eb779d1c82a0..93f674ed6a73 100644 --- a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts +++ b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts @@ -326,7 +326,6 @@ describe('HA Full Setup', () => { const { receipt } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - wait: { returnReceipt: true }, }); await waitForProven(aztecNode, receipt, { @@ -445,7 +444,6 @@ describe('HA Full Setup', () => { const { receipt } = await deployer.deploy(ownerAddress, ownerAddress, 42).send({ from: ownerAddress, contractAddressSalt: Fr.random(), - wait: { returnReceipt: true }, }); expect(receipt.blockNumber).toBeDefined(); logger.info(`Transaction mined in block ${receipt.blockNumber}`); @@ -604,7 +602,6 @@ describe('HA Full Setup', () => { const receipt = await deployer.deploy(ownerAddress, ownerAddress, 201).send({ from: ownerAddress, contractAddressSalt: new Fr(201), - wait: { returnReceipt: true }, }); expect(receipt.receipt.blockNumber).toBeDefined(); const [block] = await aztecNode.getCheckpointedBlocks(receipt.receipt.blockNumber!, 1); @@ -647,7 +644,6 @@ describe('HA Full Setup', () => { const { receipt } = await deployer.deploy(ownerAddress, ownerAddress, i + 100).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(i + 100)), - wait: { returnReceipt: true }, }); expect(receipt.blockNumber).toBeDefined(); diff --git a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts index fd4ee33f95bc..b9e15d0378cc 100644 --- a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts +++ b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts @@ -108,11 +108,8 @@ describe('e2e_2_pxes', () => { const deployChildContractViaServerA = async () => { logger.info(`Deploying Child contract...`); - const { - receipt: { instance }, - } = await ChildContract.deploy(walletA).send({ + const { instance } = await ChildContract.deploy(walletA).send({ from: accountAAddress, - wait: { returnReceipt: true }, }); logger.info('Child contract deployed'); diff --git a/yarn-project/end-to-end/src/e2e_abi_types.test.ts b/yarn-project/end-to-end/src/e2e_abi_types.test.ts index a7702f78c720..0244327b214a 100644 --- a/yarn-project/end-to-end/src/e2e_abi_types.test.ts +++ b/yarn-project/end-to-end/src/e2e_abi_types.test.ts @@ -1,4 +1,6 @@ -import { AztecAddress } from '@aztec/aztec.js/addresses'; +import { FunctionSelector } from '@aztec/aztec.js/abi'; +import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses'; +import { Fr } from '@aztec/aztec.js/fields'; import type { Wallet } from '@aztec/aztec.js/wallet'; import { MAX_FIELD_VALUE } from '@aztec/constants'; import { AbiTypesContract } from '@aztec/noir-test-contracts.js/AbiTypes'; @@ -84,6 +86,39 @@ describe('AbiTypes', () => { ]); }); + it('decodes EthAddress return value', async () => { + const ethAddr = EthAddress.fromString('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'); + + const { result } = await abiTypesContract.methods + .return_eth_address(ethAddr) + .simulate({ from: defaultAccountAddress }); + + expect(result).toBeInstanceOf(EthAddress); + expect(result).toEqual(ethAddr); + }); + + it('decodes FunctionSelector return value', async () => { + const selector = FunctionSelector.fromField(new Fr(0xdeadbeefn)); + + const { result } = await abiTypesContract.methods + .return_function_selector(selector) + .simulate({ from: defaultAccountAddress }); + + expect(result).toBeInstanceOf(FunctionSelector); + expect(result).toEqual(selector); + }); + + it('decodes wrapped field struct as Fr', async () => { + const value = new Fr(42n); + + const { result } = await abiTypesContract.methods + .return_wrapped_field(42n) + .simulate({ from: defaultAccountAddress }); + + expect(result).toBeInstanceOf(Fr); + expect(result).toEqual(value); + }); + it('passes utility parameters', async () => { const { result: minResult } = await abiTypesContract.methods .return_utility_parameters(false, 0n, 0n, I64_MIN, { w: 0n, x: false, y: 0n, z: I64_MIN }) diff --git a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts index b40af1162298..58e7980edcea 100644 --- a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts +++ b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts @@ -36,11 +36,8 @@ describe('e2e_avm_simulator', () => { let secondAvmContract: AvmTestContract; beforeEach(async () => { - ({ - receipt: { contract: avmContract, instance: avmContractInstance }, - } = await AvmTestContract.deploy(wallet).send({ + ({ contract: avmContract, instance: avmContractInstance } = await AvmTestContract.deploy(wallet).send({ from: defaultAccountAddress, - wait: { returnReceipt: true }, })); ({ contract: secondAvmContract } = await AvmTestContract.deploy(wallet).send({ from: defaultAccountAddress })); }); diff --git a/yarn-project/end-to-end/src/e2e_block_building.test.ts b/yarn-project/end-to-end/src/e2e_block_building.test.ts index e2ba50f41ee7..1904675cb6bb 100644 --- a/yarn-project/end-to-end/src/e2e_block_building.test.ts +++ b/yarn-project/end-to-end/src/e2e_block_building.test.ts @@ -446,7 +446,8 @@ describe('e2e_block_building', () => { // call test contract const valuesAsArray = Object.values(values); - const action = testContract.methods.emit_array_as_encrypted_log(valuesAsArray, ownerAddress, true); + const tag = 42n; + const action = testContract.methods.emit_array_as_encrypted_log(tag, valuesAsArray, ownerAddress, true); const tx = await proveInteraction(wallet, action, { from: ownerAddress }); const rct = await tx.send(); @@ -467,14 +468,12 @@ describe('e2e_block_building', () => { expect(events[1].event).toEqual(nestedValues); // The last log is not encrypted. - // The first field is the first value and is siloed with contract address by the kernel circuit. - const expectedFirstField = await computeSiloedPrivateLogFirstField( - testContract.address, - new Fr(valuesAsArray[0]), - ); - expect(privateLogs[2].fields.slice(0, 5).map((f: Fr) => f.toBigInt())).toEqual([ - expectedFirstField.toBigInt(), - ...valuesAsArray.slice(1), + // fields[0] is the tag, siloed with the contract address by the kernel circuit. + // The payload starts at fields[1]. + const expectedSiloedTag = await computeSiloedPrivateLogFirstField(testContract.address, new Fr(tag)); + expect(privateLogs[2].fields.slice(0, 6).map((f: Fr) => f.toBigInt())).toEqual([ + expectedSiloedTag.toBigInt(), + ...valuesAsArray, ]); }, 60_000); }); diff --git a/yarn-project/end-to-end/src/e2e_contract_updates.test.ts b/yarn-project/end-to-end/src/e2e_contract_updates.test.ts index e34cedae2e97..01342cd6cf66 100644 --- a/yarn-project/end-to-end/src/e2e_contract_updates.test.ts +++ b/yarn-project/end-to-end/src/e2e_contract_updates.test.ts @@ -110,12 +110,9 @@ describe('e2e_contract_updates', () => { } sequencer = maybeSequencer; - ({ - receipt: { contract, instance }, - } = await UpdatableContract.deploy(wallet, constructorArgs[0]).send({ + ({ contract, instance } = await UpdatableContract.deploy(wallet, constructorArgs[0]).send({ from: defaultAccountAddress, contractAddressSalt: salt, - wait: { returnReceipt: true }, })); const registerMethod = await publishContractClass(wallet, UpdatedContractArtifact); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts index 601be528a2ca..181302b58961 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts @@ -139,11 +139,14 @@ describe('e2e_deploy_contract deploy method', () => { it('publicly deploys a contract with no constructor', async () => { logger.debug(`Deploying contract with no constructor`); const { contract } = await NoConstructorContract.deploy(wallet).send({ from: defaultAccountAddress }); + const arbitraryTag = 99; const arbitraryValue = 42; logger.debug(`Call a public function to check that it was publicly deployed`); - const { receipt } = await contract.methods.emit_public(arbitraryValue).send({ from: defaultAccountAddress }); + const { receipt } = await contract.methods + .emit_public(arbitraryTag, arbitraryValue) + .send({ from: defaultAccountAddress }); const logs = await aztecNode.getPublicLogs({ txHash: receipt.txHash }); - expect(logs.logs[0].log.getEmittedFields()).toEqual([new Fr(arbitraryValue)]); + expect(logs.logs[0].log.getEmittedFields()).toEqual([new Fr(arbitraryTag), new Fr(arbitraryValue)]); }); it('refuses to deploy a contract with no constructor and no public deployment', async () => { diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts index ae2c526cdbce..3764964f4f82 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts @@ -36,10 +36,8 @@ describe('e2e_deploy_contract legacy', () => { deployer: defaultAccountAddress, }); const deployer = new ContractDeployer(TestContractArtifact, wallet); - const { receipt } = await deployer - .deploy() - .send({ from: defaultAccountAddress, contractAddressSalt: salt, wait: { returnReceipt: true } }); - expect(receipt.contract.address).toEqual(deploymentData.address); + const { contract } = await deployer.deploy().send({ from: defaultAccountAddress, contractAddressSalt: salt }); + expect(contract.address).toEqual(deploymentData.address); const { instance, isContractPublished } = await wallet.getContractMetadata(deploymentData.address); expect(instance).toBeDefined(); expect(isContractPublished).toBe(true); @@ -65,11 +63,11 @@ describe('e2e_deploy_contract legacy', () => { for (let index = 0; index < 2; index++) { logger.info(`Deploying contract ${index + 1}...`); - const { receipt } = await deployer + const { contract: deployed } = await deployer .deploy() - .send({ from: defaultAccountAddress, contractAddressSalt: Fr.random(), wait: { returnReceipt: true } }); + .send({ from: defaultAccountAddress, contractAddressSalt: Fr.random() }); logger.info(`Sending TX to contract ${index + 1}...`); - await receipt.contract.methods + await deployed.methods .get_master_incoming_viewing_public_key(defaultAccountAddress) .send({ from: defaultAccountAddress }); } @@ -104,8 +102,8 @@ describe('e2e_deploy_contract legacy', () => { }; const [goodTxPromiseResult, badTxReceiptResult] = await Promise.allSettled([ - goodDeploy.send({ ...firstOpts, wait: { returnReceipt: true } }), - badDeploy.send({ ...secondOpts, wait: { dontThrowOnRevert: true, returnReceipt: true } }), + goodDeploy.send({ ...firstOpts }), + badDeploy.send({ ...secondOpts, wait: { dontThrowOnRevert: true } }), ]); expect(goodTxPromiseResult.status).toBe('fulfilled'); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts index ea5443226953..33d63164a9c1 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts @@ -140,6 +140,23 @@ describe('e2e_deploy_contract private initialization', () => { ); }); + it('refuses to simulate a utility function that requires initialization', async () => { + const owner = (await wallet.createAccount()).address; + const initArgs: InitTestCtorArgs = [owner, 42]; + const contract = await t.registerContract(wallet, InitTestContract, { initArgs }); + await expect(contract.methods.utility_init_check(owner).simulate({ from: defaultAccountAddress })).rejects.toThrow( + /Not initialized/, + ); + }); + + it('allows calling a utility function after initialization', async () => { + const { contract, initArgs } = await deployUninitialized(); + const owner = defaultAccountAddress; + await contract.methods.constructor(...initArgs).send({ from: defaultAccountAddress }); + const result = await contract.methods.utility_init_check(owner).simulate({ from: defaultAccountAddress }); + expect(result.result).toEqual(2n); + }); + // A public call enqueued before the private constructor should fail the init check, even though the // private constructor emits the init nullifier in the same tx. it('refuses to call a public function enqueued before private initialization in same tx', async () => { @@ -161,7 +178,7 @@ describe('e2e_deploy_contract private initialization', () => { ]); await batch.send({ from: defaultAccountAddress }); expect((await contract.methods.pub_no_init_check(owner).simulate({ from: defaultAccountAddress })).result).toEqual( - 84n, + 1n, ); }); diff --git a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts index bf2bc79020cd..253d6948cdfa 100644 --- a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts @@ -90,7 +90,7 @@ describe('e2e_fees account_init', () => { const [bobsInitialGas] = await t.getGasBalanceFn(bobsAddress); expect(bobsInitialGas).toEqual(mintAmount); - const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM, wait: { returnReceipt: true } }); + const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM }); expect(tx.transactionFee!).toBeGreaterThan(0n); await expect(t.getGasBalanceFn(bobsAddress)).resolves.toEqual([bobsInitialGas - tx.transactionFee!]); @@ -102,7 +102,6 @@ describe('e2e_fees account_init', () => { const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM, fee: { paymentMethod }, - wait: { returnReceipt: true }, }); expect(tx.transactionFee!).toBeGreaterThan(0n); await expect(t.getGasBalanceFn(bobsAddress)).resolves.toEqual([claim.claimAmount - tx.transactionFee!]); @@ -122,7 +121,6 @@ describe('e2e_fees account_init', () => { const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM, fee: { paymentMethod }, - wait: { returnReceipt: true }, }); const actualFee = tx.transactionFee!; expect(actualFee).toBeGreaterThan(0n); @@ -152,7 +150,6 @@ describe('e2e_fees account_init', () => { from: NO_FROM, skipInstancePublication: false, fee: { paymentMethod }, - wait: { returnReceipt: true }, }); const actualFee = tx.transactionFee!; @@ -195,7 +192,6 @@ describe('e2e_fees account_init', () => { skipInstancePublication: true, skipInitialization: false, universalDeploy: true, - wait: { returnReceipt: true }, }); // alice paid in Fee Juice diff --git a/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts b/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts index b456cd167784..190dbf2b97aa 100644 --- a/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts @@ -1,5 +1,4 @@ import type { AztecAddress } from '@aztec/aztec.js/addresses'; -import type { DeployTxReceipt } from '@aztec/aztec.js/contracts'; import { type FeePaymentMethod, PublicFeePaymentMethod } from '@aztec/aztec.js/fee'; import type { AztecNode } from '@aztec/aztec.js/node'; import type { Wallet } from '@aztec/aztec.js/wallet'; @@ -186,7 +185,6 @@ describe('e2e_fees gas_estimation', () => { from: aliceAddress, fee: { gasSettings: limits ? { ...gasSettings, ...limits } : gasSettings }, skipClassPublication: true, - wait: { returnReceipt: true }, }; }; @@ -201,10 +199,10 @@ describe('e2e_fees gas_estimation', () => { const estimatedGas = sim3.estimatedGas!; logGasEstimate(estimatedGas); - const [{ receipt: withEstimate }, { receipt: withoutEstimate }] = (await Promise.all([ + const [{ receipt: withEstimate }, { receipt: withoutEstimate }] = await Promise.all([ deployMethod().send(deployOpts(estimatedGas)), deployMethod().send(deployOpts()), - ])) as unknown as { receipt: DeployTxReceipt }[]; + ]); // Estimation should yield that teardown has no cost, so should send the tx with zero for teardown expect(withEstimate.transactionFee!).toEqual(withoutEstimate.transactionFee!); diff --git a/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts b/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts index 53060168826d..9abc4c4b8b38 100644 --- a/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts +++ b/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts @@ -117,7 +117,6 @@ describe('e2e_multi_validator_node', () => { const { receipt: tx } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - wait: { returnReceipt: true }, }); await waitForProven(aztecNode, tx, { provenTimeout: (config.aztecProofSubmissionEpochs + 1) * config.aztecEpochDuration * config.aztecSlotDuration, @@ -178,7 +177,6 @@ describe('e2e_multi_validator_node', () => { const { receipt: tx } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - wait: { returnReceipt: true }, }); await waitForProven(aztecNode, tx, { provenTimeout: (config.aztecProofSubmissionEpochs + 1) * config.aztecEpochDuration * config.aztecSlotDuration, diff --git a/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts b/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts index c4da4436f003..a766ed446312 100644 --- a/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts +++ b/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts @@ -57,6 +57,7 @@ describe('e2e_multiple_blobs', () => { contract.methods.emit_nullifier(Fr.random()), contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(Fr.random(), EthAddress.random()), contract.methods.emit_array_as_encrypted_log( + 0n, Array.from({ length: 5 }).map(() => Fr.random()), defaultAccountAddress, true, diff --git a/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts b/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts index 9968050c18b7..fe8db573792b 100644 --- a/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts +++ b/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts @@ -1,11 +1,7 @@ import { AztecAddress } from '@aztec/aztec.js/addresses'; import { Fr } from '@aztec/aztec.js/fields'; -import type { AztecNode } from '@aztec/aztec.js/node'; -import { PRIVATE_LOG_CIPHERTEXT_LEN } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { OffchainEffectContract, type TestEvent } from '@aztec/noir-test-contracts.js/OffchainEffect'; -import { MessageContext } from '@aztec/stdlib/logs'; -import { OFFCHAIN_MESSAGE_IDENTIFIER } from '@aztec/stdlib/tx'; import { jest } from '@jest/globals'; @@ -18,20 +14,17 @@ const TIMEOUT = 120_000; describe('e2e_offchain_effect', () => { let contract1: OffchainEffectContract; let contract2: OffchainEffectContract; - let aztecNode: AztecNode; jest.setTimeout(TIMEOUT); let wallet: TestWallet; let defaultAccountAddress: AztecAddress; let teardown: () => Promise; - beforeAll(async () => { ({ teardown, wallet, accounts: [defaultAccountAddress], - aztecNode, } = await setup(1)); ({ contract: contract1 } = await OffchainEffectContract.deploy(wallet).send({ from: defaultAccountAddress })); ({ contract: contract2 } = await OffchainEffectContract.deploy(wallet).send({ from: defaultAccountAddress })); @@ -95,44 +88,35 @@ describe('e2e_offchain_effect', () => { it('should emit event as offchain message and process it', async () => { const [a, b, c] = [1n, 2n, 3n]; - const provenTx = await proveInteraction( - wallet, - contract1.methods.emit_event_as_offchain_message_for_msg_sender(a, b, c), - { from: defaultAccountAddress }, - ); - const { txHash, blockNumber, blockHash } = await provenTx.send(); - - const offchainEffects = provenTx.offchainEffects; - expect(offchainEffects).toHaveLength(1); - const offchainEffect = offchainEffects[0]; - - // The data contains the ciphertext, an identifier and the recipient - expect(offchainEffect.data.length).toEqual(PRIVATE_LOG_CIPHERTEXT_LEN + 2); - - const identifier = offchainEffect.data[0]; - expect(identifier).toEqual(OFFCHAIN_MESSAGE_IDENTIFIER); - - const recipientAddressFr = offchainEffect.data[1]; - // Recipient was set to message sender inside the emit_event_as_offchain_message_for_msg_sender function const recipient = defaultAccountAddress; - expect(recipient.toField()).toEqual(recipientAddressFr); - - const ciphertext = offchainEffect.data.slice(2, PRIVATE_LOG_CIPHERTEXT_LEN); - const txEffect = (await aztecNode.getTxEffect(txHash))!.data; + const { receipt, offchainMessages } = await contract1.methods + .emit_event_as_offchain_message_for_msg_sender(a, b, c) + .send({ from: defaultAccountAddress }); - const messageContext = MessageContext.fromTxEffectAndRecipient(txEffect, recipient); + expect(offchainMessages).toHaveLength(1); + const msg = offchainMessages[0]; + expect(msg.recipient).toEqual(recipient); - // Process the message + // Deliver the offchain message via offchain_receive await contract1.methods - .process_message(ciphertext, messageContext.toNoirStruct()) - .simulate({ from: defaultAccountAddress }); + .offchain_receive([ + { + ciphertext: msg.payload, + recipient, + // eslint-disable-next-line camelcase + tx_hash: receipt.txHash.hash, + // eslint-disable-next-line camelcase + anchor_block_timestamp: msg.anchorBlockTimestamp, + }, + ]) + .simulate({ from: recipient }); // Get the event from PXE const events = await wallet.getPrivateEvents(OffchainEffectContract.events.TestEvent, { contractAddress: contract1.address, - fromBlock: BlockNumber(blockNumber!), - toBlock: BlockNumber(blockNumber! + 1), + fromBlock: BlockNumber(receipt.blockNumber!), + toBlock: BlockNumber(receipt.blockNumber! + 1), scopes: [recipient], }); @@ -144,9 +128,9 @@ describe('e2e_offchain_effect', () => { c, }, metadata: { - l2BlockNumber: blockNumber, - l2BlockHash: blockHash, - txHash, + l2BlockNumber: receipt.blockNumber, + l2BlockHash: receipt.blockHash, + txHash: receipt.txHash, }, }); }); @@ -154,36 +138,29 @@ describe('e2e_offchain_effect', () => { it('should emit note as offchain message and process it', async () => { const value = 123n; const owner = defaultAccountAddress; - const provenTx = await proveInteraction(wallet, contract1.methods.emit_note_as_offchain_message(value, owner), { - from: defaultAccountAddress, - }); - const { txHash } = await provenTx.send(); - - const offchainEffects = provenTx.offchainEffects; - expect(offchainEffects).toHaveLength(1); - const offchainEffect = offchainEffects[0]; - - // The data contains the ciphertext, an identifier, and the recipient - expect(offchainEffect.data.length).toEqual(PRIVATE_LOG_CIPHERTEXT_LEN + 2); - - const identifier = offchainEffect.data[0]; - expect(identifier).toEqual(OFFCHAIN_MESSAGE_IDENTIFIER); - - const recipientAddressFr = offchainEffect.data[1]; - // Recipient was set to message sender inside the emit_note_as_offchain_message function const recipient = defaultAccountAddress; - expect(recipient.toField()).toEqual(recipientAddressFr); - - const ciphertext = offchainEffect.data.slice(2, PRIVATE_LOG_CIPHERTEXT_LEN); - const txEffect = (await aztecNode.getTxEffect(txHash))!.data; + const { receipt, offchainMessages } = await contract1.methods + .emit_note_as_offchain_message(value, owner) + .send({ from: defaultAccountAddress }); - const messageContext = MessageContext.fromTxEffectAndRecipient(txEffect, recipient); + expect(offchainMessages).toHaveLength(1); + const msg = offchainMessages[0]; + expect(msg.recipient).toEqual(recipient); - // Process the message + // Deliver the offchain message via offchain_receive await contract1.methods - .process_message(ciphertext, messageContext.toNoirStruct()) - .simulate({ from: defaultAccountAddress }); + .offchain_receive([ + { + ciphertext: msg.payload, + recipient, + // eslint-disable-next-line camelcase + tx_hash: receipt.txHash.hash, + // eslint-disable-next-line camelcase + anchor_block_timestamp: msg.anchorBlockTimestamp, + }, + ]) + .simulate({ from: recipient }); // Get the note value const { result: noteValue } = await contract1.methods diff --git a/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts b/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts index 4102cbedfadb..6e070ba5c513 100644 --- a/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts +++ b/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import { AztecAddress } from '@aztec/aztec.js/addresses'; -import { NO_WAIT, extractOffchainOutput } from '@aztec/aztec.js/contracts'; +import { extractOffchainOutput } from '@aztec/aztec.js/contracts'; import type { AztecNode } from '@aztec/aztec.js/node'; import type { CheatCodes } from '@aztec/aztec/testing'; import type { BlockNumber } from '@aztec/foundation/branded-types'; @@ -78,8 +78,6 @@ describe('e2e_offchain_payment', () => { await cheatCodes.eth.reorg(1); await aztecNodeAdmin.rollbackTo(Number(block) - 1); expect(await aztecNode.getBlockNumber()).toBe(Number(block) - 1); - - await aztecNodeAdmin.resumeSync(); } it('processes an offchain-delivered private payment via QR-style handoff', async () => { @@ -100,6 +98,7 @@ describe('e2e_offchain_payment', () => { const messageForBob = offchainMessages.find(msg => msg.recipient.equals(bob)); expect(messageForBob).toBeTruthy(); + // Deliver Bob's offchain message (the payment note). await contract.methods .offchain_receive([ { @@ -111,11 +110,27 @@ describe('e2e_offchain_payment', () => { ]) .simulate({ from: bob }); - // TODO(F-413): we need to implement scopes on capsules so we can check Alice's balance too here. This is not - // possible right now because the offchain inbox is shared for all accounts using this contract in the same PXE, - // which is bad. + // TODO(F-324): until we implement F-324, we need Alice to self-deliver her own change note + const messageForAlice = offchainMessages.find(msg => msg.recipient.equals(alice)); + expect(messageForAlice).toBeTruthy(); + + // Deliver Alice's offchain message (the change note). + await contract.methods + .offchain_receive([ + { + ciphertext: messageForAlice!.payload, + recipient: alice, + tx_hash: receipt.txHash.hash, + anchor_block_timestamp: messageForAlice!.anchorBlockTimestamp, + }, + ]) + .simulate({ from: alice }); + const { result: bobBalance } = await contract.methods.get_balance(bob).simulate({ from: bob }); expect(bobBalance).toBe(paymentAmount); + + const { result: aliceBalance } = await contract.methods.get_balance(alice).simulate({ from: alice }); + expect(aliceBalance).toBe(mintAmount - paymentAmount); }); it('reprocesses an offchain-delivered payment after an L1 reorg', async () => { @@ -135,6 +150,9 @@ describe('e2e_offchain_payment', () => { const txBlockNumber = receipt.blockNumber!; const txHash = provenTx.getTxHash(); + const txEffectBeforeReorg = await aztecNode.getTxEffect(txHash); + expect(txEffectBeforeReorg).toBeTruthy(); + const { offchainMessages } = extractOffchainOutput( provenTx.offchainEffects, provenTx.data.constants.anchorBlockHeader.globalVariables.timestamp, @@ -142,7 +160,7 @@ describe('e2e_offchain_payment', () => { const messageForBob = offchainMessages.find(msg => msg.recipient.equals(bob)); expect(messageForBob).toBeTruthy(); - // Deliver the offchain message for eventual processing + // Deliver Bob's offchain message (the payment note). await contract.methods .offchain_receive([ { @@ -154,10 +172,28 @@ describe('e2e_offchain_payment', () => { ]) .simulate({ from: bob }); + // Deliver Alice's offchain message (the change note). + const messageForAlice = offchainMessages.find(msg => msg.recipient.equals(alice)); + expect(messageForAlice).toBeTruthy(); + + await contract.methods + .offchain_receive([ + { + ciphertext: messageForAlice!.payload, + recipient: alice, + tx_hash: txHash.hash, + anchor_block_timestamp: messageForAlice!.anchorBlockTimestamp, + }, + ]) + .simulate({ from: alice }); + // Check that Bob got the payment before a re-org const { result: bobBalance } = await contract.methods.get_balance(bob).simulate({ from: bob }); expect(bobBalance).toBe(paymentAmount); + const { result: aliceBalance } = await contract.methods.get_balance(alice).simulate({ from: alice }); + expect(aliceBalance).toBe(mintAmount - paymentAmount); + await forceReorg(txBlockNumber); // Verify that the payment TX is no longer present after the reorg @@ -168,8 +204,12 @@ describe('e2e_offchain_payment', () => { const { result: bobAfterRollback } = await contract.methods.get_balance(bob).simulate({ from: bob }); expect(bobAfterRollback).toBe(0n); - // Resend the tx after the reorg and force block production so the sequencer picks it up. - await provenTx.send({ wait: NO_WAIT }); + // Verify Alice's balance also rolled back to full mint amount (transfer was reverted) + const { result: aliceAfterRollback } = await contract.methods.get_balance(alice).simulate({ from: alice }); + expect(aliceAfterRollback).toBe(mintAmount); + + // The archiver re-syncs the same checkpoints from L1 after the reorg, so the tx gets re-mined automatically. + // Force an empty block so the PXE re-syncs and reprocesses the offchain-delivered notes. await forceEmptyBlock(); // Check that the message was reprocessed and Bob has his payment again. @@ -177,5 +217,8 @@ describe('e2e_offchain_payment', () => { // for the system to re-process it. const { result: bobBalanceAfterResentTx } = await contract.methods.get_balance(bob).simulate({ from: bob }); expect(bobBalanceAfterResentTx).toBe(paymentAmount); + + const { result: aliceBalanceAfterResentTx } = await contract.methods.get_balance(alice).simulate({ from: alice }); + expect(aliceBalanceAfterResentTx).toBe(mintAmount - paymentAmount); }); }); diff --git a/yarn-project/end-to-end/src/e2e_simple.test.ts b/yarn-project/end-to-end/src/e2e_simple.test.ts index e38df666d579..2943d35e83a4 100644 --- a/yarn-project/end-to-end/src/e2e_simple.test.ts +++ b/yarn-project/end-to-end/src/e2e_simple.test.ts @@ -75,7 +75,6 @@ describe('e2e_simple', () => { const { receipt: txReceipt } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - wait: { returnReceipt: true }, }); await waitForProven(aztecNode, txReceipt, { provenTimeout: (config.aztecProofSubmissionEpochs + 1) * config.aztecEpochDuration * config.aztecSlotDuration, diff --git a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts index 7dc8268ce083..2796891bd9ed 100644 --- a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts @@ -104,15 +104,13 @@ export class FullProverTest { await publicDeployAccounts(this.wallet, this.accounts.slice(0, 2)); this.logger.info('Applying base setup: deploying token contract'); - const { - receipt: { contract: asset, instance }, - } = await TokenContract.deploy( + const { contract: asset, instance } = await TokenContract.deploy( this.wallet, this.accounts[0], FullProverTest.TOKEN_NAME, FullProverTest.TOKEN_SYMBOL, FullProverTest.TOKEN_DECIMALS, - ).send({ from: this.accounts[0], wait: { returnReceipt: true } }); + ).send({ from: this.accounts[0] }); this.logger.verbose(`Token deployed to ${asset.address}`); this.fakeProofsAsset = asset; diff --git a/yarn-project/end-to-end/src/fixtures/token_utils.ts b/yarn-project/end-to-end/src/fixtures/token_utils.ts index a0d67993b7eb..fafa3f7d5b2c 100644 --- a/yarn-project/end-to-end/src/fixtures/token_utils.ts +++ b/yarn-project/end-to-end/src/fixtures/token_utils.ts @@ -6,11 +6,8 @@ import { TokenContract } from '@aztec/noir-contracts.js/Token'; export async function deployToken(wallet: Wallet, admin: AztecAddress, initialAdminBalance: bigint, logger: Logger) { logger.info(`Deploying Token contract...`); - const { - receipt: { contract, instance }, - } = await TokenContract.deploy(wallet, admin, 'TokenName', 'TokenSymbol', 18).send({ + const { contract, instance } = await TokenContract.deploy(wallet, admin, 'TokenName', 'TokenSymbol', 18).send({ from: admin, - wait: { returnReceipt: true }, }); if (initialAdminBalance > 0n) { diff --git a/yarn-project/end-to-end/src/spartan/n_tps.test.ts b/yarn-project/end-to-end/src/spartan/n_tps.test.ts index 7c3f861f0885..71c2d019ac62 100644 --- a/yarn-project/end-to-end/src/spartan/n_tps.test.ts +++ b/yarn-project/end-to-end/src/spartan/n_tps.test.ts @@ -2,7 +2,7 @@ import { NO_WAIT } from '@aztec/aztec.js/contracts'; import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee'; import { type AztecNode, createAztecNodeClient } from '@aztec/aztec.js/node'; import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; -import { times, timesAsync } from '@aztec/foundation/collection'; +import { times, timesParallel } from '@aztec/foundation/collection'; import { randomBigInt } from '@aztec/foundation/crypto/random'; import { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, createLogger } from '@aztec/foundation/log'; @@ -270,12 +270,17 @@ describe('sustained N TPS test', () => { await retryUntil( async () => { - const blockNumber = await aztecNode.getBlockNumber(); - if (blockNumber > INITIAL_L2_BLOCK_NUM) { - return true; + try { + const blockNumber = await aztecNode.getBlockNumber(); + if (blockNumber > INITIAL_L2_BLOCK_NUM) { + return true; + } + logger.info('Waiting for the first block to mine...', { blockNumber, threshold: INITIAL_L2_BLOCK_NUM }); + return false; + } catch (err) { + logger.warn('Failed to get block number from RPC', { error: String(err) }); + return false; } - logger.info('Waiting for the first block to mine...'); - return false; }, 'get block number', 60 * 60 * 3, // wait up to 3 hours @@ -285,7 +290,7 @@ describe('sustained N TPS test', () => { const initialBlockNumber = await aztecNode.getBlockNumber(); logger.info('Initial block mined', { blockNumber: initialBlockNumber }); - testWallets = await timesAsync(lowValueAccounts + highValueAccounts, i => { + testWallets = await timesParallel(lowValueAccounts + highValueAccounts, i => { logger.info(`Creating wallet and pxe for wallet ${i + 1}/${lowValueAccounts + highValueAccounts}`); return createWalletAndAztecNodeClient(rpcUrl, config.REAL_VERIFIER, logger); }); diff --git a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts index 6827bd11fd92..a9d82e38e351 100644 --- a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts +++ b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts @@ -11,7 +11,7 @@ import type { Wallet } from '@aztec/aztec.js/wallet'; import { createEthereumChain } from '@aztec/ethereum/chain'; import { createExtendedL1Client } from '@aztec/ethereum/client'; import type { Logger } from '@aztec/foundation/log'; -import { retryUntil } from '@aztec/foundation/retry'; +import { makeBackoff, retry, retryUntil } from '@aztec/foundation/retry'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { registerInitialLocalNetworkAccountsInWallet } from '@aztec/wallets/testing'; @@ -139,8 +139,8 @@ async function deployAccountWithDiagnostics( ): Promise { const deployMethod = await account.getDeployMethod(); let txHash; + let gasSettings: any; try { - let gasSettings; if (estimateGas) { const sim = await deployMethod.simulate({ from: NO_FROM, fee: { paymentMethod } }); gasSettings = sim.estimatedGas; @@ -170,6 +170,51 @@ async function deployAccountWithDiagnostics( }); throw error; } + + // Track the tx hash across retries so we don't re-send when the previous tx is still pending. + let sentTxHash: { txHash: any } | undefined; + + await retry( + async () => { + // Check if already deployed (handles case where previous attempt succeeded but waitForTx timed out) + const existing = await aztecNode.getContract(account.address); + if (existing) { + logger.info(`${accountLabel} already deployed at ${account.address}, skipping`); + return; + } + + // If we already sent a tx, check if it was dropped before deciding to re-send. + if (sentTxHash) { + const prevReceipt = await aztecNode.getTxReceipt(sentTxHash.txHash); + if (prevReceipt.isDropped()) { + logger.info(`${accountLabel} previous tx ${sentTxHash.txHash} was dropped, re-sending`); + sentTxHash = undefined; + } else { + logger.info(`${accountLabel} previous tx ${sentTxHash.txHash} still pending, waiting again...`); + } + } + + if (!sentTxHash) { + const deployResult = await deployMethod.send({ + from: AztecAddress.ZERO, + fee: { paymentMethod, gasSettings }, + wait: NO_WAIT, + }); + sentTxHash = { txHash: deployResult.txHash }; + logger.info(`${accountLabel} tx sent`, { txHash: sentTxHash.txHash.toString() }); + } + + const receipt = await waitForTx(aztecNode, sentTxHash.txHash, { timeout: 600 }); + if (receipt.isDropped()) { + sentTxHash = undefined; + throw new Error(`${accountLabel} tx was dropped, retrying...`); + } + logger.info(`${accountLabel} deployed at ${account.address}`); + }, + `deploy ${accountLabel}`, + makeBackoff([1, 2, 4, 8, 16]), + logger, + ); } async function deployAccountsInBatches( @@ -346,14 +391,18 @@ async function deployTokenAndMint( logger: Logger, ) { logger.verbose(`Deploying TokenContract...`); - const { - receipt: { contract: tokenContract }, - } = await TokenContract.deploy(wallet, admin, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS).send({ + const { contract: tokenContract } = await TokenContract.deploy( + wallet, + admin, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS, + ).send({ from: admin, fee: { paymentMethod, }, - wait: { timeout: 600, returnReceipt: true }, + wait: { timeout: 600 }, }); const tokenAddress = tokenContract.address; diff --git a/yarn-project/foundation/src/schemas/api.ts b/yarn-project/foundation/src/schemas/api.ts index 0231f0652587..2090999e363d 100644 --- a/yarn-project/foundation/src/schemas/api.ts +++ b/yarn-project/foundation/src/schemas/api.ts @@ -37,7 +37,10 @@ export type ApiSchema = { }; /** Return whether an API schema defines a valid function schema for a given method name. */ -export function schemaHasMethod(schema: ApiSchema, methodName: string) { +export function schemaHasMethod( + schema: T, + methodName: string, +): methodName is Extract { return ( typeof methodName === 'string' && Object.hasOwn(schema, methodName) && diff --git a/yarn-project/ivc-integration/src/avm_integration.test.ts b/yarn-project/ivc-integration/src/avm_integration.test.ts index 4c045f526eca..60c16afac7de 100644 --- a/yarn-project/ivc-integration/src/avm_integration.test.ts +++ b/yarn-project/ivc-integration/src/avm_integration.test.ts @@ -101,7 +101,7 @@ describe('AVM Integration', () => { const [bytecodes, witnessStack, tailPublicInputs, vks] = await generateTestingIVCStack(1, 0); chonkPublicInputs = tailPublicInputs; backend = new AztecClientBackend(bytecodes, barretenberg); - const [proofAsFields, , vkBytes] = await backend.prove(witnessStack, vks); + const { proofFields: proofAsFields, vk: vkBytes } = await backend.prove(witnessStack, vks); chonkProof = await proofBytesToRecursiveProof(proofAsFields, vkBytes); }); diff --git a/yarn-project/ivc-integration/src/batch_verifier.test.ts b/yarn-project/ivc-integration/src/batch_verifier.test.ts index 66dd1ca18cca..2d174aca0eec 100644 --- a/yarn-project/ivc-integration/src/batch_verifier.test.ts +++ b/yarn-project/ivc-integration/src/batch_verifier.test.ts @@ -25,7 +25,7 @@ describe('Batch Chonk Verifier workloads', () => { logger.info('Generating simple proof...'); const [bytecodes1, witnesses1, , vks1] = await generateTestingIVCStack(1, 0); const backend1 = new AztecClientBackend(bytecodes1, bb); - const [proofFields1, , generatedVk1] = await backend1.prove(witnesses1, vks1); + const { proofFields: proofFields1, vk: generatedVk1 } = await backend1.prove(witnesses1, vks1); validProofFields = proofFields1; invalidProofFields = corruptProofFields(validProofFields); vk = generatedVk1; @@ -33,7 +33,7 @@ describe('Batch Chonk Verifier workloads', () => { logger.info('Generating complex proof...'); const [bytecodes2, witnesses2, , vks2] = await generateTestingIVCStack(1, 1); const backend2 = new AztecClientBackend(bytecodes2, bb); - const [proofFields2, , generatedVk2] = await backend2.prove(witnesses2, vks2); + const { proofFields: proofFields2, vk: generatedVk2 } = await backend2.prove(witnesses2, vks2); validProofFields2 = proofFields2; vk2 = generatedVk2; diff --git a/yarn-project/ivc-integration/src/batch_verifier_queue.test.ts b/yarn-project/ivc-integration/src/batch_verifier_queue.test.ts index 466c2072512a..d901e8c50f9c 100644 --- a/yarn-project/ivc-integration/src/batch_verifier_queue.test.ts +++ b/yarn-project/ivc-integration/src/batch_verifier_queue.test.ts @@ -32,7 +32,7 @@ describe('Batch Chonk Verifier Queue', () => { logger.info('Generating proof for tests...'); const [bytecodes, witnesses, , vks] = await generateTestingIVCStack(1, 0); const backend = new AztecClientBackend(bytecodes, bb); - const [proofFields, , generatedVk] = await backend.prove(witnesses, vks); + const { proofFields, vk: generatedVk } = await backend.prove(witnesses, vks); validProofFields = proofFields; invalidProofFields = corruptProofFields(validProofFields); vk = generatedVk; diff --git a/yarn-project/ivc-integration/src/chonk_browser.test.ts b/yarn-project/ivc-integration/src/chonk_browser.test.ts index 6c21cba5ac58..3c82c9dbafa8 100644 --- a/yarn-project/ivc-integration/src/chonk_browser.test.ts +++ b/yarn-project/ivc-integration/src/chonk_browser.test.ts @@ -320,7 +320,7 @@ describe('Chonk Integration - Browser with Puppeteer', () => { const backend = new AztecClientBackend(bytecodes, barretenberg); console.log('[Test] Proving...'); - const [, proof, vk] = await backend.prove(witnessStack, vks); + const { proof, vk } = await backend.prove(witnessStack, vks); console.log(\`[Test] Proof generated, size: \${proof.length} bytes\`); console.log('[Test] Verifying...'); diff --git a/yarn-project/ivc-integration/src/chonk_integration.test.ts b/yarn-project/ivc-integration/src/chonk_integration.test.ts index b089de47d01f..381e9db7d5b5 100644 --- a/yarn-project/ivc-integration/src/chonk_integration.test.ts +++ b/yarn-project/ivc-integration/src/chonk_integration.test.ts @@ -1,8 +1,7 @@ -import { AztecClientBackend, BackendType, Barretenberg, toChonkProof } from '@aztec/bb.js'; +import { AztecClientBackend, BackendType, Barretenberg } from '@aztec/bb.js'; import { createLogger } from '@aztec/foundation/log'; import { jest } from '@jest/globals'; -import { Decoder } from 'msgpackr'; import { ungzip } from 'pako'; import { @@ -41,7 +40,7 @@ describe.each([BackendType.Wasm, BackendType.NativeUnixSocket])('Client IVC Inte it('Should generate a verifiable client IVC proof from a simple mock tx via bb.js, verified by bb', async () => { const [bytecodes, witnessStack, , vks] = await generateTestingIVCStack(1, 0); const backend = new AztecClientBackend(bytecodes, barretenberg); - const [, proof, vk] = await backend.prove(witnessStack, vks); + const { proof, vk } = await backend.prove(witnessStack, vks); const verified = await backend.verify(proof, vk); expect(verified).toBe(true); }); @@ -57,30 +56,24 @@ describe.each([BackendType.Wasm, BackendType.NativeUnixSocket])('Client IVC Inte it('Should generate a verifiable client IVC proof from a complex mock tx', async () => { const [bytecodes, witnessStack, , vks] = await generateTestingIVCStack(1, 1); const backend = new AztecClientBackend(bytecodes, barretenberg); - const [, proof, vk] = await backend.prove(witnessStack, vks); + const { proof, vk } = await backend.prove(witnessStack, vks); const verified = await backend.verify(proof, vk); expect(verified).toBe(true); }); - it('Should compress and decompress a client IVC proof via bbapi', async () => { + it('Should compress and decompress a client IVC proof, producing a smaller proof', async () => { const [bytecodes, witnessStack, , vks] = await generateTestingIVCStack(1, 0); const ivcBackend = new AztecClientBackend(bytecodes, barretenberg); - const [, proof, vk] = await ivcBackend.prove(witnessStack, vks); + const { proof, vk, compressedProof } = await ivcBackend.prove(witnessStack, vks, { compress: true }); - // Decode the msgpack-encoded proof back to a ChonkProof object - const chonkProof = toChonkProof(new Decoder({ useRecords: false }).decode(proof)); + expect(compressedProof).toBeDefined(); + expect(compressedProof!.length).toBeGreaterThan(0); + expect(compressedProof!.length).toBeLessThan(proof.length); + logger.info(`Uncompressed proof: ${proof.length} bytes, compressed: ${compressedProof!.length} bytes`); + logger.info(`Compression ratio: ${(proof.length / compressedProof!.length).toFixed(2)}x`); - // Compress via bbapi - const compressResult = await barretenberg.chonkCompressProof({ proof: chonkProof }); - expect(compressResult.compressedProof.length).toBeGreaterThan(0); - logger.info(`Compressed proof: ${compressResult.compressedProof.length} bytes`); - - // Decompress via bbapi - const decompressResult = await barretenberg.chonkDecompressProof({ - compressedProof: compressResult.compressedProof, - }); - - // Verify the decompressed proof matches the original + // Decompress and verify roundtrip + const decompressResult = await barretenberg.chonkDecompressProof({ compressedProof: compressedProof! }); const verified = await barretenberg.chonkVerify({ proof: decompressResult.proof, vk }); expect(verified.valid).toBe(true); }); diff --git a/yarn-project/ivc-integration/src/rollup_ivc_integration.test.ts b/yarn-project/ivc-integration/src/rollup_ivc_integration.test.ts index 22af06649351..baf716b7b34d 100644 --- a/yarn-project/ivc-integration/src/rollup_ivc_integration.test.ts +++ b/yarn-project/ivc-integration/src/rollup_ivc_integration.test.ts @@ -74,7 +74,7 @@ describe('Rollup IVC Integration', () => { clientIVCPublicInputs = tailPublicInputs; const backend = new AztecClientBackend(bytecodes, barretenberg); - const [proofAsFields, , vkBytes] = await backend.prove(witnessStack, vks); + const { proofFields: proofAsFields, vk: vkBytes } = await backend.prove(witnessStack, vks); chonkProof = await proofBytesToRecursiveProof(proofAsFields, vkBytes); // Create an AVM proof diff --git a/yarn-project/ivc-integration/src/serve.ts b/yarn-project/ivc-integration/src/serve.ts index d45dd5d3e1b1..9fc3db35d04f 100644 --- a/yarn-project/ivc-integration/src/serve.ts +++ b/yarn-project/ivc-integration/src/serve.ts @@ -112,7 +112,7 @@ if (!document.getElementById('status')) { }); const backend = new AztecClientBackend(bytecodes, barretenberg); - const [, proof, vk] = await backend.prove(witnessStack, precomputedVks); + const { proof, vk } = await backend.prove(witnessStack, precomputedVks); const verified = await backend.verify(proof, vk); logger.info(`verified? ${verified}`); diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts index aa154872dada..c8c0d36bbbfb 100644 --- a/yarn-project/p2p/src/client/factory.ts +++ b/yarn-project/p2p/src/client/factory.ts @@ -56,7 +56,7 @@ export async function createP2PClient( telemetry: TelemetryClient = getTelemetryClient(), deps: P2PClientDeps = {}, ) { - const config = await configureP2PClientAddresses({ + const config = configureP2PClientAddresses({ ...inputConfig, dataStoreMapSizeKb: inputConfig.p2pStoreMapSizeKb ?? inputConfig.dataStoreMapSizeKb, }); diff --git a/yarn-project/p2p/src/services/discv5/discV5_service.ts b/yarn-project/p2p/src/services/discv5/discV5_service.ts index de288a63092a..d242c39ed6e9 100644 --- a/yarn-project/p2p/src/services/discv5/discV5_service.ts +++ b/yarn-project/p2p/src/services/discv5/discV5_service.ts @@ -96,7 +96,7 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService lookupTimeout: 2000, requestTimeout: 2000, allowUnverifiedSessions: true, - enrUpdate: !p2pIp ? true : false, // If no p2p IP is set, enrUpdate can automatically resolve it + enrUpdate: config.queryForIp && !p2pIp, // Enable native ENR IP discovery when no static IP is configured ...configOverrides.config, }, metricsRegistry, @@ -129,9 +129,11 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService private onMultiaddrUpdated(m: Multiaddr) { // We want to update our tcp port to match the udp port // p2pBroadcastPort is optional on config, however it is set to default within the p2p client factory - const multiAddrTcp = multiaddr(convertToMultiaddr(m.nodeAddress().address, this.config.p2pBroadcastPort!, 'tcp')); + const address = m.nodeAddress().address; + const multiAddrTcp = multiaddr(convertToMultiaddr(address, this.config.p2pBroadcastPort!, 'tcp')); this.enr.setLocationMultiaddr(multiAddrTcp); this.logger.info('Multiaddr updated', { multiaddr: multiAddrTcp.toString() }); + this.emit('ip:changed', address); } public async start(): Promise { diff --git a/yarn-project/p2p/src/services/discv5/discv5_service.test.ts b/yarn-project/p2p/src/services/discv5/discv5_service.test.ts index fe4528c27ec2..31e71eadeacd 100644 --- a/yarn-project/p2p/src/services/discv5/discv5_service.test.ts +++ b/yarn-project/p2p/src/services/discv5/discv5_service.test.ts @@ -111,12 +111,16 @@ describe('Discv5Service', () => { await stopNodes(node1, node2); }); - it('should automatically resolve p2p ip if not set', async () => { + it('should automatically resolve p2p ip if not set and queryForIp is true', async () => { const extraNodes = 3; const nodes: DiscV5Service[] = []; - // Create a node with no p2pIp - const node = await createNode({ p2pIp: undefined, config: { addrVotesToUpdateEnr: 1, pingInterval: 200 } }); + // Create a node with no p2pIp and queryForIp=true -- enrUpdate should be enabled + const node = await createNode({ + p2pIp: undefined, + queryForIp: true, + config: { addrVotesToUpdateEnr: 1, pingInterval: 200 }, + }); await node.start(); nodes.push(node); @@ -134,12 +138,19 @@ describe('Discv5Service', () => { expect(node.getEnr().ip).toEqual(undefined); + // ip:changed should be emitted when the ENR IP is resolved + let discoveredIp: string | undefined; + node.on('ip:changed', (ip: string) => { + discoveredIp = ip; + }); + await runDiscoveryUntil(nodes, () => node.getEnr().ip !== undefined); - // Expect it's IP has been updated, and that the tcp and udp ports are the same + // Expect IP has been updated, tcp and udp ports match, and ip:changed event was emitted expect(node.getEnr().ip).not.toEqual(undefined); expect(node.getEnr().tcp).not.toEqual(undefined); expect(node.getEnr().tcp).toEqual(node.getEnr().udp); + expect(discoveredIp).toEqual(node.getEnr().ip?.toString()); await stopNodes(...nodes); }); diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts index f3330dd612b7..fb02a49804b6 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts @@ -24,6 +24,8 @@ import { ServerWorldStateSynchronizer } from '@aztec/world-state'; import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'; import type { Message, PeerId } from '@libp2p/interface'; import { TopicValidatorResult } from '@libp2p/interface'; +import type { ConnectionManager } from '@libp2p/interface-internal'; +import { multiaddr } from '@multiformats/multiaddr'; import { type MockProxy, mock } from 'jest-mock-extended'; import { type P2PConfig, p2pConfigMappings } from '../../config.js'; @@ -36,6 +38,8 @@ import type { MemPools } from '../../mem_pools/interface.js'; import type { TxPoolV2 } from '../../mem_pools/tx_pool_v2/interfaces.js'; import type { TransactionValidator } from '../../msg_validators/tx_validator/factory.js'; import type { PubSubLibp2p } from '../../util.js'; +import { convertToMultiaddr } from '../../util.js'; +import { DummyPeerDiscoveryService } from '../dummy_service.js'; import type { PeerManagerInterface } from '../peer-manager/interface.js'; import type { ReqRespInterface } from '../reqresp/interface.js'; import { BitVector } from '../reqresp/protocols/block_txs/bitvector.js'; @@ -1027,6 +1031,148 @@ describe('LibP2PService', () => { expect(reportMessageValidationResultSpy).toHaveBeenCalledWith('msg-1', MOCK_PEER_ID, TopicValidatorResult.Reject); }); }); + + describe('discv5 ip:changed bridge (queryForIp)', () => { + const p2pPort = 40400; + const firstIp = '203.0.113.5'; + const secondIp = '198.51.100.2'; + + function createQueryForIpService() { + const peerDiscovery = new DummyPeerDiscoveryService(); + const addressManager = { + removeObservedAddr: jest.fn(), + addObservedAddr: jest.fn(), + confirmObservedAddr: jest.fn(), + }; + const mockPeerId = mock({ toString: () => MOCK_PEER_ID }); + const nodeState = { status: 'stopped' as string }; + const mockNode = { + get status() { + return nodeState.status; + }, + set status(v: string) { + nodeState.status = v; + }, + peerId: mockPeerId, + start: jest.fn(() => { + nodeState.status = 'started'; + }), + stop: jest.fn(() => { + nodeState.status = 'stopped'; + }), + services: { + pubsub: { + subscribe: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + getMeshPeers: jest.fn(() => []), + }, + components: { + addressManager, + connectionManager: {} as unknown as ConnectionManager, + }, + }, + } as unknown as PubSubLibp2p; + + const config: P2PConfig = { + ...getDefaultConfig(p2pConfigMappings), + seenMessageCacheSize: 1000, + debugP2PInstrumentMessages: false, + disableTransactions: true, + l1ChainId: 1, + rollupVersion: 1, + l1Contracts: { rollupAddress: EthAddress.random() }, + queryForIp: true, + p2pIp: undefined, + p2pPort, + p2pDiscoveryDisabled: true, + peerCheckIntervalMS: 60_000, // Long enough that heartbeat won't run during this unit test + }; + + const mockPeerManager = mock(); + mockPeerManager.initializePeers.mockResolvedValue(undefined); + mockPeerManager.stop.mockResolvedValue(undefined); + mockPeerManager.heartbeat.mockResolvedValue(undefined); + + const mockReqResp = mock(); + mockReqResp.start.mockResolvedValue(undefined); + mockReqResp.stop.mockResolvedValue(undefined); + + const mempools = mock(); + const archiver = mock(); + const epochCache = mock(); + const mockProofVerifier = mock({ + verifyProof: () => Promise.resolve({ valid: true, durationMs: 1, totalDurationMs: 1 }), + }); + const mockWorldStateSynchronizer = mock(); + + const service = new LibP2PService( + config, + mockNode, + peerDiscovery, + mockReqResp, + mockPeerManager, + mempools, + archiver, + epochCache, + mockProofVerifier, + mockWorldStateSynchronizer, + getTelemetryClient(), + createLogger('p2p:test:queryForIp'), + ); + + return { service, peerDiscovery, addressManager, config }; + } + + it('registers observed announce address when discv5 emits ip:changed', async () => { + const { service, peerDiscovery, addressManager } = createQueryForIpService(); + const expectedAddr = multiaddr(convertToMultiaddr(firstIp, p2pPort, 'tcp')); + + await service.start(); + peerDiscovery.emit('ip:changed', firstIp); + + expect(addressManager.addObservedAddr).toHaveBeenCalledWith(expectedAddr); + expect(addressManager.confirmObservedAddr).toHaveBeenCalledWith(expectedAddr); + expect(addressManager.removeObservedAddr).not.toHaveBeenCalled(); + + await service.stop(); + }); + + it('removes previous observed address when ip:changed fires again with a new IP', async () => { + const { service, peerDiscovery, addressManager } = createQueryForIpService(); + const firstAddr = multiaddr(convertToMultiaddr(firstIp, p2pPort, 'tcp')); + const secondAddr = multiaddr(convertToMultiaddr(secondIp, p2pPort, 'tcp')); + + await service.start(); + peerDiscovery.emit('ip:changed', firstIp); + addressManager.removeObservedAddr.mockClear(); + addressManager.addObservedAddr.mockClear(); + addressManager.confirmObservedAddr.mockClear(); + + peerDiscovery.emit('ip:changed', secondIp); + + expect(addressManager.removeObservedAddr).toHaveBeenCalledWith(firstAddr); + expect(addressManager.addObservedAddr).toHaveBeenCalledWith(secondAddr); + expect(addressManager.confirmObservedAddr).toHaveBeenCalledWith(secondAddr); + + await service.stop(); + }); + + it('unsubscribes from ip:changed on stop so later emits are ignored', async () => { + const { service, peerDiscovery, addressManager } = createQueryForIpService(); + + await service.start(); + peerDiscovery.emit('ip:changed', firstIp); + addressManager.addObservedAddr.mockClear(); + addressManager.confirmObservedAddr.mockClear(); + + await service.stop(); + peerDiscovery.emit('ip:changed', secondIp); + + expect(addressManager.addObservedAddr).not.toHaveBeenCalled(); + expect(addressManager.confirmObservedAddr).not.toHaveBeenCalled(); + }); + }); }); /** Mock type for tx objects used in block txs validation tests. */ diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index 7235ecb51bd8..64501ea14f11 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -51,9 +51,10 @@ import { yamux } from '@chainsafe/libp2p-yamux'; import { bootstrap } from '@libp2p/bootstrap'; import { identify } from '@libp2p/identify'; import { type Message, type MultiaddrConnection, type PeerId, TopicValidatorResult } from '@libp2p/interface'; -import type { ConnectionManager } from '@libp2p/interface-internal'; +import type { AddressManager, ConnectionManager } from '@libp2p/interface-internal'; import { mplex } from '@libp2p/mplex'; import { tcp } from '@libp2p/tcp'; +import { multiaddr } from '@multiformats/multiaddr'; import { ENR } from '@nethermindeth/enr'; import { createLibp2p } from 'libp2p'; @@ -174,6 +175,10 @@ export class LibP2PService extends WithTracer implements P2PService { private checkpointReceivedCallback: P2PCheckpointReceivedCallback; private gossipSubEventHandler: (e: CustomEvent) => void; + private ipChangedHandler?: (ip: string) => void; + + /** Discovered public IP address (set when queryForIp is enabled and no static IP was configured). */ + private discoveredP2pIp?: string; private instrumentation: P2PInstrumentation; @@ -442,8 +447,9 @@ export class LibP2PService extends WithTracer implements P2PService { topics: topicScoreParams, }), }) as (components: GossipSubComponents) => GossipSub, - components: (components: { connectionManager: ConnectionManager }) => ({ + components: (components: { connectionManager: ConnectionManager; addressManager: AddressManager }) => ({ connectionManager: components.connectionManager, + addressManager: components.addressManager, }), }, logger: createLibp2pComponentLogger(logger.module, logger.getBindings()), @@ -502,10 +508,10 @@ export class LibP2PService extends WithTracer implements P2PService { // Get listen & announce addresses for logging const { p2pIp, p2pPort } = this.config; - if (!p2pIp) { + if (!p2pIp && !this.config.queryForIp) { throw new Error('Announce address not provided.'); } - const announceTcpMultiaddr = convertToMultiaddr(p2pIp, p2pPort, 'tcp'); + const announceTcpMultiaddr = p2pIp ? convertToMultiaddr(p2pIp, p2pPort, 'tcp') : undefined; // Create request response protocol handlers const txHandler = reqRespTxHandler(this.mempools); @@ -559,6 +565,31 @@ export class LibP2PService extends WithTracer implements P2PService { if (!this.config.p2pDiscoveryDisabled) { await this.peerDiscoveryService.start(); } + + // When queryForIp is enabled and no static IP was configured, bridge discv5 IP discovery to libp2p. + // Discv5 discovers our public IP via peer WHOAREYOU exchanges (enrUpdate=true) and emits 'ip:changed'. + // We confirm the discovered address in the libp2p AddressManager so it appears in getMultiaddrs() + // and is pushed to all connected peers via the identify protocol. + if (this.config.queryForIp && !p2pIp) { + this.ipChangedHandler = (ip: string) => { + const addressManager = this.node.services.components.addressManager; + const newAddr = multiaddr(convertToMultiaddr(ip, this.config.p2pPort, 'tcp')); + + // Remove old discovered IP if one exists + if (this.discoveredP2pIp) { + const oldAddr = multiaddr(convertToMultiaddr(this.discoveredP2pIp, this.config.p2pPort, 'tcp')); + addressManager.removeObservedAddr(oldAddr); + } + + addressManager.addObservedAddr(newAddr); + addressManager.confirmObservedAddr(newAddr); + // Store discovered IP + this.discoveredP2pIp = ip; + this.logger.info('Public IP discovered via discv5', { ip }); + }; + this.peerDiscoveryService.on('ip:changed', this.ipChangedHandler); + } + this.discoveryRunningPromise = new RunningPromise( async () => { await this.peerManager.heartbeat(); @@ -571,7 +602,7 @@ export class LibP2PService extends WithTracer implements P2PService { this.logger.info(`Started P2P service`, { listen: this.config.listenAddress, port: this.config.p2pPort, - announce: announceTcpMultiaddr, + announce: announceTcpMultiaddr ?? 'pending (queryForIp=true)', peerId: this.node.peerId.toString(), }); } @@ -584,6 +615,12 @@ export class LibP2PService extends WithTracer implements P2PService { // Remove gossip sub listener this.node.services.pubsub.removeEventListener(GossipSubEvent.MESSAGE, this.gossipSubEventHandler); + // Remove ip:changed listener if registered + if (this.ipChangedHandler) { + this.peerDiscoveryService.off('ip:changed', this.ipChangedHandler); + this.ipChangedHandler = undefined; + } + // Stop peer manager this.logger.debug('Stopping peer manager...'); await this.peerManager.stop(); diff --git a/yarn-project/p2p/src/services/service.ts b/yarn-project/p2p/src/services/service.ts index 59594e169788..94f0977d9e41 100644 --- a/yarn-project/p2p/src/services/service.ts +++ b/yarn-project/p2p/src/services/service.ts @@ -196,6 +196,13 @@ export interface PeerDiscoveryService extends EventEmitter { on(event: 'peer:discovered', listener: (enr: ENR) => void): this; emit(event: 'peer:discovered', enr: ENR): boolean; + /** + * Event emitted when our public IP is discovered or changes via discv5 peer interactions. + * Only emitted when enrUpdate is enabled (i.e. queryForIp=true and no static p2pIp). + */ + on(event: 'ip:changed', listener: (ip: string) => void): this; + emit(event: 'ip:changed', ip: string): boolean; + getStatus(): PeerDiscoveryState; getEnr(): ENR | undefined; diff --git a/yarn-project/p2p/src/test-helpers/mock-pubsub.ts b/yarn-project/p2p/src/test-helpers/mock-pubsub.ts index cf48654e0aff..fe52ac8686d9 100644 --- a/yarn-project/p2p/src/test-helpers/mock-pubsub.ts +++ b/yarn-project/p2p/src/test-helpers/mock-pubsub.ts @@ -15,6 +15,7 @@ import { type TopicValidatorResult, TypedEventEmitter, } from '@libp2p/interface'; +import type { AddressManager, ConnectionManager } from '@libp2p/interface-internal'; import type { P2PConfig } from '../config.js'; import type { MemPools } from '../mem_pools/interface.js'; @@ -212,6 +213,14 @@ export class MockPubSub implements PubSubLibp2p { get services() { return { pubsub: this.gossipSub, + components: { + addressManager: { + removeObservedAddr: () => {}, + addObservedAddr: () => {}, + confirmObservedAddr: () => {}, + } as unknown as AddressManager, + connectionManager: {} as unknown as ConnectionManager, + }, }; } diff --git a/yarn-project/p2p/src/util.ts b/yarn-project/p2p/src/util.ts index 37bba2f5f0b9..5850b70bb920 100644 --- a/yarn-project/p2p/src/util.ts +++ b/yarn-project/p2p/src/util.ts @@ -7,7 +7,7 @@ import type { GossipSub } from '@chainsafe/libp2p-gossipsub'; import { generateKeyPair, marshalPrivateKey, unmarshalPrivateKey } from '@libp2p/crypto/keys'; import type { Identify } from '@libp2p/identify'; import type { PeerId, PrivateKey } from '@libp2p/interface'; -import type { ConnectionManager } from '@libp2p/interface-internal'; +import type { AddressManager, ConnectionManager } from '@libp2p/interface-internal'; import { createFromPrivKey } from '@libp2p/peer-id-factory'; import { resolve } from 'dns/promises'; import { promises as fs } from 'fs'; @@ -31,6 +31,10 @@ export interface PubSubLibp2p extends Pick & { score: Pick }; + components: { + connectionManager: ConnectionManager; + addressManager: AddressManager; + }; }; } @@ -39,6 +43,7 @@ export type FullLibp2p = Libp2p<{ pubsub: GossipSub; components: { connectionManager: ConnectionManager; + addressManager: AddressManager; }; }>; @@ -102,24 +107,15 @@ function addressToMultiAddressType(address: string): 'ip4' | 'ip6' | 'dns' { } } -export async function configureP2PClientAddresses( - _config: P2PConfig & DataStoreConfig, -): Promise { +export function configureP2PClientAddresses(_config: P2PConfig & DataStoreConfig): P2PConfig & DataStoreConfig { const config = { ..._config }; - const { p2pIp, queryForIp, p2pBroadcastPort, p2pPort } = config; + const { p2pBroadcastPort, p2pPort } = config; // If no broadcast port is provided, use the given p2p port as the broadcast port if (!p2pBroadcastPort) { config.p2pBroadcastPort = p2pPort; } - // check if no announce IP was provided - if (!p2pIp) { - if (queryForIp) { - const publicIp = await getPublicIp(); - config.p2pIp = publicIp; - } - } // TODO(md): guard against setting a local ip address as the announce ip return config; diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts index 3b5d2ad06725..58248202981f 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts @@ -24,7 +24,6 @@ describe('EventValidationRequest', () => { 2, // bounded_vec_len 6, // event_commitment 7, // tx_hash - 8, // recipient ].map(n => new Fr(n)); const request = EventValidationRequest.fromFields(serialized, 10); @@ -35,7 +34,6 @@ describe('EventValidationRequest', () => { expect(request.serializedEvent).toEqual([new Fr(4), new Fr(5)]); expect(request.eventCommitment).toEqual(new Fr(6)); expect(request.txHash).toEqual(TxHash.fromBigInt(7n)); - expect(request.recipient).toEqual(AztecAddress.fromBigInt(8n)); }); it('throws if fed more fields than expected', () => { @@ -57,11 +55,10 @@ describe('EventValidationRequest', () => { 2, // bounded_vec_len 6, // event_commitment 7, // tx_hash - 8, // recipient ].map(n => new Fr(n)); expect(() => EventValidationRequest.fromFields(serialized, 10)).toThrow( - 'Error converting array of fields to EventValidationRequest: expected 17 fields but received 18 (maxEventSerializedLen=10).', + 'Error converting array of fields to EventValidationRequest: expected 16 fields but received 17 (maxEventSerializedLen=10).', ); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts index ccf4b7a944ae..992afadc74b5 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts @@ -16,7 +16,6 @@ export class EventValidationRequest { public serializedEvent: Fr[], public eventCommitment: Fr, public txHash: TxHash, - public recipient: AztecAddress, ) {} static fromFields(fields: Fr[], maxEventSerializedLen: number): EventValidationRequest { @@ -33,7 +32,6 @@ export class EventValidationRequest { const eventCommitment = reader.readField(); const txHash = TxHash.fromField(reader.readField()); - const recipient = AztecAddress.fromField(reader.readField()); if (reader.remainingFields() !== 0) { throw new Error( @@ -48,7 +46,6 @@ export class EventValidationRequest { serializedEvent, eventCommitment, txHash, - recipient, ); } } diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.test.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.test.ts deleted file mode 100644 index 72e8f5c01c9a..000000000000 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { Fr } from '@aztec/foundation/curves/bn254'; -import { TxHash } from '@aztec/stdlib/tx'; - -import { MessageTxContext } from './message_tx_context.js'; - -describe('MessageTxContext', () => { - it('serialization of some matches snapshot', () => { - const txHash = new TxHash(new Fr(123)); - const uniqueNoteHashes = [new Fr(4n), new Fr(5n)]; - const firstNullifier = new Fr(6n); - const ctx = new MessageTxContext(txHash, uniqueNoteHashes, firstNullifier); - const serialized = MessageTxContext.toSerializedOption(ctx); - expect(serialized.map(f => f.toString())).toMatchInlineSnapshot( - ` - [ - "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x000000000000000000000000000000000000000000000000000000000000007b", - "0x0000000000000000000000000000000000000000000000000000000000000004", - "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000000000000000000000000000006", - ] - `, - ); - }); - it('serialization of none matches snapshot', () => { - const serialized = MessageTxContext.toSerializedOption(null); - expect(serialized.map(f => f.toString())).toMatchInlineSnapshot( - ` - [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - ] - `, - ); - }); - it('serialization length of empty matches', () => { - const txHash = new TxHash(new Fr(123)); - const uniqueNoteHashes = [new Fr(4n), new Fr(5n)]; - const firstNullifier = new Fr(6n); - const ctx = new MessageTxContext(txHash, uniqueNoteHashes, firstNullifier); - expect(ctx.toFields().length).toEqual(MessageTxContext.toEmptyFields().length); - }); -}); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.ts deleted file mode 100644 index a379e0219f76..000000000000 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MAX_NOTE_HASHES_PER_TX } from '@aztec/constants'; -import { range } from '@aztec/foundation/array'; -import { Fr } from '@aztec/foundation/curves/bn254'; -import type { TxHash } from '@aztec/stdlib/tx'; - -/** - * Intermediate struct used to return resolved message contexts from PXE. The - * `utilityResolveMessageContexts` oracle stores values of this type in a CapsuleArray. - */ -export class MessageTxContext { - constructor( - public txHash: TxHash, - public uniqueNoteHashesInTx: Fr[], - public firstNullifierInTx: Fr, - ) {} - - toFields(): Fr[] { - return [ - this.txHash.hash, - ...serializeBoundedVec(this.uniqueNoteHashesInTx, MAX_NOTE_HASHES_PER_TX), - this.firstNullifierInTx, - ]; - } - - static toEmptyFields(): Fr[] { - const serializationLen = - 1 /* txHash */ + MAX_NOTE_HASHES_PER_TX + 1 /* uniqueNoteHashesInTx BVec */ + 1; /* firstNullifierInTx */ - return range(serializationLen).map(_ => Fr.zero()); - } - - static toSerializedOption(response: MessageTxContext | null): Fr[] { - if (response) { - return [new Fr(1), ...response.toFields()]; - } else { - return [new Fr(0), ...MessageTxContext.toEmptyFields()]; - } - } -} - -/** - * Helper function to serialize a bounded vector according to Noir's BoundedVec format - * @param values - The values to serialize - * @param maxLength - The maximum length of the bounded vector - * @returns The serialized bounded vector as Fr[] - */ -function serializeBoundedVec(values: Fr[], maxLength: number): Fr[] { - if (values.length > maxLength) { - throw new Error(`Attempted to serialize ${values} values into a BoundedVec with max length ${maxLength}`); - } - - const lengthDiff = maxLength - values.length; - const zeroPaddingArray = Array(lengthDiff).fill(Fr.ZERO); - const storage = values.concat(zeroPaddingArray); - return [...storage, new Fr(values.length)]; -} diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts index 686d57c0e74a..aea6e8b6ebba 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts @@ -24,7 +24,6 @@ describe('NoteValidationRequest', () => { '0x0000000000000000000000000000000000000000000000000000000000000006', // note hash '0x0000000000000000000000000000000000000000000000000000000000000007', // nullifier '0x0000000000000000000000000000000000000000000000000000000000000008', // tx hash - '0x0000000000000000000000000000000000000000000000000000000000000009', // recipient ].map(Fr.fromHexString); const request = NoteValidationRequest.fromFields(serialized, 8); @@ -38,7 +37,6 @@ describe('NoteValidationRequest', () => { expect(request.noteHash).toEqual(new Fr(6)); expect(request.nullifier).toEqual(new Fr(7)); expect(request.txHash).toEqual(TxHash.fromBigInt(8n)); - expect(request.recipient).toEqual(AztecAddress.fromBigInt(9n)); }); it('throws if fed more fields than expected', () => { @@ -61,11 +59,10 @@ describe('NoteValidationRequest', () => { '0x0000000000000000000000000000000000000000000000000000000000000006', // note hash '0x0000000000000000000000000000000000000000000000000000000000000007', // nullifier '0x0000000000000000000000000000000000000000000000000000000000000008', // tx hash - '0x0000000000000000000000000000000000000000000000000000000000000009', // recipient ].map(Fr.fromHexString); expect(() => NoteValidationRequest.fromFields(serialized, 8)).toThrow( - 'Error converting array of fields to NoteValidationRequest: expected 18 fields but received 19 (maxNotePackedLen=8).', + 'Error converting array of fields to NoteValidationRequest: expected 17 fields but received 18 (maxNotePackedLen=8).', ); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts index 286ad25ef377..355a6a03a858 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts @@ -18,7 +18,6 @@ export class NoteValidationRequest { public noteHash: Fr, public nullifier: Fr, public txHash: TxHash, - public recipient: AztecAddress, ) {} static fromFields(fields: Fr[], maxNotePackedLen: number): NoteValidationRequest { @@ -37,7 +36,6 @@ export class NoteValidationRequest { const noteHash = reader.readField(); const nullifier = reader.readField(); const txHash = TxHash.fromField(reader.readField()); - const recipient = AztecAddress.fromField(reader.readField()); if (reader.remainingFields() !== 0) { throw new Error( @@ -55,7 +53,6 @@ export class NoteValidationRequest { noteHash, nullifier, txHash, - recipient, ); } } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index 0af35e567caa..bb4a50387a21 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -119,30 +119,39 @@ export interface IUtilityExecutionOracle { startStorageSlot: Fr, numberOfElements: number, ): Promise; - fetchTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr): Promise; + fetchTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr, scope: AztecAddress): Promise; validateAndStoreEnqueuedNotesAndEvents( contractAddress: AztecAddress, noteValidationRequestsArrayBaseSlot: Fr, eventValidationRequestsArrayBaseSlot: Fr, maxNotePackedLen: number, maxEventSerializedLen: number, + scope: AztecAddress, ): Promise; bulkRetrieveLogs( contractAddress: AztecAddress, logRetrievalRequestsArrayBaseSlot: Fr, logRetrievalResponsesArrayBaseSlot: Fr, + scope: AztecAddress, ): Promise; utilityResolveMessageContexts( contractAddress: AztecAddress, messageContextRequestsArrayBaseSlot: Fr, messageContextResponsesArrayBaseSlot: Fr, + scope: AztecAddress, + ): Promise; + storeCapsule(contractAddress: AztecAddress, key: Fr, capsule: Fr[], scope: AztecAddress): void; + loadCapsule(contractAddress: AztecAddress, key: Fr, scope: AztecAddress): Promise; + deleteCapsule(contractAddress: AztecAddress, key: Fr, scope: AztecAddress): void; + copyCapsule( + contractAddress: AztecAddress, + srcKey: Fr, + dstKey: Fr, + numEntries: number, + scope: AztecAddress, ): Promise; - storeCapsule(contractAddress: AztecAddress, key: Fr, capsule: Fr[]): Promise; - loadCapsule(contractAddress: AztecAddress, key: Fr): Promise; - deleteCapsule(contractAddress: AztecAddress, key: Fr): Promise; - copyCapsule(contractAddress: AztecAddress, srcKey: Fr, dstKey: Fr, numEntries: number): Promise; aes128Decrypt(ciphertext: Buffer, iv: Buffer, symKey: Buffer): Promise; - getSharedSecret(address: AztecAddress, ephPk: Point): Promise; + getSharedSecret(address: AztecAddress, ephPk: Point, contractAddress: AztecAddress): Promise; invalidateContractSyncCache(contractAddress: AztecAddress, scopes: AztecAddress[]): void; emitOffchainEffect(data: Fr[]): Promise; } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts index ccf8b110c5a6..e241a3b0f1a7 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts @@ -1,5 +1,6 @@ -import { Fr } from '@aztec/foundation/curves/bn254'; +import { toACVMField } from '@aztec/simulator/client'; import type { ACIRCallback, ACVMField } from '@aztec/simulator/client'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { Oracle } from './oracle.js'; @@ -23,11 +24,12 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { contractAddress: ACVMField[], slot: ACVMField[], tSize: ACVMField[], - ): Promise<(ACVMField | ACVMField[])[]> => oracle.aztec_utl_loadCapsule(contractAddress, slot, tSize), + ): Promise<(ACVMField | ACVMField[])[]> => + oracle.aztec_utl_getCapsule(contractAddress, slot, tSize, [toACVMField(AztecAddress.ZERO)]), privateStoreInExecutionCache: (values: ACVMField[], hash: ACVMField[]): Promise => - oracle.aztec_prv_storeInExecutionCache(values, hash), + oracle.aztec_prv_setHashPreimage(values, hash), privateLoadFromExecutionCache: (returnsHash: ACVMField[]): Promise => - oracle.aztec_prv_loadFromExecutionCache(returnsHash), + oracle.aztec_prv_getHashPreimage(returnsHash), privateCallPrivateFunction: ( contractAddress: ACVMField[], functionSelector: ACVMField[], @@ -60,38 +62,22 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { startStorageSlot: ACVMField[], numberOfElements: ACVMField[], ): Promise => - oracle.aztec_utl_storageRead(blockHash, contractAddress, startStorageSlot, numberOfElements), + oracle.aztec_utl_getFromPublicStorage(blockHash, contractAddress, startStorageSlot, numberOfElements), utilityStoreCapsule: ( contractAddress: ACVMField[], slot: ACVMField[], capsule: ACVMField[], - ): Promise => oracle.aztec_utl_storeCapsule(contractAddress, slot, capsule), + ): Promise => + oracle.aztec_utl_setCapsule(contractAddress, slot, capsule, [toACVMField(AztecAddress.ZERO)]), utilityCopyCapsule: ( contractAddress: ACVMField[], srcSlot: ACVMField[], dstSlot: ACVMField[], numEntries: ACVMField[], - ): Promise => oracle.aztec_utl_copyCapsule(contractAddress, srcSlot, dstSlot, numEntries), - utilityDeleteCapsule: (contractAddress: ACVMField[], slot: ACVMField[]): Promise => - oracle.aztec_utl_deleteCapsule(contractAddress, slot), - utilityGetSharedSecret: ( - address: ACVMField[], - ephPKField0: ACVMField[], - ephPKField1: ACVMField[], - ephPKField2: ACVMField[], - ): Promise => oracle.aztec_utl_getSharedSecret(address, ephPKField0, ephPKField1, ephPKField2), - utilityFetchTaggedLogs: (pendingTaggedLogArrayBaseSlot: ACVMField[]): Promise => - oracle.aztec_utl_fetchTaggedLogs(pendingTaggedLogArrayBaseSlot), - utilityBulkRetrieveLogs: ( - contractAddress: ACVMField[], - logRetrievalRequestsArrayBaseSlot: ACVMField[], - logRetrievalResponsesArrayBaseSlot: ACVMField[], ): Promise => - oracle.aztec_utl_bulkRetrieveLogs( - contractAddress, - logRetrievalRequestsArrayBaseSlot, - logRetrievalResponsesArrayBaseSlot, - ), + oracle.aztec_utl_copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, [toACVMField(AztecAddress.ZERO)]), + utilityDeleteCapsule: (contractAddress: ACVMField[], slot: ACVMField[]): Promise => + oracle.aztec_utl_deleteCapsule(contractAddress, slot, [toACVMField(AztecAddress.ZERO)]), utilityGetL1ToL2MembershipWitness: ( contractAddress: ACVMField[], messageHash: ACVMField[], @@ -99,37 +85,23 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { ): Promise<(ACVMField | ACVMField[])[]> => oracle.aztec_utl_getL1ToL2MembershipWitness(contractAddress, messageHash, secret), utilityEmitOffchainEffect: (data: ACVMField[]): Promise => oracle.aztec_utl_emitOffchainEffect(data), - // Adapter: old 3-param signature → new 5-param with injected constants. - // Values derived from: MAX_MESSAGE_CONTENT_LEN(11) - RESERVED_FIELDS (3 for notes, 1 for events). - utilityValidateAndStoreEnqueuedNotesAndEvents: ( - contractAddress: ACVMField[], - noteValidationRequestsArrayBaseSlot: ACVMField[], - eventValidationRequestsArrayBaseSlot: ACVMField[], - ): Promise => - oracle.aztec_utl_validateAndStoreEnqueuedNotesAndEvents( - contractAddress, - noteValidationRequestsArrayBaseSlot, - eventValidationRequestsArrayBaseSlot, - [new Fr(8).toString()], - [new Fr(10).toString()], - ), // Renames (same signature, different oracle name) privateNotifySetMinRevertibleSideEffectCounter: (counter: ACVMField[]): Promise => oracle.aztec_prv_notifyRevertiblePhaseStart(counter), privateIsSideEffectCounterRevertible: (sideEffectCounter: ACVMField[]): Promise => - oracle.aztec_prv_inRevertiblePhase(sideEffectCounter), + oracle.aztec_prv_isExecutionInRevertiblePhase(sideEffectCounter), // Signature changes: old 4-param oracles → new 1-param validatePublicCalldata privateNotifyEnqueuedPublicFunctionCall: ( _contractAddress: ACVMField[], calldataHash: ACVMField[], _sideEffectCounter: ACVMField[], _isStaticCall: ACVMField[], - ): Promise => oracle.aztec_prv_validatePublicCalldata(calldataHash), + ): Promise => oracle.aztec_prv_assertValidPublicCalldata(calldataHash), privateNotifySetPublicTeardownFunctionCall: ( _contractAddress: ACVMField[], calldataHash: ACVMField[], _sideEffectCounter: ACVMField[], _isStaticCall: ACVMField[], - ): Promise => oracle.aztec_prv_validatePublicCalldata(calldataHash), + ): Promise => oracle.aztec_prv_assertValidPublicCalldata(calldataHash), }; } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 8acde41f2747..bda48431fd03 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -108,13 +108,13 @@ export class Oracle { } // eslint-disable-next-line camelcase - aztec_prv_storeInExecutionCache(values: ACVMField[], [hash]: ACVMField[]): Promise { + aztec_prv_setHashPreimage(values: ACVMField[], [hash]: ACVMField[]): Promise { this.handlerAsPrivate().storeInExecutionCache(values.map(Fr.fromString), Fr.fromString(hash)); return Promise.resolve([]); } // eslint-disable-next-line camelcase - async aztec_prv_loadFromExecutionCache([returnsHash]: ACVMField[]): Promise { + async aztec_prv_getHashPreimage([returnsHash]: ACVMField[]): Promise { const values = await this.handlerAsPrivate().loadFromExecutionCache(Fr.fromString(returnsHash)); return [values.map(toACVMField)]; } @@ -252,7 +252,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_tryGetPublicKeysAndPartialAddress([address]: ACVMField[]): Promise<(ACVMField | ACVMField[])[]> { + async aztec_utl_getPublicKeysAndPartialAddress([address]: ACVMField[]): Promise<(ACVMField | ACVMField[])[]> { const parsedAddress = AztecAddress.fromField(Fr.fromString(address)); const result = await this.handlerAsUtility().tryGetPublicKeysAndPartialAddress(parsedAddress); @@ -380,7 +380,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_checkNullifierExists([innerNullifier]: ACVMField[]): Promise { + async aztec_utl_doesNullifierExist([innerNullifier]: ACVMField[]): Promise { const exists = await this.handlerAsUtility().checkNullifierExists(Fr.fromString(innerNullifier)); return [toACVMField(exists)]; } @@ -400,7 +400,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_storageRead( + async aztec_utl_getFromPublicStorage( [blockHash]: ACVMField[], [contractAddress]: ACVMField[], [startStorageSlot]: ACVMField[], @@ -464,7 +464,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_prv_validatePublicCalldata([calldataHash]: ACVMField[]): Promise { + async aztec_prv_assertValidPublicCalldata([calldataHash]: ACVMField[]): Promise { await this.handlerAsPrivate().validatePublicCalldata(Fr.fromString(calldataHash)); return []; } @@ -476,7 +476,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_prv_inRevertiblePhase([sideEffectCounter]: ACVMField[]): Promise { + async aztec_prv_isExecutionInRevertiblePhase([sideEffectCounter]: ACVMField[]): Promise { const isRevertible = await this.handlerAsPrivate().inRevertiblePhase(Fr.fromString(sideEffectCounter).toNumber()); return Promise.resolve([toACVMField(isRevertible)]); } @@ -491,8 +491,14 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_fetchTaggedLogs([pendingTaggedLogArrayBaseSlot]: ACVMField[]): Promise { - await this.handlerAsUtility().fetchTaggedLogs(Fr.fromString(pendingTaggedLogArrayBaseSlot)); + async aztec_utl_getPendingTaggedLogs( + [pendingTaggedLogArrayBaseSlot]: ACVMField[], + [scope]: ACVMField[], + ): Promise { + await this.handlerAsUtility().fetchTaggedLogs( + Fr.fromString(pendingTaggedLogArrayBaseSlot), + AztecAddress.fromString(scope), + ); return []; } @@ -503,6 +509,7 @@ export class Oracle { [eventValidationRequestsArrayBaseSlot]: ACVMField[], [maxNotePackedLen]: ACVMField[], [maxEventSerializedLen]: ACVMField[], + [scope]: ACVMField[], ): Promise { await this.handlerAsUtility().validateAndStoreEnqueuedNotesAndEvents( AztecAddress.fromString(contractAddress), @@ -510,62 +517,71 @@ export class Oracle { Fr.fromString(eventValidationRequestsArrayBaseSlot), Fr.fromString(maxNotePackedLen).toNumber(), Fr.fromString(maxEventSerializedLen).toNumber(), + AztecAddress.fromString(scope), ); return []; } // eslint-disable-next-line camelcase - async aztec_utl_bulkRetrieveLogs( + async aztec_utl_getLogsByTag( [contractAddress]: ACVMField[], [logRetrievalRequestsArrayBaseSlot]: ACVMField[], [logRetrievalResponsesArrayBaseSlot]: ACVMField[], + [scope]: ACVMField[], ): Promise { await this.handlerAsUtility().bulkRetrieveLogs( AztecAddress.fromString(contractAddress), Fr.fromString(logRetrievalRequestsArrayBaseSlot), Fr.fromString(logRetrievalResponsesArrayBaseSlot), + AztecAddress.fromString(scope), ); return []; } // eslint-disable-next-line camelcase - async aztec_utl_utilityResolveMessageContexts( + async aztec_utl_getMessageContextsByTxHash( [contractAddress]: ACVMField[], [messageContextRequestsArrayBaseSlot]: ACVMField[], [messageContextResponsesArrayBaseSlot]: ACVMField[], + [scope]: ACVMField[], ): Promise { await this.handlerAsUtility().utilityResolveMessageContexts( AztecAddress.fromString(contractAddress), Fr.fromString(messageContextRequestsArrayBaseSlot), Fr.fromString(messageContextResponsesArrayBaseSlot), + AztecAddress.fromString(scope), ); return []; } // eslint-disable-next-line camelcase - async aztec_utl_storeCapsule( + aztec_utl_setCapsule( [contractAddress]: ACVMField[], [slot]: ACVMField[], capsule: ACVMField[], + [scope]: ACVMField[], ): Promise { - await this.handlerAsUtility().storeCapsule( + this.handlerAsUtility().storeCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(slot), capsule.map(Fr.fromString), + AztecAddress.fromField(Fr.fromString(scope)), ); - return []; + return Promise.resolve([]); } // eslint-disable-next-line camelcase - async aztec_utl_loadCapsule( + async aztec_utl_getCapsule( [contractAddress]: ACVMField[], [slot]: ACVMField[], [tSize]: ACVMField[], + [scope]: ACVMField[], ): Promise<(ACVMField | ACVMField[])[]> { const values = await this.handlerAsUtility().loadCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(slot), + AztecAddress.fromField(Fr.fromString(scope)), ); // We are going to return a Noir Option struct to represent the possibility of null values. Options are a struct @@ -580,12 +596,17 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_deleteCapsule([contractAddress]: ACVMField[], [slot]: ACVMField[]): Promise { - await this.handlerAsUtility().deleteCapsule( + aztec_utl_deleteCapsule( + [contractAddress]: ACVMField[], + [slot]: ACVMField[], + [scope]: ACVMField[], + ): Promise { + this.handlerAsUtility().deleteCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(slot), + AztecAddress.fromField(Fr.fromString(scope)), ); - return []; + return Promise.resolve([]); } // eslint-disable-next-line camelcase @@ -594,18 +615,20 @@ export class Oracle { [srcSlot]: ACVMField[], [dstSlot]: ACVMField[], [numEntries]: ACVMField[], + [scope]: ACVMField[], ): Promise { await this.handlerAsUtility().copyCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(srcSlot), Fr.fromString(dstSlot), Fr.fromString(numEntries).toNumber(), + AztecAddress.fromField(Fr.fromString(scope)), ); return []; } // eslint-disable-next-line camelcase - async aztec_utl_tryAes128Decrypt( + async aztec_utl_decryptAes128( ciphertextBVecStorage: ACVMField[], [ciphertextLength]: ACVMField[], iv: ACVMField[], @@ -632,16 +655,18 @@ export class Oracle { [ephPKField0]: ACVMField[], [ephPKField1]: ACVMField[], [ephPKField2]: ACVMField[], + [contractAddress]: ACVMField[], ): Promise { const secret = await this.handlerAsUtility().getSharedSecret( AztecAddress.fromField(Fr.fromString(address)), Point.fromFields([ephPKField0, ephPKField1, ephPKField2].map(Fr.fromString)), + AztecAddress.fromField(Fr.fromString(contractAddress)), ); - return secret.toFields().map(toACVMField); + return [toACVMField(secret)]; } // eslint-disable-next-line camelcase - aztec_utl_invalidateContractSyncCache( + aztec_utl_setContractSyncCacheInvalid( [contractAddress]: ACVMField[], scopes: ACVMField[], [scopeCount]: ACVMField[], diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts index 1990079798eb..47bb5757e5b8 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts @@ -14,6 +14,7 @@ import { mock } from 'jest-mock-extended'; import type { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; import type { MessageContextService } from '../../messages/message_context_service.js'; +import { ORACLE_VERSION } from '../../oracle_version.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; import type { CapsuleStore } from '../../storage/capsule_store/capsule_store.js'; import type { ContractStore } from '../../storage/contract_store/contract_store.js'; @@ -182,4 +183,48 @@ describe('Oracle Version Check test suite', () => { expect(assertCompatibleOracleVersionSpy).toHaveBeenCalledTimes(1); }, 30_000); }); + + describe('oracle version mismatch error messages', () => { + let oracle: UtilityExecutionOracle; + + beforeEach(() => { + oracle = new UtilityExecutionOracle({ + contractAddress, + authWitnesses: [], + capsules: [], + anchorBlockHeader, + contractStore, + noteStore, + keyStore, + addressStore, + aztecNode, + recipientTaggingStore, + senderAddressBookStore, + capsuleStore, + privateEventStore, + messageContextService, + contractSyncService, + jobId: 'test', + scopes: 'ALL_SCOPES', + }); + }); + + it('suggests upgrading PXE when contract oracle version is newer', () => { + const newerVersion = ORACLE_VERSION + 1; + expect(() => oracle.assertCompatibleOracleVersion(newerVersion)).toThrow( + /Incompatible private environment version:.*Upgrade your private environment to a compatible version.*See https:\/\/docs\.aztec\.network\/errors\/8/, + ); + }); + + it('suggests recompiling the contract when contract oracle version is older', () => { + const olderVersion = ORACLE_VERSION - 1; + expect(() => oracle.assertCompatibleOracleVersion(olderVersion)).toThrow( + /Incompatible private environment version:.*Recompile the contract with a compatible version of Aztec\.nr.*See https:\/\/docs\.aztec\.network\/errors\/8/, + ); + }); + + it('does not throw when oracle version matches', () => { + expect(() => oracle.assertCompatibleOracleVersion(ORACLE_VERSION)).not.toThrow(); + }); + }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index 09b7ee03368b..a1ac38608ddb 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -289,18 +289,21 @@ describe('Private Execution test suite', () => { messageContextService.resolveMessageContexts.mockResolvedValue([]); // Configure mock to actually perform sync_state calls (needed for nested call tests) contractSyncService.ensureContractSynced.mockImplementation( - async (contractAddress, functionToInvokeAfterSync, utilityExecutor, anchorBlockHeader, jobId) => { - await syncState( - contractAddress, - contractStore, - functionToInvokeAfterSync, - utilityExecutor, - noteStore, - aztecNode, - anchorBlockHeader, - jobId, - 'ALL_SCOPES', - ); + async (contractAddress, functionToInvokeAfterSync, utilityExecutor, anchorBlockHeader, jobId, scopes) => { + const scopeAddresses = scopes === 'ALL_SCOPES' ? [owner] : scopes; + for (const scope of scopeAddresses) { + await syncState( + contractAddress, + contractStore, + functionToInvokeAfterSync, + utilityExecutor, + noteStore, + aztecNode, + anchorBlockHeader, + jobId, + scope, + ); + } }, ); contracts = {}; @@ -460,7 +463,7 @@ describe('Private Execution test suite', () => { describe('no constructor', () => { it('emits a field array as an encrypted log', async () => { - const args = [times(5, () => Fr.random()), owner, false]; + const args = [Fr.ZERO, times(5, () => Fr.random()), owner, false]; const result = await runSimulator({ artifact: TestContractArtifact, functionName: 'emit_array_as_encrypted_log', @@ -719,7 +722,7 @@ describe('Private Execution test suite', () => { contractAddress: parentAddress, }); - expect(contractStore.getFunctionCall).toHaveBeenCalledWith('sync_state', [], childAddress); + expect(contractStore.getFunctionCall).toHaveBeenCalledWith('sync_state', [owner], childAddress); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index bbfee4e337d1..104746f118dd 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -1,18 +1,24 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; +import { Grumpkin } from '@aztec/foundation/crypto/grumpkin'; import { Fr } from '@aztec/foundation/curves/bn254'; -import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; +import { GrumpkinScalar, Point } from '@aztec/foundation/curves/grumpkin'; import type { KeyStore } from '@aztec/key-store'; import { StatefulTestContractArtifact } from '@aztec/noir-test-contracts.js/StatefulTest'; import { WASMSimulator } from '@aztec/simulator/client'; import { FunctionCall, FunctionSelector, FunctionType, encodeArguments } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash } from '@aztec/stdlib/block'; -import { CompleteAddress, type ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import { + CompleteAddress, + type ContractInstanceWithAddress, + computeContractAddressFromInstance, +} from '@aztec/stdlib/contract'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; -import { deriveKeys } from '@aztec/stdlib/keys'; +import { PublicKeys, deriveKeys } from '@aztec/stdlib/keys'; +import { MessageContext } from '@aztec/stdlib/logs'; import { Note, NoteDao } from '@aztec/stdlib/note'; import { makeL2Tips } from '@aztec/stdlib/testing'; -import { BlockHeader, GlobalVariables, TxHash } from '@aztec/stdlib/tx'; +import { BlockHeader, Capsule, GlobalVariables, TxHash } from '@aztec/stdlib/tx'; import { mock } from 'jest-mock-extended'; import type { _MockProxy } from 'jest-mock-extended/lib/Mock.js'; @@ -28,7 +34,6 @@ import type { RecipientTaggingStore } from '../../storage/tagging_store/recipien import type { SenderAddressBookStore } from '../../storage/tagging_store/sender_address_book_store.js'; import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js'; import { ContractFunctionSimulator } from '../contract_function_simulator.js'; -import { MessageTxContext } from '../noir-structs/message_tx_context.js'; import { UtilityExecutionOracle } from './utility_execution_oracle.js'; describe('Utility Execution test suite', () => { @@ -138,7 +143,6 @@ describe('Utility Execution test suite', () => { }); it('should run the summed_values function on StatefulTestContractArtifact', async () => { - const contractAddress = await AztecAddress.random(); const artifact = { ...StatefulTestContractArtifact.functions.find(f => f.name === 'summed_values')!, contractName: StatefulTestContractArtifact.name, @@ -146,11 +150,27 @@ describe('Utility Execution test suite', () => { const notes: Note[] = [...Array(5).fill(buildNote(1n)), ...Array(2).fill(buildNote(2n))]; + // The initializer nullifier check requires the instance to be a valid preimage of the contract address, so we + // can't use a random contract address here. + const instanceFields = { + version: 1 as const, + salt: Fr.random(), + deployer: await AztecAddress.random(), + currentContractClassId: new Fr(42), + originalContractClassId: new Fr(42), + initializationHash: Fr.random(), + publicKeys: await PublicKeys.random(), + }; + const contractAddress = await computeContractAddressFromInstance(instanceFields); + aztecNode.getPublicStorageAt.mockResolvedValue(Fr.ZERO); + // The init check calls check_nullifier_exists, which queries findLeavesIndexes. + aztecNode.findLeavesIndexes.mockResolvedValue([ + { data: 1n, l2BlockNumber: BlockNumber(1), l2BlockHash: BlockHash.random() }, + ]); contractStore.getFunctionArtifact.mockResolvedValue(artifact); contractStore.getContractInstance.mockResolvedValue({ - currentContractClassId: new Fr(42), - originalContractClassId: new Fr(42), + ...instanceFields, address: contractAddress, } as ContractInstanceWithAddress); contractStore.getFunctionArtifactWithDebugMetadata.mockImplementation(async (address, selector) => { @@ -246,6 +266,76 @@ describe('Utility Execution test suite', () => { }); }); + describe('capsules', () => { + it('forwards scope to the capsule store', async () => { + const scope = await AztecAddress.random(); + const slot = Fr.random(); + const srcSlot = Fr.random(); + const dstSlot = Fr.random(); + const capsule = [Fr.random()]; + + capsuleStore.loadCapsule.mockResolvedValueOnce(capsule); + + utilityExecutionOracle.storeCapsule(contractAddress, slot, capsule, scope); + await utilityExecutionOracle.loadCapsule(contractAddress, slot, scope); + utilityExecutionOracle.deleteCapsule(contractAddress, slot, scope); + await utilityExecutionOracle.copyCapsule(contractAddress, srcSlot, dstSlot, 1, scope); + + expect(capsuleStore.storeCapsule).toHaveBeenCalledWith(contractAddress, slot, capsule, 'test-job-id', scope); + expect(capsuleStore.loadCapsule).toHaveBeenCalledWith(contractAddress, slot, 'test-job-id', scope); + expect(capsuleStore.deleteCapsule).toHaveBeenCalledWith(contractAddress, slot, 'test-job-id', scope); + expect(capsuleStore.copyCapsule).toHaveBeenCalledWith( + contractAddress, + srcSlot, + dstSlot, + 1, + 'test-job-id', + scope, + ); + }); + + it('loads transient capsules by scope', async () => { + const scope = await AztecAddress.random(); + const slot = Fr.random(); + const transientGlobal = [Fr.random()]; + const transientScoped = [Fr.random()]; + const persisted = [Fr.random()]; + + utilityExecutionOracle = new UtilityExecutionOracle({ + contractAddress, + authWitnesses: [], + capsules: [ + new Capsule(contractAddress, slot, transientGlobal), + new Capsule(contractAddress, slot, transientScoped, scope), + ], + anchorBlockHeader, + contractStore, + noteStore, + keyStore, + addressStore, + aztecNode, + recipientTaggingStore, + senderAddressBookStore, + capsuleStore, + privateEventStore, + messageContextService, + contractSyncService, + jobId: 'test-job-id', + scopes: 'ALL_SCOPES', + }); + + capsuleStore.loadCapsule.mockResolvedValueOnce(persisted); + + expect(await utilityExecutionOracle.loadCapsule(contractAddress, slot, AztecAddress.ZERO)).toEqual( + transientGlobal, + ); + expect(await utilityExecutionOracle.loadCapsule(contractAddress, slot, AztecAddress.ZERO)).toEqual( + transientGlobal, + ); + expect(await utilityExecutionOracle.loadCapsule(contractAddress, slot, scope)).toEqual(transientScoped); + }); + }); + describe('invalidateContractSyncCache', () => { it('throws when contract address does not match', async () => { const otherAddress = await AztecAddress.random(); @@ -267,29 +357,30 @@ describe('Utility Execution test suite', () => { describe('utilityResolveMessageContexts', () => { const requestSlot = Fr.random(); const responseSlot = Fr.random(); + const scope = AztecAddress.fromBigInt(42n); it('throws when contractAddress does not match', async () => { const wrongAddress = await AztecAddress.random(); await expect( - utilityExecutionOracle.utilityResolveMessageContexts(wrongAddress, requestSlot, responseSlot), + utilityExecutionOracle.utilityResolveMessageContexts(wrongAddress, requestSlot, responseSlot, scope), ).rejects.toThrow(`Got a message context request from ${wrongAddress}, expected ${contractAddress}`); }); it('sets null in response capsule for zero tx hashes', async () => { capsuleStore.readCapsuleArray.mockResolvedValueOnce([[Fr.ZERO]]); - await utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot); + await utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot, scope); const response = capsuleStore.setCapsuleArray.mock.calls.find( call => call[0].equals(contractAddress) && call[1].equals(responseSlot), ); expect(response).toBeDefined(); const responseFields = response![2][0]; - expect(responseFields).toEqual(MessageTxContext.toSerializedOption(null)); + expect(responseFields).toEqual(MessageContext.toSerializedOption(null)); expect(aztecNode.getTxEffect).not.toHaveBeenCalled(); }); - it('resolves a valid tx hash into a MessageTxContext', async () => { + it('resolves a valid tx hash into a MessageContext', async () => { const txHash = TxHash.random(); const noteHash = Fr.random(); const firstNullifier = Fr.random(); @@ -302,14 +393,14 @@ describe('Utility Execution test suite', () => { data: { txHash, noteHashes: [noteHash], nullifiers: [firstNullifier] }, } as any); - await utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot); + await utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot, scope); const response = capsuleStore.setCapsuleArray.mock.calls.find( call => call[0].equals(contractAddress) && call[1].equals(responseSlot), ); expect(response).toBeDefined(); const responseFields = response![2][0]; - const expected = MessageTxContext.toSerializedOption(new MessageTxContext(txHash, [noteHash], firstNullifier)); + const expected = MessageContext.toSerializedOption(new MessageContext(txHash, [noteHash], firstNullifier)); expect(responseFields).toEqual(expected); }); @@ -324,42 +415,116 @@ describe('Utility Execution test suite', () => { data: { txHash, noteHashes: [], nullifiers: [Fr.random()] }, } as any); - await utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot); + await utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot, scope); const response = capsuleStore.setCapsuleArray.mock.calls.find( call => call[0].equals(contractAddress) && call[1].equals(responseSlot), ); expect(response).toBeDefined(); const responseFields = response![2][0]; - expect(responseFields).toEqual(MessageTxContext.toSerializedOption(null)); + expect(responseFields).toEqual(MessageContext.toSerializedOption(null)); }); it('throws on empty capsule entry', async () => { capsuleStore.readCapsuleArray.mockResolvedValueOnce([[]]); await expect( - utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot), + utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot, scope), ).rejects.toThrow('Malformed message context request at index 0: expected 1 field (tx hash), got 0'); }); it('throws on capsule entry with extra fields', async () => { capsuleStore.readCapsuleArray.mockResolvedValueOnce([[Fr.random(), Fr.random()]]); await expect( - utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot), + utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot, scope), ).rejects.toThrow('Malformed message context request at index 0: expected 1 field (tx hash), got 2'); }); it('clears the request capsule after processing', async () => { capsuleStore.readCapsuleArray.mockResolvedValueOnce([]); - await utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot); - expect(capsuleStore.setCapsuleArray).toHaveBeenCalledWith(contractAddress, requestSlot, [], 'test-job-id'); + await utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot, scope); + expect(capsuleStore.setCapsuleArray).toHaveBeenCalledWith( + contractAddress, + requestSlot, + [], + 'test-job-id', + scope, + ); }); it('clears the request capsule even on error', async () => { capsuleStore.readCapsuleArray.mockResolvedValueOnce([[]]); await expect( - utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot), + utilityExecutionOracle.utilityResolveMessageContexts(contractAddress, requestSlot, responseSlot, scope), ).rejects.toThrow(); - expect(capsuleStore.setCapsuleArray).toHaveBeenCalledWith(contractAddress, requestSlot, [], 'test-job-id'); + expect(capsuleStore.setCapsuleArray).toHaveBeenCalledWith( + contractAddress, + requestSlot, + [], + 'test-job-id', + scope, + ); + }); + }); + + describe('getSharedSecret', () => { + it('returns different shared secrets for different contract addresses', async () => { + // Generate a deterministic ephemeral public key + const ephSk = GrumpkinScalar.random(); + const ephPk = await Grumpkin.mul(Grumpkin.generator, ephSk); + + // Derive keys so we can mock getMasterSecretKey (used by getSharedSecret) + const { masterIncomingViewingSecretKey: ownerIvskM } = await deriveKeys(ownerSecretKey); + keyStore.getMasterSecretKey.mockImplementation((publicKey: Point) => { + if (publicKey.equals(ownerCompleteAddress.publicKeys.masterIncomingViewingPublicKey)) { + return Promise.resolve(ownerIvskM); + } + throw new Error(`Unknown public key ${publicKey}`); + }); + + const contractAddressA = await AztecAddress.random(); + const contractAddressB = await AztecAddress.random(); + + const makeOracle = (addr: AztecAddress) => + new UtilityExecutionOracle({ + contractAddress: addr, + authWitnesses: [], + capsules: [], + anchorBlockHeader, + contractStore, + noteStore, + keyStore, + addressStore, + aztecNode, + recipientTaggingStore, + senderAddressBookStore, + capsuleStore, + privateEventStore, + messageContextService, + contractSyncService, + jobId: 'test-job-id', + scopes: 'ALL_SCOPES', + }); + + const oracleA = makeOracle(contractAddressA); + const oracleB = makeOracle(contractAddressB); + + const secretA = await oracleA.getSharedSecret(owner, ephPk, contractAddressA); + const secretB = await oracleB.getSharedSecret(owner, ephPk, contractAddressB); + + // After app-siloing, different contracts must get different shared secrets for the same + // (address, ephPk) pair. This prevents cross-contract decryption attacks. + expect(secretA).not.toEqual(secretB); + }); + + it('rejects when contract address does not match execution context', async () => { + const ephSk = GrumpkinScalar.random(); + const ephPk = await Grumpkin.mul(Grumpkin.generator, ephSk); + + const { masterIncomingViewingSecretKey: ownerIvskM } = await deriveKeys(ownerSecretKey); + keyStore.getMasterSecretKey.mockResolvedValue(ownerIvskM); + + const wrongAddress = await AztecAddress.random(); + await expect(utilityExecutionOracle.getSharedSecret(owner, ephPk, wrongAddress)).rejects.toThrow(/expected/); }); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index 12422b44540e..d526d87ba49f 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -15,14 +15,14 @@ import { siloNullifier } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; import { type PublicKeys, computeAddressSecret } from '@aztec/stdlib/keys'; -import { deriveEcdhSharedSecret } from '@aztec/stdlib/logs'; +import { MessageContext, deriveAppSiloedSharedSecret } from '@aztec/stdlib/logs'; import { getNonNullifiedL1ToL2MessageWitness } from '@aztec/stdlib/messaging'; import type { NoteStatus } from '@aztec/stdlib/note'; import { MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; import type { BlockHeader, Capsule, OffchainEffect } from '@aztec/stdlib/tx'; import type { AccessScopes } from '../../access_scopes.js'; -import { createContractLogger, logContractMessage } from '../../contract_logging.js'; +import { createContractLogger, logContractMessage, stripAztecnrLogPrefix } from '../../contract_logging.js'; import type { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; import { EventService } from '../../events/event_service.js'; import { LogService } from '../../logs/log_service.js'; @@ -39,7 +39,6 @@ import type { SenderAddressBookStore } from '../../storage/tagging_store/sender_ import { EventValidationRequest } from '../noir-structs/event_validation_request.js'; import { LogRetrievalRequest } from '../noir-structs/log_retrieval_request.js'; import { LogRetrievalResponse } from '../noir-structs/log_retrieval_response.js'; -import { MessageTxContext } from '../noir-structs/message_tx_context.js'; import { NoteValidationRequest } from '../noir-structs/note_validation_request.js'; import { UtilityContext } from '../noir-structs/utility_context.js'; import { pickNotes } from '../pick_notes.js'; @@ -77,6 +76,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra isUtility = true as const; private contractLogger: Logger | undefined; + private aztecnrLogger: Logger | undefined; private offchainEffects: OffchainEffect[] = []; protected readonly contractAddress: AztecAddress; @@ -128,15 +128,25 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra const LEGACY_ORACLE_VERSION = 12; if (isProtocolContract(this.contractAddress)) { if (version !== LEGACY_ORACLE_VERSION && version !== ORACLE_VERSION) { + const hint = + version > ORACLE_VERSION + ? 'The contract was compiled with a newer version of Aztec.nr than your private environment supports. Upgrade your private environment to a compatible version.' + : 'The contract was compiled with an older version of Aztec.nr than your private environment supports. Recompile the contract with a compatible version of Aztec.nr.'; throw new Error( - `Expected legacy oracle version ${LEGACY_ORACLE_VERSION} or current oracle version ${ORACLE_VERSION} for alpha payload contract at ${this.contractAddress}, got ${version}.`, + `Incompatible private environment version: ${hint} See https://docs.aztec.network/errors/8 (expected oracle version ${LEGACY_ORACLE_VERSION} or ${ORACLE_VERSION}, got ${version})`, ); } return; } if (version !== ORACLE_VERSION) { - throw new Error(`Incompatible oracle version. Expected version ${ORACLE_VERSION}, got ${version}.`); + const hint = + version > ORACLE_VERSION + ? 'The contract was compiled with a newer version of Aztec.nr than your private environment supports. Upgrade your private environment to a compatible version.' + : 'The contract was compiled with an older version of Aztec.nr than your private environment supports. Recompile the contract with a compatible version of Aztec.nr.'; + throw new Error( + `Incompatible private environment version: ${hint} See https://docs.aztec.network/errors/8 (expected oracle version ${ORACLE_VERSION}, got ${version})`, + ); } } @@ -173,16 +183,18 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra /** * Fetches the index and sibling path of a leaf at a given block from the note hash tree. - * @param anchorBlockHash - The hash of a block that contains the note hash tree root in which to find the membership - * witness. + * @param blockHash - The hash of a block that contains the note hash tree root in which to find the + * membership witness. * @param noteHash - The note hash to find in the note hash tree. * @returns The membership witness containing the leaf index and sibling path */ public getNoteHashMembershipWitness( - anchorBlockHash: BlockHash, + blockHash: BlockHash, noteHash: Fr, ): Promise | undefined> { - return this.aztecNode.getNoteHashMembershipWitness(anchorBlockHash, noteHash); + return this.#queryWithBlockHashNotAfterAnchor(blockHash, () => + this.aztecNode.getNoteHashMembershipWitness(blockHash, noteHash), + ); } /** @@ -191,16 +203,21 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra * 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. * - * @param anchorBlockHash - The hash of a block that contains the archive tree root in which to find the membership + * @param referenceBlockHash - The hash of a block that contains the archive tree root in which to find the membership * witness. * @param blockHash - The block hash to find in the archive tree. * @returns The membership witness containing the leaf index and sibling path */ public getBlockHashMembershipWitness( - anchorBlockHash: BlockHash, + referenceBlockHash: BlockHash, blockHash: BlockHash, ): Promise | undefined> { - return this.aztecNode.getBlockHashMembershipWitness(anchorBlockHash, blockHash); + // Note that we validate that the reference block hash is at or before the anchor block - we don't test the block + // hash at all. If the block hash did not exist by the reference block hash, then the node will not return the + // membership witness as there is none. + return this.#queryWithBlockHashNotAfterAnchor(referenceBlockHash, () => + this.aztecNode.getBlockHashMembershipWitness(referenceBlockHash, blockHash), + ); } /** @@ -213,7 +230,9 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra blockHash: BlockHash, nullifier: Fr, ): Promise { - return this.aztecNode.getNullifierMembershipWitness(blockHash, nullifier); + return this.#queryWithBlockHashNotAfterAnchor(blockHash, () => + this.aztecNode.getNullifierMembershipWitness(blockHash, nullifier), + ); } /** @@ -229,7 +248,9 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra blockHash: BlockHash, nullifier: Fr, ): Promise { - return this.aztecNode.getLowNullifierMembershipWitness(blockHash, nullifier); + return this.#queryWithBlockHashNotAfterAnchor(blockHash, () => + this.aztecNode.getLowNullifierMembershipWitness(blockHash, nullifier), + ); } /** @@ -239,7 +260,9 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra * @returns - The witness */ public getPublicDataWitness(blockHash: BlockHash, leafSlot: Fr): Promise { - return this.aztecNode.getPublicDataWitness(blockHash, leafSlot); + return this.#queryWithBlockHashNotAfterAnchor(blockHash, () => + this.aztecNode.getPublicDataWitness(blockHash, leafSlot), + ); } /** @@ -380,7 +403,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra } /** - * Fetches a message from the executionStore, given its key. + * Returns the membership witness of an un-nullified L1 to L2 message. * @param contractAddress - Address of a contract by which the message was emitted. * @param messageHash - Hash of the message. * @param secret - Secret used to compute a nullifier. @@ -393,6 +416,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra contractAddress, messageHash, secret, + await this.anchorBlockHeader.hash(), ); return new MessageLoadOracleInputs(messageIndex, siblingPath); @@ -405,29 +429,31 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra * @param startStorageSlot - The starting storage slot. * @param numberOfElements - Number of elements to read from the starting storage slot. */ - public async storageRead( + public storageRead( blockHash: BlockHash, contractAddress: AztecAddress, startStorageSlot: Fr, numberOfElements: number, ) { - const slots = Array(numberOfElements) - .fill(0) - .map((_, i) => new Fr(startStorageSlot.value + BigInt(i))); + return this.#queryWithBlockHashNotAfterAnchor(blockHash, async () => { + const slots = Array(numberOfElements) + .fill(0) + .map((_, i) => new Fr(startStorageSlot.value + BigInt(i))); - const values = await Promise.all( - slots.map(storageSlot => this.aztecNode.getPublicStorageAt(blockHash, contractAddress, storageSlot)), - ); + const values = await Promise.all( + slots.map(storageSlot => this.aztecNode.getPublicStorageAt(blockHash, contractAddress, storageSlot)), + ); - this.logger.debug( - `Oracle storage read: slots=[${slots.map(slot => slot.toString()).join(', ')}] address=${contractAddress.toString()} values=[${values.join(', ')}]`, - ); + this.logger.debug( + `Oracle storage read: slots=[${slots.map(slot => slot.toString()).join(', ')}] address=${contractAddress.toString()} values=[${values.join(', ')}]`, + ); - return values; + return values; + }); } /** - * Returns a per-contract logger whose output is prefixed with `contract_log::()`. + * Returns a per-contract logger whose output is prefixed with `contract:()`. */ async #getContractLogger(): Promise { if (!this.contractLogger) { @@ -436,21 +462,42 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra this.contractLogger = await createContractLogger( this.contractAddress, addr => this.contractStore.getDebugContractName(addr), + 'user', { instanceId: this.jobId }, ); } return this.contractLogger; } + /** + * Returns a per-contract logger whose output is prefixed with `aztecnr:()`. + */ + async #getAztecnrLogger(): Promise { + if (!this.aztecnrLogger) { + // Purpose of instanceId is to distinguish logs from different instances of the same component. It makes sense + // to re-use jobId as instanceId here as executions of different PXE jobs are isolated. + this.aztecnrLogger = await createContractLogger( + this.contractAddress, + addr => this.contractStore.getDebugContractName(addr), + 'aztecnr', + { instanceId: this.jobId }, + ); + } + return this.aztecnrLogger; + } + public async log(level: number, message: string, fields: Fr[]): Promise { if (!LogLevels[level]) { throw new Error(`Invalid log level: ${level}`); } - const logger = await this.#getContractLogger(); - logContractMessage(logger, LogLevels[level], message, fields); + + const { kind, message: strippedMessage } = stripAztecnrLogPrefix(message); + + const logger = kind == 'aztecnr' ? await this.#getAztecnrLogger() : await this.#getContractLogger(); + logContractMessage(logger, LogLevels[level], strippedMessage, fields); } - public async fetchTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr) { + public async fetchTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr, scope: AztecAddress) { const logService = new LogService( this.aztecNode, this.anchorBlockHeader, @@ -463,7 +510,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra this.logger.getBindings(), ); - await logService.fetchTaggedLogs(this.contractAddress, pendingTaggedLogArrayBaseSlot, this.scopes); + await logService.fetchTaggedLogs(this.contractAddress, pendingTaggedLogArrayBaseSlot, scope); } /** @@ -482,6 +529,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra eventValidationRequestsArrayBaseSlot: Fr, maxNotePackedLen: number, maxEventSerializedLen: number, + scope: AztecAddress, ) { // TODO(#10727): allow other contracts to store notes if (!this.contractAddress.equals(contractAddress)) { @@ -491,11 +539,11 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // We read all note and event validation requests and process them all concurrently. This makes the process much // faster as we don't need to wait for the network round-trip. const noteValidationRequests = ( - await this.capsuleStore.readCapsuleArray(contractAddress, noteValidationRequestsArrayBaseSlot, this.jobId) + await this.capsuleStore.readCapsuleArray(contractAddress, noteValidationRequestsArrayBaseSlot, this.jobId, scope) ).map(fields => NoteValidationRequest.fromFields(fields, maxNotePackedLen)); const eventValidationRequests = ( - await this.capsuleStore.readCapsuleArray(contractAddress, eventValidationRequestsArrayBaseSlot, this.jobId) + await this.capsuleStore.readCapsuleArray(contractAddress, eventValidationRequestsArrayBaseSlot, this.jobId, scope) ).map(fields => EventValidationRequest.fromFields(fields, maxEventSerializedLen)); const noteService = new NoteService(this.noteStore, this.aztecNode, this.anchorBlockHeader, this.jobId); @@ -510,7 +558,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra request.noteHash, request.nullifier, request.txHash, - request.recipient, + scope, ), ); @@ -523,21 +571,34 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra request.serializedEvent, request.eventCommitment, request.txHash, - request.recipient, + scope, ), ); await Promise.all([...noteStorePromises, ...eventStorePromises]); // Requests are cleared once we're done. - await this.capsuleStore.setCapsuleArray(contractAddress, noteValidationRequestsArrayBaseSlot, [], this.jobId); - await this.capsuleStore.setCapsuleArray(contractAddress, eventValidationRequestsArrayBaseSlot, [], this.jobId); + await this.capsuleStore.setCapsuleArray( + contractAddress, + noteValidationRequestsArrayBaseSlot, + [], + this.jobId, + scope, + ); + await this.capsuleStore.setCapsuleArray( + contractAddress, + eventValidationRequestsArrayBaseSlot, + [], + this.jobId, + scope, + ); } public async bulkRetrieveLogs( contractAddress: AztecAddress, logRetrievalRequestsArrayBaseSlot: Fr, logRetrievalResponsesArrayBaseSlot: Fr, + scope: AztecAddress, ) { // TODO(#10727): allow other contracts to process partial notes if (!this.contractAddress.equals(contractAddress)) { @@ -547,7 +608,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // We read all log retrieval requests and process them all concurrently. This makes the process much faster as we // don't need to wait for the network round-trip. const logRetrievalRequests = ( - await this.capsuleStore.readCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, this.jobId) + await this.capsuleStore.readCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, this.jobId, scope) ).map(LogRetrievalRequest.fromFields); const logService = new LogService( @@ -565,7 +626,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra const maybeLogRetrievalResponses = await logService.bulkRetrieveLogs(logRetrievalRequests); // Requests are cleared once we're done. - await this.capsuleStore.setCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, [], this.jobId); + await this.capsuleStore.setCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, [], this.jobId, scope); // The responses are stored as Option in a second CapsuleArray. await this.capsuleStore.setCapsuleArray( @@ -573,6 +634,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra logRetrievalResponsesArrayBaseSlot, maybeLogRetrievalResponses.map(LogRetrievalResponse.toSerializedOption), this.jobId, + scope, ); } @@ -580,15 +642,22 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra contractAddress: AztecAddress, messageContextRequestsArrayBaseSlot: Fr, messageContextResponsesArrayBaseSlot: Fr, + scope: AztecAddress, ) { try { if (!this.contractAddress.equals(contractAddress)) { throw new Error(`Got a message context request from ${contractAddress}, expected ${this.contractAddress}`); } + + // TODO(@mverzilli): this is a prime example of where using a volatile array would make much more sense, we don't + // need scopes here, we just need a bit of shared memory to cross boundaries between Noir and TS. + // At the same time, we don't want to allow any global scope access other than where backwards compatibility + // forces us to. Hence we need the scope here to be artificial. const requestCapsules = await this.capsuleStore.readCapsuleArray( contractAddress, messageContextRequestsArrayBaseSlot, this.jobId, + scope, ); const txHashes = requestCapsules.map((fields, i) => { @@ -609,50 +678,65 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra await this.capsuleStore.setCapsuleArray( contractAddress, messageContextResponsesArrayBaseSlot, - maybeMessageContexts.map(MessageTxContext.toSerializedOption), + maybeMessageContexts.map(MessageContext.toSerializedOption), this.jobId, + scope, ); } finally { - await this.capsuleStore.setCapsuleArray(contractAddress, messageContextRequestsArrayBaseSlot, [], this.jobId); + await this.capsuleStore.setCapsuleArray( + contractAddress, + messageContextRequestsArrayBaseSlot, + [], + this.jobId, + scope, + ); } } - public storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[]): Promise { + public storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], scope: AztecAddress): void { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - this.capsuleStore.storeCapsule(this.contractAddress, slot, capsule, this.jobId); - return Promise.resolve(); + this.capsuleStore.storeCapsule(contractAddress, slot, capsule, this.jobId, scope); } - public async loadCapsule(contractAddress: AztecAddress, slot: Fr): Promise { + public async loadCapsule(contractAddress: AztecAddress, slot: Fr, scope: AztecAddress): Promise { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return ( - // TODO(#12425): On the following line, the pertinent capsule gets overshadowed by the transient one. Tackle this. - this.capsules.find(c => c.contractAddress.equals(contractAddress) && c.storageSlot.equals(slot))?.data ?? - (await this.capsuleStore.loadCapsule(this.contractAddress, slot, this.jobId)) - ); + const maybeTransientCapsule = this.capsules.find( + c => + c.contractAddress.equals(contractAddress) && + c.storageSlot.equals(slot) && + (c.scope ?? AztecAddress.ZERO).equals(scope), + )?.data; + + // TODO(#12425): On the following line, the pertinent capsule gets overshadowed by the transient one. Tackle this. + return maybeTransientCapsule ?? (await this.capsuleStore.loadCapsule(contractAddress, slot, this.jobId, scope)); } - public deleteCapsule(contractAddress: AztecAddress, slot: Fr): Promise { + public deleteCapsule(contractAddress: AztecAddress, slot: Fr, scope: AztecAddress): void { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - this.capsuleStore.deleteCapsule(this.contractAddress, slot, this.jobId); - return Promise.resolve(); + this.capsuleStore.deleteCapsule(contractAddress, slot, this.jobId, scope); } - public copyCapsule(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { + public copyCapsule( + contractAddress: AztecAddress, + srcSlot: Fr, + dstSlot: Fr, + numEntries: number, + scope: AztecAddress, + ): Promise { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.capsuleStore.copyCapsule(this.contractAddress, srcSlot, dstSlot, numEntries, this.jobId); + return this.capsuleStore.copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, this.jobId, scope); } /** @@ -673,19 +757,24 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra } /** - * Retrieves the shared secret for a given address and ephemeral public key. + * Retrieves the app-siloed shared secret for a given address and ephemeral public key. * @param address - The address to get the secret for. * @param ephPk - The ephemeral public key to get the secret for. - * @returns The secret for the given address. + * @param contractAddress - The contract address for app-siloing (validated against execution context). + * @returns The app-siloed shared secret as a Field. */ - public async getSharedSecret(address: AztecAddress, ephPk: Point): Promise { - // TODO(#12656): return an app-siloed secret + public async getSharedSecret(address: AztecAddress, ephPk: Point, contractAddress: AztecAddress): Promise { + if (!contractAddress.equals(this.contractAddress)) { + throw new Error( + `getSharedSecret called with contract address ${contractAddress}, expected ${this.contractAddress}`, + ); + } const recipientCompleteAddress = await this.getCompleteAddressOrFail(address); const ivskM = await this.keyStore.getMasterSecretKey( recipientCompleteAddress.publicKeys.masterIncomingViewingPublicKey, ); const addressSecret = await computeAddressSecret(await recipientCompleteAddress.getPreaddress(), ivskM); - return deriveEcdhSharedSecret(addressSecret, ephPk); + return deriveAppSiloedSharedSecret(addressSecret, ephPk, this.contractAddress); } public emitOffchainEffect(data: Fr[]): Promise { @@ -697,4 +786,24 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra public getOffchainEffects(): OffchainEffect[] { return this.offchainEffects; } + + /** Runs a query concurrently with a validation that the block hash is not ahead of the anchor block. */ + async #queryWithBlockHashNotAfterAnchor(blockHash: BlockHash, query: () => Promise): Promise { + const [response] = await Promise.all([ + query(), + (async () => { + const header = await this.aztecNode.getBlockHeader(blockHash); + if (!header) { + throw new Error(`Could not find block header for block hash ${blockHash}`); + } + + if (header.getBlockNumber() > this.anchorBlockHeader.getBlockNumber()) { + throw new Error( + `Made a node query with a reference block hash ${blockHash} with block number ${header.getBlockNumber()}, which is ahead of the anchor block number ${this.anchorBlockHeader.getBlockNumber()} (from anchor block hash ${await this.anchorBlockHeader.hash()}).`, + ); + } + })(), + ]); + return response; + } } diff --git a/yarn-project/pxe/src/contract_logging.ts b/yarn-project/pxe/src/contract_logging.ts index cb32e2026fa1..e6030d469400 100644 --- a/yarn-project/pxe/src/contract_logging.ts +++ b/yarn-project/pxe/src/contract_logging.ts @@ -5,18 +5,22 @@ import type { DebugLog } from '@aztec/stdlib/logs'; /** Resolves a contract address to a human-readable name, if available. */ export type ContractNameResolver = (address: AztecAddress) => Promise; +export type CONTRACT_LOG_KIND = 'aztecnr' | 'user'; /** - * Creates a logger whose output is prefixed with `contract_log::()`. + * Creates a logger whose output is prefixed with `contract:()`. */ export async function createContractLogger( contractAddress: AztecAddress, getContractName: ContractNameResolver, + kind: CONTRACT_LOG_KIND, options?: { instanceId?: string }, ): Promise { const addrAbbrev = contractAddress.toString().slice(0, 10); const name = await getContractName(contractAddress); - const module = name ? `contract_log::${name}(${addrAbbrev})` : `contract_log::Unknown(${addrAbbrev})`; + + const prefix = kind == 'aztecnr' ? 'aztecnr' : 'contract'; + const module = name ? `${prefix}:${name}(${addrAbbrev})` : `${prefix}:Unknown(${addrAbbrev})`; return createLogger(module, options); } @@ -29,11 +33,20 @@ export function logContractMessage(logger: Logger, level: LogLevel, message: str /** * Displays debug logs collected during public function simulation, - * using the `contract_log::` prefixed logger format. + * using the `contract:` prefixed logger format. */ export async function displayDebugLogs(debugLogs: DebugLog[], getContractName: ContractNameResolver): Promise { for (const log of debugLogs) { - const logger = await createContractLogger(log.contractAddress, getContractName); - logContractMessage(logger, log.level, log.message, log.fields); + const { kind, message } = stripAztecnrLogPrefix(log.message); + const logger = await createContractLogger(log.contractAddress, getContractName, kind); + logContractMessage(logger, log.level, message, log.fields); + } +} + +export function stripAztecnrLogPrefix(message: string): { kind: CONTRACT_LOG_KIND; message: string } { + if (message.startsWith('[aztec-nr] ')) { + return { kind: 'aztecnr', message: message.slice('[aztec-nr] '.length) }; + } else { + return { kind: 'user', message }; } } diff --git a/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts b/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts index 9e2d6b5f0eba..0ed395978b75 100644 --- a/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts +++ b/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts @@ -28,9 +28,6 @@ describe('ContractSyncService', () => { const anchorBlockHeader = makeBlockHeader(0); const classId = Fr.fromHexString('0xdeadbeef'); - /** Sentinel for undefined scopes (sync all accounts). */ - const ALL_SCOPES = 'ALL_SCOPES' as const; - beforeEach(() => { utilityExecutor = jest .fn<(call: FunctionCall, scopes: AccessScopes) => Promise>() @@ -65,7 +62,13 @@ describe('ContractSyncService', () => { // syncNoteNullifiers returns early when no notes noteStore.getNotes.mockResolvedValue([]); - service = new ContractSyncService(aztecNode, contractStore, noteStore, createLogger('test:contract-sync')); + service = new ContractSyncService( + aztecNode, + contractStore, + noteStore, + () => Promise.resolve([scopeA, scopeB]), + createLogger('test:contract-sync'), + ); }); describe('ensureContractSynced', () => { @@ -90,12 +93,13 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes(ALL_SCOPES); + // ALL_SCOPES resolves to [scopeA, scopeB] via getRegisteredAccounts, so syncState is called once per account + expectSyncedScopes([scopeA], [scopeB]); // After syncing all scopes, scope-specific calls should be skipped await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeB]); - expectSyncedScopes(ALL_SCOPES); + expectSyncedScopes([scopeA], [scopeB]); }); it('still syncs all scopes even after scope-specific sync', async () => { @@ -108,7 +112,8 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes([scopeA], ALL_SCOPES); + // ALL_SCOPES resolves to [scopeA, scopeB]; both are re-synced since ALL_SCOPES bypasses per-scope cache + expectSyncedScopes([scopeA], [scopeA], [scopeB]); }); it('empty scopes array skips sync entirely', async () => { @@ -243,7 +248,7 @@ describe('ContractSyncService', () => { scopeA, scopeB, ]); - expectSyncedScopes([scopeA, scopeB]); + expectSyncedScopes([scopeA], [scopeB]); service.invalidateContractForScopes(contractAddress, [scopeA]); @@ -252,7 +257,7 @@ describe('ContractSyncService', () => { scopeB, ]); // Only scopeA should be re-synced, scopeB is still cached. - expectSyncedScopes([scopeA, scopeB], [scopeA]); + expectSyncedScopes([scopeA], [scopeB], [scopeA]); }); it('invalidates multiple scopes at once', async () => { @@ -260,7 +265,7 @@ describe('ContractSyncService', () => { scopeA, scopeB, ]); - expectSyncedScopes([scopeA, scopeB]); + expectSyncedScopes([scopeA], [scopeB]); service.invalidateContractForScopes(contractAddress, [scopeA, scopeB]); @@ -269,11 +274,11 @@ describe('ContractSyncService', () => { scopeB, ]); // Both scopes should be re-synced. - expectSyncedScopes([scopeA, scopeB], [scopeA, scopeB]); + expectSyncedScopes([scopeA], [scopeB], [scopeA], [scopeB]); }); it('also invalidates the ALL_SCOPES entry', async () => { - // Sync ALL_SCOPES -- covers every account. + // Sync ALL_SCOPES -- covers every account. Resolves to [scopeA, scopeB] via getRegisteredAccounts. await service.ensureContractSynced( contractAddress, null, @@ -282,18 +287,18 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes('ALL_SCOPES'); + expectSyncedScopes([scopeA], [scopeB]); // Syncing scopeA is a no-op because ALL_SCOPES already covers it. await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectSyncedScopes('ALL_SCOPES'); + expectSyncedScopes([scopeA], [scopeB]); // Invalidate scopeA -- this should also clear the ALL_SCOPES entry. service.invalidateContractForScopes(contractAddress, [scopeA]); // Now syncing scopeA triggers a re-sync. await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectSyncedScopes('ALL_SCOPES', [scopeA]); + expectSyncedScopes([scopeA], [scopeB], [scopeA]); // And syncing ALL_SCOPES also triggers a re-sync since it was invalidated too. await service.ensureContractSynced( @@ -304,7 +309,7 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes('ALL_SCOPES', [scopeA], 'ALL_SCOPES'); + expectSyncedScopes([scopeA], [scopeB], [scopeA], [scopeA], [scopeB]); }); it('empty scopes is a no-op', async () => { @@ -316,7 +321,7 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes('ALL_SCOPES'); + expectSyncedScopes([scopeA], [scopeB]); service.invalidateContractForScopes(contractAddress, []); @@ -329,7 +334,7 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes('ALL_SCOPES'); + expectSyncedScopes([scopeA], [scopeB]); }); it('does not affect other contracts', async () => { diff --git a/yarn-project/pxe/src/contract_sync/contract_sync_service.ts b/yarn-project/pxe/src/contract_sync/contract_sync_service.ts index 8726efada832..8708010c62e2 100644 --- a/yarn-project/pxe/src/contract_sync/contract_sync_service.ts +++ b/yarn-project/pxe/src/contract_sync/contract_sync_service.ts @@ -31,6 +31,7 @@ export class ContractSyncService implements StagedStore { private aztecNode: AztecNode, private contractStore: ContractStore, private noteStore: NoteStore, + private getRegisteredAccounts: () => Promise, private log: Logger, ) {} @@ -92,18 +93,28 @@ export class ContractSyncService implements StagedStore { scopes: AccessScopes, ): Promise { this.log.debug(`Syncing contract ${contractAddress}`); + + // Resolve ALL_SCOPES to actual registered accounts, since sync_state must be called once per account. + const scopeAddresses = scopes === 'ALL_SCOPES' ? await this.getRegisteredAccounts() : scopes; + await Promise.all([ - syncState( - contractAddress, - this.contractStore, - functionToInvokeAfterSync, - utilityExecutor, - this.noteStore, - this.aztecNode, - anchorBlockHeader, - jobId, - scopes, - ), + // Call sync_state sequentially for each scope address — each invocation synchronizes one account's private + // state using scoped capsule arrays. + (async () => { + for (const scope of scopeAddresses) { + await syncState( + contractAddress, + this.contractStore, + functionToInvokeAfterSync, + utilityExecutor, + this.noteStore, + this.aztecNode, + anchorBlockHeader, + jobId, + scope, + ); + } + })(), verifyCurrentClassId(contractAddress, this.aztecNode, this.contractStore, anchorBlockHeader), ]); this.log.debug(`Contract ${contractAddress} synced`); diff --git a/yarn-project/pxe/src/contract_sync/helpers.ts b/yarn-project/pxe/src/contract_sync/helpers.ts index b2806d6151ab..8f437d10dd7f 100644 --- a/yarn-project/pxe/src/contract_sync/helpers.ts +++ b/yarn-project/pxe/src/contract_sync/helpers.ts @@ -48,11 +48,11 @@ export async function syncState( aztecNode: AztecNode, anchorBlockHeader: BlockHeader, jobId: string, - scopes: AccessScopes, + scope: AztecAddress, ) { // Protocol contracts don't have private state to sync if (!isProtocolContract(contractAddress)) { - const syncStateFunctionCall = await contractStore.getFunctionCall('sync_state', [], contractAddress); + const syncStateFunctionCall = await contractStore.getFunctionCall('sync_state', [scope], contractAddress); if (functionToInvokeAfterSync && functionToInvokeAfterSync.equals(syncStateFunctionCall.selector)) { throw new Error( 'Forbidden `sync_state` invocation. `sync_state` can only be invoked by PXE, manual execution can lead to inconsistencies.', @@ -60,6 +60,7 @@ export async function syncState( } const noteService = new NoteService(noteStore, aztecNode, anchorBlockHeader, jobId); + const scopes: AccessScopes = [scope]; // Both sync_state and syncNoteNullifiers interact with the note store, but running them in parallel is safe // because note store is designed to handle concurrent operations. diff --git a/yarn-project/pxe/src/events/event_service.test.ts b/yarn-project/pxe/src/events/event_service.test.ts index 0b92c3c4aeed..09567b12bbc9 100644 --- a/yarn-project/pxe/src/events/event_service.test.ts +++ b/yarn-project/pxe/src/events/event_service.test.ts @@ -103,13 +103,24 @@ describe('validateAndStoreEvent', () => { }; aztecNode.getTxEffect.mockImplementation(() => Promise.resolve(indexedTxEffect)); - await expect(runStoreEvent).rejects.toThrow(/Could not find tx effect for tx hash .* as of block number/); + await expect(runStoreEvent).rejects.toThrow( + /Obtained a newer tx effect for .* for an event validation request than the anchor block/, + ); }); - it('should throw if event commitment is not in the tx effects', async () => { - await expect(runStoreEvent({ eventCommitment: Fr.random() })).rejects.toThrow( - /Event commitment .* is not present in tx/, - ); + it('should not store event if event commitment is not in the tx effects', async () => { + // The service logs a warning and returns early rather than throwing + await runStoreEvent({ eventCommitment: Fr.random() }); + + // Verify event was not stored + const result = await privateEventStore.getPrivateEvents(eventSelector, { + contractAddress, + fromBlock: blockNumber, + toBlock: blockNumber + 1, + scopes: [recipient], + }); + + expect(result.length).toEqual(0); }); it('should store event for later retrieval', async () => { diff --git a/yarn-project/pxe/src/events/event_service.ts b/yarn-project/pxe/src/events/event_service.ts index 10c3f2503f99..4e1103b29239 100644 --- a/yarn-project/pxe/src/events/event_service.ts +++ b/yarn-project/pxe/src/events/event_service.ts @@ -1,4 +1,5 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; +import { createLogger } from '@aztec/foundation/log'; import type { EventSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { siloNullifier } from '@aztec/stdlib/hash'; @@ -13,6 +14,7 @@ export class EventService { private readonly aztecNode: AztecNode, private readonly privateEventStore: PrivateEventStore, private readonly jobId: string, + private readonly log = createLogger('pxe:event_service'), ) {} public async validateAndStoreEvent( @@ -36,19 +38,30 @@ export class EventService { const anchorBlockNumber = this.anchorBlockHeader.getBlockNumber(); if (!txEffect) { - throw new Error(`Could not find tx effect for tx hash ${txHash}`); + // We error out instead of just logging a warning and skipping the event because this would indicate a bug. This + // is because the node has already served info about this tx either when obtaining the log (TxScopedL2Log contain + // tx info) or when getting metadata for the offchain message (before the message got passed to `process_log`). + throw new Error(`Could not find tx effect for tx hash ${txHash} when processing an event.`); } if (txEffect.l2BlockNumber > anchorBlockNumber) { - throw new Error(`Could not find tx effect for tx hash ${txHash} as of block number ${anchorBlockNumber}`); + // We should never process a message from a tx past the anchor block. If we got here, a preprocessing step made + // a mistake. + throw new Error( + `Obtained a newer tx effect for ${txHash} for an event validation request than the anchor block ${anchorBlockNumber}. This is a bug as smart contracts should not issue event validation requests for events from blocks newer than the anchor block.`, + ); } // Find the index of the event commitment in the nullifiers array to determine event ordering within the tx const eventIndexInTx = txEffect.data.nullifiers.findIndex(n => n.equals(siloedEventCommitment)); if (eventIndexInTx === -1) { - throw new Error( - `Event commitment ${eventCommitment} (siloed as ${siloedEventCommitment}) is not present in tx ${txHash}`, + // Unlike in NoteService, this might not be a bug since the commitment hasn't been verified yet in the message + // processing pipeline. A malformed or malicious message could trigger this condition. Because of this we don't + // error out and we just show a warning. + this.log.warn( + `Skipping event whose commitment is not present in its tx. siloedEventCommitment=${siloedEventCommitment}, contract=${contractAddress}, selector=${selector}, eventCommitment=${eventCommitment}, txHash=${txHash}`, ); + return; } return this.privateEventStore.storePrivateEventLog( diff --git a/yarn-project/pxe/src/logs/log_service.ts b/yarn-project/pxe/src/logs/log_service.ts index 7da90c092a62..c0a46e64148c 100644 --- a/yarn-project/pxe/src/logs/log_service.ts +++ b/yarn-project/pxe/src/logs/log_service.ts @@ -12,7 +12,6 @@ import { } from '@aztec/stdlib/logs'; import type { BlockHeader } from '@aztec/stdlib/tx'; -import type { AccessScopes } from '../access_scopes.js'; import type { LogRetrievalRequest } from '../contract_function_simulator/noir-structs/log_retrieval_request.js'; import { LogRetrievalResponse } from '../contract_function_simulator/noir-structs/log_retrieval_response.js'; import { AddressStore } from '../storage/address_store/address_store.js'; @@ -114,46 +113,40 @@ export class LogService { ); } - public async fetchTaggedLogs(contractAddress: AztecAddress, pendingTaggedLogArrayBaseSlot: Fr, scopes: AccessScopes) { + public async fetchTaggedLogs( + contractAddress: AztecAddress, + pendingTaggedLogArrayBaseSlot: Fr, + recipient: AztecAddress, + ) { this.log.verbose(`Fetching tagged logs for ${contractAddress.toString()}`); // We only load logs from block up to and including the anchor block number const anchorBlockNumber = this.anchorBlockHeader.getBlockNumber(); const anchorBlockHash = await this.anchorBlockHeader.hash(); - // Determine recipients: use scopes if provided, otherwise get all accounts - const recipients = scopes !== 'ALL_SCOPES' && scopes.length > 0 ? scopes : await this.keyStore.getAccounts(); - - // For each recipient, fetch secrets, load logs, and store them. - // We run these per-recipient tasks in parallel so that logs are loaded for all recipients concurrently. - await Promise.all( - recipients.map(async recipient => { - // Get all secrets for this recipient (one per sender) - const secrets = await this.#getSecretsForSenders(contractAddress, recipient); - - // Load logs for all sender-recipient pairs in parallel - const logArrays = await Promise.all( - secrets.map(secret => - loadPrivateLogsForSenderRecipientPair( - secret, - this.aztecNode, - this.recipientTaggingStore, - anchorBlockNumber, - anchorBlockHash, - this.jobId, - ), - ), - ); + // Get all secrets for this recipient (one per sender) + const secrets = await this.#getSecretsForSenders(contractAddress, recipient); + + // Load logs for all sender-recipient pairs in parallel + const logArrays = await Promise.all( + secrets.map(secret => + loadPrivateLogsForSenderRecipientPair( + secret, + this.aztecNode, + this.recipientTaggingStore, + anchorBlockNumber, + anchorBlockHash, + this.jobId, + ), + ), + ); - // Flatten all logs from all secrets - const allLogs = logArrays.flat(); + // Flatten all logs from all secrets + const allLogs = logArrays.flat(); - // Store the logs for this recipient - if (allLogs.length > 0) { - await this.#storePendingTaggedLogs(contractAddress, pendingTaggedLogArrayBaseSlot, recipient, allLogs); - } - }), - ); + if (allLogs.length > 0) { + await this.#storePendingTaggedLogs(contractAddress, pendingTaggedLogArrayBaseSlot, recipient, allLogs); + } } async #getSecretsForSenders( @@ -201,13 +194,18 @@ export class LogService { scopedLog.txHash, scopedLog.noteHashes, scopedLog.firstNullifier, - recipient, ); return pendingTaggedLog.toFields(); }); // TODO: This looks like it could belong more at the oracle interface level - return this.capsuleStore.appendToCapsuleArray(contractAddress, capsuleArrayBaseSlot, pendingTaggedLogs, this.jobId); + return this.capsuleStore.appendToCapsuleArray( + contractAddress, + capsuleArrayBaseSlot, + pendingTaggedLogs, + this.jobId, + recipient, + ); } } diff --git a/yarn-project/pxe/src/messages/message_context_service.test.ts b/yarn-project/pxe/src/messages/message_context_service.test.ts index 6449f1e1b6cf..d81cd8034fb3 100644 --- a/yarn-project/pxe/src/messages/message_context_service.test.ts +++ b/yarn-project/pxe/src/messages/message_context_service.test.ts @@ -2,11 +2,11 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { BlockHash } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; +import { MessageContext } from '@aztec/stdlib/logs'; import { TxHash } from '@aztec/stdlib/tx'; import { mock } from 'jest-mock-extended'; -import { MessageTxContext } from '../contract_function_simulator/noir-structs/message_tx_context.js'; import { MessageContextService } from './message_context_service.js'; describe('MessageContextService', () => { @@ -63,7 +63,7 @@ describe('MessageContextService', () => { ); }); - it('resolves a valid tx hash into a MessageTxContext', async () => { + it('resolves a valid tx hash into a MessageContext', async () => { const txHash = TxHash.random(); const noteHashes = [Fr.random(), Fr.random()]; const firstNullifier = Fr.random(); @@ -77,7 +77,7 @@ describe('MessageContextService', () => { const results = await service.resolveMessageContexts([txHash.hash], anchorBlockNumber); - expect(results).toEqual([new MessageTxContext(txHash, noteHashes, firstNullifier)]); + expect(results).toEqual([new MessageContext(txHash, noteHashes, firstNullifier)]); }); it('resolves tx hashes in different situations', async () => { @@ -111,14 +111,14 @@ describe('MessageContextService', () => { const results = await service.resolveMessageContexts( [ Fr.ZERO, // zero → null - validTxHash.hash, // valid → MessageTxContext + validTxHash.hash, // valid → MessageContext notFoundTxHash.hash, // not found → null futureTxHash.hash, // beyond anchor → null ], anchorBlockNumber, ); - expect(results).toEqual([null, new MessageTxContext(validTxHash, validNoteHashes, validNullifier), null, null]); + expect(results).toEqual([null, new MessageContext(validTxHash, validNoteHashes, validNullifier), null, null]); // Zero hash should not trigger getTxEffect expect(aztecNode.getTxEffect).toHaveBeenCalledTimes(3); diff --git a/yarn-project/pxe/src/messages/message_context_service.ts b/yarn-project/pxe/src/messages/message_context_service.ts index ae39812a7c3b..3139f1a4e3dd 100644 --- a/yarn-project/pxe/src/messages/message_context_service.ts +++ b/yarn-project/pxe/src/messages/message_context_service.ts @@ -1,9 +1,8 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; +import { MessageContext } from '@aztec/stdlib/logs'; import { TxHash } from '@aztec/stdlib/tx'; -import { MessageTxContext } from '../contract_function_simulator/noir-structs/message_tx_context.js'; - /** Resolves transaction hashes into the context needed to process messages. */ export class MessageContextService { constructor(private readonly aztecNode: AztecNode) {} @@ -15,7 +14,7 @@ export class MessageContextService { * process messages that originated from that transaction. Returns `null` for tx hashes that are zero, not yet * available, or in blocks beyond the anchor block. */ - resolveMessageContexts(txHashes: Fr[], anchorBlockNumber: number): Promise<(MessageTxContext | null)[]> { + resolveMessageContexts(txHashes: Fr[], anchorBlockNumber: number): Promise<(MessageContext | null)[]> { // TODO: optimize, we might be hitting the node to get the same txHash repeatedly return Promise.all( txHashes.map(async txHashField => { @@ -38,7 +37,7 @@ export class MessageContextService { throw new Error(`Tx effect for ${txHash} has no nullifiers`); } - return new MessageTxContext(data.txHash, data.noteHashes, data.nullifiers[0]); + return new MessageContext(data.txHash, data.noteHashes, data.nullifiers[0]); }), ); } diff --git a/yarn-project/pxe/src/notes/note_service.test.ts b/yarn-project/pxe/src/notes/note_service.test.ts index f48af2861dc6..d29529735c65 100644 --- a/yarn-project/pxe/src/notes/note_service.test.ts +++ b/yarn-project/pxe/src/notes/note_service.test.ts @@ -340,7 +340,7 @@ describe('NoteService', () => { txHash, recipient.address, ), - ).rejects.toThrow(/as of block number/); + ).rejects.toThrow(/Obtained a newer tx effect for .* for a note validation request than the anchor block/); }); it('should nullify note if nullifier index is found', async () => { diff --git a/yarn-project/pxe/src/notes/note_service.ts b/yarn-project/pxe/src/notes/note_service.ts index 3e1bd42a9704..dd50499c3ffd 100644 --- a/yarn-project/pxe/src/notes/note_service.ts +++ b/yarn-project/pxe/src/notes/note_service.ts @@ -121,7 +121,7 @@ export class NoteService { noteHash: Fr, nullifier: Fr, txHash: TxHash, - recipient: AztecAddress, + scope: AztecAddress, ): Promise { // We are going to store the new note in the NoteStore, which will let us later return it via `getNotes`. // There's two things we need to check before we do this however: @@ -155,16 +155,28 @@ export class NoteService { this.aztecNode.findLeavesIndexes(anchorBlockHash, MerkleTreeId.NULLIFIER_TREE, [siloedNullifier]), ]); if (!txEffect) { - throw new Error(`Could not find tx effect for tx hash ${txHash}`); + // We error out instead of just logging a warning and skipping the note because this would indicate a bug. This + // is because the node has already served info about this tx either when obtaining the log (TxScopedL2Log contain + // tx info) or when getting metadata for the offchain message (before the message got passed to `process_log`). + throw new Error(`Could not find tx effect for tx hash ${txHash} when processing a note.`); } if (txEffect.l2BlockNumber > anchorBlockNumber) { - throw new Error(`Could not find tx effect for tx hash ${txHash} as of block number ${anchorBlockNumber}`); + // If the message was delivered onchain, this would indicate a bug: log sync should never load logs from blocks + // newer than the anchor block. If the note came via an offchain message, it would likely also be a bug, since we + // sync a new anchor block before calling `process_message`. For this not to be a bug, the message would need to + // come from a newer block than the anchor served by the node, implying the node isn't properly synced. + // We therefore error out here rather than assuming the offchain message was constructed by a malicious + // sender with the intention of bricking recipient's PXE (if we assumed that we would just ignore the message). + throw new Error( + `Obtained a newer tx effect for ${txHash} for a note validation request than the anchor block ${anchorBlockNumber}. This is a bug as we should not ever be processing a note from a newer block than the anchor block.`, + ); } // Find the index of the note hash in the noteHashes array to determine note ordering within the tx const noteIndexInTx = txEffect.data.noteHashes.findIndex(nh => nh.equals(uniqueNoteHash)); if (noteIndexInTx === -1) { + // Similar to the comment above - we error out as this would indicate a bug in nonce discovery. throw new Error(`Note hash ${noteHash} (uniqued as ${uniqueNoteHash}) is not present in tx ${txHash}`); } @@ -184,8 +196,7 @@ export class NoteService { noteIndexInTx, ); - // The note was found by `recipient`, so we use that as the scope when storing the note. - await this.noteStore.addNotes([noteDao], recipient, this.jobId); + await this.noteStore.addNotes([noteDao], scope, this.jobId); if (nullifierIndex !== undefined) { // We found nullifier index which implies that the note has already been nullified. diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index e4ecf3b179c1..869b34e473aa 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -4,9 +4,9 @@ /// /// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is called /// and if the oracle version is incompatible an error is thrown. -export const ORACLE_VERSION = 18; +export const ORACLE_VERSION = 21; /// This hash is computed as by hashing the Oracle interface and it is used to detect when the Oracle interface changes, /// which in turn implies that you need to update the ORACLE_VERSION constant in this file and in /// `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. -export const ORACLE_INTERFACE_HASH = '57e5b07c6d55fb167ef90f8d0f410f9bdb5b154a31159c624a061be40b02a2c2'; +export const ORACLE_INTERFACE_HASH = '83f1de1a9741a34916fd58cf12b857d0bac90f74bf00751b20304301a3f5c8eb'; diff --git a/yarn-project/pxe/src/pxe.ts b/yarn-project/pxe/src/pxe.ts index 47aea6b5b89d..dd27286ceb0d 100644 --- a/yarn-project/pxe/src/pxe.ts +++ b/yarn-project/pxe/src/pxe.ts @@ -215,6 +215,7 @@ export class PXE { node, contractStore, noteStore, + () => keyStore.getAccounts(), createLogger('pxe:contract_sync', bindings), ); const messageContextService = new MessageContextService(node); @@ -502,7 +503,9 @@ export class PXE { * @returns The synced block header */ public getSyncedBlockHeader(): Promise { - return this.anchorBlockStore.getBlockHeader(); + return this.#putInJobQueue(() => { + return this.anchorBlockStore.getBlockHeader(); + }); } /** diff --git a/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts b/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts index 51217891caee..501276f802b0 100644 --- a/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts +++ b/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts @@ -7,14 +7,14 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { CapsuleStore } from './capsule_store.js'; describe('capsule data provider', () => { + let scope: AztecAddress; let contract: AztecAddress; let capsuleStore: CapsuleStore; let store: AztecLMDBStoreV2; beforeEach(async () => { - // Setup mock contract address contract = await AztecAddress.random(); - // Setup store + scope = await AztecAddress.random(); store = await openTmpStore('capsule_store_test'); capsuleStore = new CapsuleStore(store); }); @@ -24,8 +24,8 @@ describe('capsule data provider', () => { const slot = new Fr(1); const values = [new Fr(42)]; - capsuleStore.storeCapsule(contract, slot, values, 'test'); - const result = await capsuleStore.loadCapsule(contract, slot, 'test'); + capsuleStore.storeCapsule(contract, slot, values, 'test', scope); + const result = await capsuleStore.loadCapsule(contract, slot, 'test', scope); expect(result).toEqual(values); }); @@ -33,8 +33,8 @@ describe('capsule data provider', () => { const slot = new Fr(1); const values = [new Fr(42), new Fr(43), new Fr(44)]; - capsuleStore.storeCapsule(contract, slot, values, 'test'); - const result = await capsuleStore.loadCapsule(contract, slot, 'test'); + capsuleStore.storeCapsule(contract, slot, values, 'test', scope); + const result = await capsuleStore.loadCapsule(contract, slot, 'test', scope); expect(result).toEqual(values); }); @@ -43,10 +43,10 @@ describe('capsule data provider', () => { const initialValues = [new Fr(42)]; const newValues = [new Fr(100)]; - capsuleStore.storeCapsule(contract, slot, initialValues, 'test'); - capsuleStore.storeCapsule(contract, slot, newValues, 'test'); + capsuleStore.storeCapsule(contract, slot, initialValues, 'test', scope); + capsuleStore.storeCapsule(contract, slot, newValues, 'test', scope); - const result = await capsuleStore.loadCapsule(contract, slot, 'test'); + const result = await capsuleStore.loadCapsule(contract, slot, 'test', scope); expect(result).toEqual(newValues); }); @@ -56,19 +56,44 @@ describe('capsule data provider', () => { const values1 = [new Fr(42)]; const values2 = [new Fr(100)]; - capsuleStore.storeCapsule(contract, slot, values1, 'test'); - capsuleStore.storeCapsule(anotherContract, slot, values2, 'test'); + capsuleStore.storeCapsule(contract, slot, values1, 'test', scope); + capsuleStore.storeCapsule(anotherContract, slot, values2, 'test', scope); - const result1 = await capsuleStore.loadCapsule(contract, slot, 'test'); - const result2 = await capsuleStore.loadCapsule(anotherContract, slot, 'test'); + const result1 = await capsuleStore.loadCapsule(contract, slot, 'test', scope); + const result2 = await capsuleStore.loadCapsule(anotherContract, slot, 'test', scope); expect(result1).toEqual(values1); expect(result2).toEqual(values2); }); + it('stores values for different scopes independently', async () => { + const scopeA = await AztecAddress.random(); + const scopeB = await AztecAddress.random(); + const slot = new Fr(1); + const valuesA = [new Fr(42)]; + const valuesB = [new Fr(100)]; + + capsuleStore.storeCapsule(contract, slot, valuesA, 'test', scopeA); + capsuleStore.storeCapsule(contract, slot, valuesB, 'test', scopeB); + + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scopeA)).toEqual(valuesA); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scopeB)).toEqual(valuesB); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scope)).toBeNull(); + }); + + it('different scopes are isolated', async () => { + const slot = new Fr(1); + const values = [new Fr(42)]; + + capsuleStore.storeCapsule(contract, slot, values, 'test', scope); + + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scope)).toEqual(values); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', AztecAddress.ZERO)).toBeNull(); + }); + it('returns null for non-existent slots', async () => { const slot = Fr.random(); - const result = await capsuleStore.loadCapsule(contract, slot, 'test'); + const result = await capsuleStore.loadCapsule(contract, slot, 'test', scope); expect(result).toBeNull(); }); }); @@ -78,17 +103,36 @@ describe('capsule data provider', () => { const slot = new Fr(1); const values = [new Fr(42)]; - capsuleStore.storeCapsule(contract, slot, values, 'test'); - capsuleStore.deleteCapsule(contract, slot, 'test'); + capsuleStore.storeCapsule(contract, slot, values, 'test', scope); + capsuleStore.deleteCapsule(contract, slot, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, slot, 'test')).toBeNull(); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scope)).toBeNull(); }); it('deletes an empty slot', async () => { const slot = new Fr(1); - capsuleStore.deleteCapsule(contract, slot, 'test'); + capsuleStore.deleteCapsule(contract, slot, 'test', scope); + + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scope)).toBeNull(); + }); + + it('deletes a scoped capsule without affecting other scopes', async () => { + const scopeA = await AztecAddress.random(); + const scopeB = await AztecAddress.random(); + const slot = Fr.random(); + const valuesA = [Fr.random(), Fr.random(), Fr.random()]; + const valuesB = [Fr.random(), Fr.random(), Fr.random()]; + const globalValues = [Fr.random(), Fr.random(), Fr.random()]; - expect(await capsuleStore.loadCapsule(contract, slot, 'test')).toBeNull(); + capsuleStore.storeCapsule(contract, slot, valuesA, 'test', scopeA); + capsuleStore.storeCapsule(contract, slot, valuesB, 'test', scopeB); + capsuleStore.storeCapsule(contract, slot, globalValues, 'test', scope); + + capsuleStore.deleteCapsule(contract, slot, 'test', scopeA); + + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scopeA)).toBeNull(); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scopeB)).toEqual(valuesB); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scope)).toEqual(globalValues); }); }); @@ -97,85 +141,98 @@ describe('capsule data provider', () => { const slot = new Fr(1); const values = [new Fr(42)]; - capsuleStore.storeCapsule(contract, slot, values, 'test'); + capsuleStore.storeCapsule(contract, slot, values, 'test', scope); const dstSlot = new Fr(5); - await capsuleStore.copyCapsule(contract, slot, dstSlot, 1, 'test'); + await capsuleStore.copyCapsule(contract, slot, dstSlot, 1, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, dstSlot, 'test')).toEqual(values); + expect(await capsuleStore.loadCapsule(contract, dstSlot, 'test', scope)).toEqual(values); }); it('copies multiple non-overlapping values', async () => { const src = new Fr(1); const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test'); + capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test', scope); const dst = new Fr(5); - await capsuleStore.copyCapsule(contract, src, dst, 3, 'test'); + await capsuleStore.copyCapsule(contract, src, dst, 3, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, dst, 'test')).toEqual(valuesArray[0]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test')).toEqual(valuesArray[1]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test')).toEqual(valuesArray[2]); + expect(await capsuleStore.loadCapsule(contract, dst, 'test', scope)).toEqual(valuesArray[0]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test', scope)).toEqual(valuesArray[1]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test', scope)).toEqual(valuesArray[2]); }); it('copies overlapping values with src ahead', async () => { const src = new Fr(1); const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test'); + capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test', scope); const dst = new Fr(2); - await capsuleStore.copyCapsule(contract, src, dst, 3, 'test'); + await capsuleStore.copyCapsule(contract, src, dst, 3, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, dst, 'test')).toEqual(valuesArray[0]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test')).toEqual(valuesArray[1]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test')).toEqual(valuesArray[2]); + expect(await capsuleStore.loadCapsule(contract, dst, 'test', scope)).toEqual(valuesArray[0]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test', scope)).toEqual(valuesArray[1]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test', scope)).toEqual(valuesArray[2]); // Slots 2 and 3 (src[1] and src[2]) should have been overwritten since they are also dst[0] and dst[1] - expect(await capsuleStore.loadCapsule(contract, src, 'test')).toEqual(valuesArray[0]); // src[0] (unchanged) - expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(1)), 'test')).toEqual(valuesArray[0]); // dst[0] - expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(2)), 'test')).toEqual(valuesArray[1]); // dst[1] + expect(await capsuleStore.loadCapsule(contract, src, 'test', scope)).toEqual(valuesArray[0]); // src[0] (unchanged) + expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(1)), 'test', scope)).toEqual(valuesArray[0]); // dst[0] + expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(2)), 'test', scope)).toEqual(valuesArray[1]); // dst[1] }); it('copies overlapping values with dst ahead', async () => { const src = new Fr(5); const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test'); + capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test', scope); const dst = new Fr(4); - await capsuleStore.copyCapsule(contract, src, dst, 3, 'test'); + await capsuleStore.copyCapsule(contract, src, dst, 3, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, dst, 'test')).toEqual(valuesArray[0]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test')).toEqual(valuesArray[1]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test')).toEqual(valuesArray[2]); + expect(await capsuleStore.loadCapsule(contract, dst, 'test', scope)).toEqual(valuesArray[0]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test', scope)).toEqual(valuesArray[1]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test', scope)).toEqual(valuesArray[2]); // Slots 5 and 6 (src[0] and src[1]) should have been overwritten since they are also dst[1] and dst[2] - expect(await capsuleStore.loadCapsule(contract, src, 'test')).toEqual(valuesArray[1]); // dst[1] - expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(1)), 'test')).toEqual(valuesArray[2]); // dst[2] - expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(2)), 'test')).toEqual(valuesArray[2]); // src[2] (unchanged) + expect(await capsuleStore.loadCapsule(contract, src, 'test', scope)).toEqual(valuesArray[1]); // dst[1] + expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(1)), 'test', scope)).toEqual(valuesArray[2]); // dst[2] + expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(2)), 'test', scope)).toEqual(valuesArray[2]); // src[2] (unchanged) }); it('copying fails if any value is empty', async () => { const src = new Fr(1); const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test'); + capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test', scope); // We skip src[1] - capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test'); + capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test', scope); const dst = new Fr(5); - await expect(capsuleStore.copyCapsule(contract, src, dst, 3, 'test')).rejects.toThrow( + await expect(capsuleStore.copyCapsule(contract, src, dst, 3, 'test', scope)).rejects.toThrow( 'Attempted to copy empty slot', ); }); + + it('copies values within a scope only', async () => { + const scope = await AztecAddress.random(); + const src = new Fr(1); + const dst = new Fr(5); + const values = [new Fr(42)]; + + capsuleStore.storeCapsule(contract, src, values, 'test', scope); + await capsuleStore.copyCapsule(contract, src, dst, 1, 'test', scope); + + expect(await capsuleStore.loadCapsule(contract, dst, 'test', scope)).toEqual(values); + expect(await capsuleStore.loadCapsule(contract, dst, 'test', AztecAddress.ZERO)).toBeNull(); + }); }); describe('arrays', () => { @@ -184,11 +241,13 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const array = range(4).map(x => [new Fr(x)]); - await capsuleStore.appendToCapsuleArray(contract, baseSlot, array, 'test'); + await capsuleStore.appendToCapsuleArray(contract, baseSlot, array, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, baseSlot, 'test')).toEqual([new Fr(array.length)]); + expect(await capsuleStore.loadCapsule(contract, baseSlot, 'test', scope)).toEqual([new Fr(array.length)]); for (const i of range(array.length)) { - expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test')).toEqual(array[i]); + expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test', scope)).toEqual( + array[i], + ); } }); @@ -196,16 +255,16 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const originalArray = range(4).map(x => [new Fr(x)]); - await capsuleStore.appendToCapsuleArray(contract, baseSlot, originalArray, 'test'); + await capsuleStore.appendToCapsuleArray(contract, baseSlot, originalArray, 'test', scope); const newElements = [[new Fr(13)], [new Fr(42)]]; - await capsuleStore.appendToCapsuleArray(contract, baseSlot, newElements, 'test'); + await capsuleStore.appendToCapsuleArray(contract, baseSlot, newElements, 'test', scope); const expectedLength = originalArray.length + newElements.length; - expect(await capsuleStore.loadCapsule(contract, baseSlot, 'test')).toEqual([new Fr(expectedLength)]); + expect(await capsuleStore.loadCapsule(contract, baseSlot, 'test', scope)).toEqual([new Fr(expectedLength)]); for (const i of range(expectedLength)) { - expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test')).toEqual( + expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test', scope)).toEqual( [...originalArray, ...newElements][i], ); } @@ -215,7 +274,7 @@ describe('capsule data provider', () => { describe('readCapsuleArray', () => { it('reads an empty array', async () => { const baseSlot = new Fr(3); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual([]); }); @@ -223,9 +282,9 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const storedArray = range(4).map(x => [new Fr(x)]); - await capsuleStore.appendToCapsuleArray(contract, baseSlot, storedArray, 'test'); + await capsuleStore.appendToCapsuleArray(contract, baseSlot, storedArray, 'test', scope); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual(storedArray); }); @@ -233,10 +292,10 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); // Store in the base slot a non-zero value, indicating a non-zero array length - capsuleStore.storeCapsule(contract, baseSlot, [new Fr(1)], 'test'); + capsuleStore.storeCapsule(contract, baseSlot, [new Fr(1)], 'test', scope); // Reading should now fail as some of the capsules in the array are empty - await expect(capsuleStore.readCapsuleArray(contract, baseSlot, 'test')).rejects.toThrow( + await expect(capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope)).rejects.toThrow( 'Expected non-empty value', ); }); @@ -247,9 +306,9 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const newArray = range(4).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test', scope); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual(newArray); }); @@ -257,12 +316,12 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const originalArray = range(4, 0).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test', scope); const newArray = range(10, 10).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test', scope); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual(newArray); }); @@ -270,18 +329,18 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const originalArray = range(10, 0).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test', scope); const newArray = range(4, 10).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test', scope); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual(newArray); // Not only do we read the expected array, but also all capsules past the new array length have been cleared for (const i of range(originalArray.length - newArray.length)) { expect( - await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + newArray.length + i)), 'test'), + await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + newArray.length + i)), 'test', scope), ).toBeNull(); } }); @@ -290,16 +349,16 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const originalArray = range(10, 0).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test', scope); - await capsuleStore.setCapsuleArray(contract, baseSlot, [], 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, [], 'test', scope); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual([]); // All capsules from the original array have been cleared for (const i of range(originalArray.length)) { - expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test')).toBeNull(); + expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test', scope)).toBeNull(); } }); }); @@ -327,6 +386,7 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { @@ -344,6 +404,7 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { @@ -361,6 +422,7 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { @@ -368,7 +430,13 @@ describe('capsule data provider', () => { }); // Append a single element - await capsuleStore.appendToCapsuleArray(contract, new Fr(0), [range(ARRAY_LENGTH).map(x => new Fr(x))], 'test'); + await capsuleStore.appendToCapsuleArray( + contract, + new Fr(0), + [range(ARRAY_LENGTH).map(x => new Fr(x))], + 'test', + scope, + ); await store.transactionAsync(async () => { await capsuleStore.commit('test'); @@ -385,6 +453,7 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { @@ -392,7 +461,7 @@ describe('capsule data provider', () => { }); // We just move the entire thing one slot. - await capsuleStore.copyCapsule(contract, new Fr(0), new Fr(1), NUMBER_OF_ITEMS, 'test'); + await capsuleStore.copyCapsule(contract, new Fr(0), new Fr(1), NUMBER_OF_ITEMS, 'test', scope); await store.transactionAsync(async () => { await capsuleStore.commit('test'); @@ -409,13 +478,14 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { await capsuleStore.commit('test'); }); - await capsuleStore.readCapsuleArray(contract, new Fr(0), 'test'); + await capsuleStore.readCapsuleArray(contract, new Fr(0), 'test', scope); await store.transactionAsync(async () => { await capsuleStore.commit('test'); @@ -432,13 +502,14 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { await capsuleStore.commit('test'); }); - await capsuleStore.setCapsuleArray(contract, new Fr(0), [], 'test'); + await capsuleStore.setCapsuleArray(contract, new Fr(0), [], 'test', scope); await store.transactionAsync(async () => { await capsuleStore.commit('test'); @@ -460,20 +531,20 @@ describe('capsule data provider', () => { const committedValues1 = [Fr.random()]; const committedValues2 = [Fr.random()]; - capsuleStore.storeCapsule(contract, slot, committedValues1, 'job-1'); + capsuleStore.storeCapsule(contract, slot, committedValues1, 'job-1', scope); // After this commit, 'job-1' should logically be reset // Any read of contract-slot after this should see committedValues1 await capsuleStore.commit('job-1'); // Any read of contract-slot should see job2committedValues - capsuleStore.storeCapsule(contract, slot, committedValues2, 'job-2'); + capsuleStore.storeCapsule(contract, slot, committedValues2, 'job-2', scope); await capsuleStore.commit('job-2'); // If we failed to properly dispose 'job-1's staged writes on commit, // Instead of reading committedValues2 (as we should), we would end // up reading committedValues1 (which would be wrong) - expect(await capsuleStore.loadCapsule(contract, slot, 'job-1')).toEqual(committedValues2); + expect(await capsuleStore.loadCapsule(contract, slot, 'job-1', scope)).toEqual(committedValues2); }); it('writes to job view are isolated from another job view', async () => { @@ -485,17 +556,17 @@ describe('capsule data provider', () => { const stagedJob2: string = 'staged-job-2'; // First set a committed capsule (using a different job that we commit) - capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId); + capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId, scope); await capsuleStore.commit(commitJobId); // Then set a staged capsule (not committed) - capsuleStore.storeCapsule(contract, slot, stagedValues, stagedJob1); + capsuleStore.storeCapsule(contract, slot, stagedValues, stagedJob1, scope); // With jobId=1, should get staged capsule - expect(await capsuleStore.loadCapsule(contract, slot, stagedJob1)).toEqual(stagedValues); + expect(await capsuleStore.loadCapsule(contract, slot, stagedJob1, scope)).toEqual(stagedValues); // With jobId=2, should get committed capsule - expect(await capsuleStore.loadCapsule(contract, slot, stagedJob2)).toEqual(committedValues); + expect(await capsuleStore.loadCapsule(contract, slot, stagedJob2, scope)).toEqual(committedValues); }); it('staged deletions hide committed data', async () => { @@ -506,17 +577,17 @@ describe('capsule data provider', () => { const stagedJob2: string = 'staged-job-2'; // First set a committed capsule - capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId); + capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId, scope); await capsuleStore.commit(commitJobId); // Delete in staging (not committed) - capsuleStore.deleteCapsule(contract, slot, stagedJob1); + capsuleStore.deleteCapsule(contract, slot, stagedJob1, scope); // Without jobId=2, should still see committed capsule - expect(await capsuleStore.loadCapsule(contract, slot, stagedJob2)).toEqual(committedValues); + expect(await capsuleStore.loadCapsule(contract, slot, stagedJob2, scope)).toEqual(committedValues); // With jobId=1, should see null (deleted in staging) - expect(await capsuleStore.loadCapsule(contract, slot, stagedJob1)).toBeNull(); + expect(await capsuleStore.loadCapsule(contract, slot, stagedJob1, scope)).toBeNull(); }); it('commit applies staged deletions', async () => { @@ -525,14 +596,14 @@ describe('capsule data provider', () => { const commitJobId: string = 'commit-job'; const deleteJobId: string = 'delete-job'; - capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId); + capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId, scope); await capsuleStore.commit(commitJobId); - capsuleStore.deleteCapsule(contract, slot, deleteJobId); + capsuleStore.deleteCapsule(contract, slot, deleteJobId, scope); await capsuleStore.commit(deleteJobId); // Now any job should see this null (deleted) - expect(await capsuleStore.loadCapsule(contract, slot, 'any-job-sees-this')).toBeNull(); + expect(await capsuleStore.loadCapsule(contract, slot, 'any-job-sees-this', scope)).toBeNull(); }); it('discardStaged removes staged data without affecting main', async () => { @@ -542,17 +613,17 @@ describe('capsule data provider', () => { const commitJobId: string = 'commit-job'; const stagingJobId: string = 'staging-job'; - capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId); + capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId, scope); await capsuleStore.commit(commitJobId); - capsuleStore.storeCapsule(contract, slot, stagedValues, stagingJobId); + capsuleStore.storeCapsule(contract, slot, stagedValues, stagingJobId, scope); await capsuleStore.discardStaged(stagingJobId); // Should still get committed capsule - expect(await capsuleStore.loadCapsule(contract, slot, 'any-job')).toEqual(committedValues); + expect(await capsuleStore.loadCapsule(contract, slot, 'any-job', scope)).toEqual(committedValues); // With stagingJobId should fall back to committed since staging was discarded - expect(await capsuleStore.loadCapsule(contract, slot, stagingJobId)).toEqual(committedValues); + expect(await capsuleStore.loadCapsule(contract, slot, stagingJobId, scope)).toEqual(committedValues); }); }); }); diff --git a/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts b/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts index 0b847c06df52..4986a6e94a30 100644 --- a/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts +++ b/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts @@ -1,7 +1,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, createLogger } from '@aztec/foundation/log'; import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { StagedStore } from '../../job_coordinator/job_coordinator.js'; @@ -10,11 +10,12 @@ export class CapsuleStore implements StagedStore { #store: AztecAsyncKVStore; - // Arbitrary data stored by contracts. Key is computed as `${contractAddress}:${key}` + // Arbitrary data stored by contracts. Key is computed as `${contractAddress}:${scope}:${key}`, using the zero + // address for the global scope. #capsules: AztecAsyncMap; - // jobId => `${contractAddress}:${key}` => capsule data - // when `#stagedCapsules.get('some-job-id').get('${some-contract-address:some-key') === null`, + // jobId => `${contractAddress}:${scope}:${key}` => capsule data + // when `#stagedCapsules.get('some-job-id').get('${some-contract-address}:${some-scope}:${some-key}') === null`, // it signals that the capsule was deleted during the job, so it needs to be deleted on commit #stagedCapsules: Map>; @@ -134,8 +135,8 @@ export class CapsuleStore implements StagedStore { * to public contract storage in that it's indexed by the contract address and storage slot but instead of the global * network state it's backed by local PXE db. */ - storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], jobId: string) { - const dbSlotKey = dbSlotToKey(contractAddress, slot); + storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], jobId: string, scope: AztecAddress) { + const dbSlotKey = dbSlotToKey(contractAddress, slot, scope); // A store overrides any pre-existing data on the slot this.#setOnStage(jobId, dbSlotKey, Buffer.concat(capsule.map(value => value.toBuffer()))); @@ -147,8 +148,8 @@ export class CapsuleStore implements StagedStore { * @param slot - The slot in the database to read. * @returns The stored data or `null` if no data is stored under the slot. */ - async loadCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string): Promise { - const dataBuffer = await this.#getFromStage(jobId, dbSlotToKey(contractAddress, slot)); + async loadCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string, scope: AztecAddress): Promise { + const dataBuffer = await this.#getFromStage(jobId, dbSlotToKey(contractAddress, slot, scope)); if (!dataBuffer) { this.logger.trace(`Data not found for contract ${contractAddress.toString()} and slot ${slot.toString()}`); return null; @@ -165,9 +166,9 @@ export class CapsuleStore implements StagedStore { * @param contractAddress - The contract address under which the data is scoped. * @param slot - The slot in the database to delete. */ - deleteCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string) { + deleteCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string, scope: AztecAddress) { // When we commit this, we will interpret null as a deletion, so we'll propagate the delete to the KV store - this.#deleteOnStage(jobId, dbSlotToKey(contractAddress, slot)); + this.#deleteOnStage(jobId, dbSlotToKey(contractAddress, slot, scope)); } /** @@ -187,6 +188,7 @@ export class CapsuleStore implements StagedStore { dstSlot: Fr, numEntries: number, jobId: string, + scope: AztecAddress, ): Promise { // This transactional context gives us "copy atomicity": // there shouldn't be concurrent writes to what's being copied here. @@ -203,8 +205,8 @@ export class CapsuleStore implements StagedStore { } for (const i of indexes) { - const currentSrcSlot = dbSlotToKey(contractAddress, srcSlot.add(new Fr(i))); - const currentDstSlot = dbSlotToKey(contractAddress, dstSlot.add(new Fr(i))); + const currentSrcSlot = dbSlotToKey(contractAddress, srcSlot.add(new Fr(i)), scope); + const currentDstSlot = dbSlotToKey(contractAddress, dstSlot.add(new Fr(i)), scope); const toCopy = await this.#getFromStage(jobId, currentSrcSlot); if (!toCopy) { @@ -224,7 +226,13 @@ export class CapsuleStore implements StagedStore { * @param baseSlot - The slot where the array length is stored * @param content - Array of capsule data to append */ - appendToCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, content: Fr[][], jobId: string): Promise { + appendToCapsuleArray( + contractAddress: AztecAddress, + baseSlot: Fr, + content: Fr[][], + jobId: string, + scope: AztecAddress, + ): Promise { // We wrap this in a transaction to serialize concurrent calls from Promise.all. // Without this, concurrent appends to the same array could race: both read length=0, // both write at the same slots, one overwrites the other. @@ -232,22 +240,22 @@ export class CapsuleStore implements StagedStore { // and not using a transaction here would heavily impact performance. return this.#store.transactionAsync(async () => { // Load current length, defaulting to 0 if not found - const lengthData = await this.loadCapsule(contractAddress, baseSlot, jobId); + const lengthData = await this.loadCapsule(contractAddress, baseSlot, jobId, scope); const currentLength = lengthData ? lengthData[0].toNumber() : 0; // Store each capsule at consecutive slots after baseSlot + 1 + currentLength for (let i = 0; i < content.length; i++) { const nextSlot = arraySlot(baseSlot, currentLength + i); - this.storeCapsule(contractAddress, nextSlot, content[i], jobId); + this.storeCapsule(contractAddress, nextSlot, content[i], jobId, scope); } // Update length to include all new capsules const newLength = currentLength + content.length; - this.storeCapsule(contractAddress, baseSlot, [new Fr(newLength)], jobId); + this.storeCapsule(contractAddress, baseSlot, [new Fr(newLength)], jobId, scope); }); } - readCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, jobId: string): Promise { + readCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, jobId: string, scope: AztecAddress): Promise { // I'm leaving this transactional context here though because I'm assuming this // gives us "read array atomicity": there shouldn't be concurrent writes to what's being copied // here. @@ -255,14 +263,14 @@ export class CapsuleStore implements StagedStore { // of jobs: different calls running concurrently on the same contract may cause trouble. return this.#store.transactionAsync(async () => { // Load length, defaulting to 0 if not found - const maybeLength = await this.loadCapsule(contractAddress, baseSlot, jobId); + const maybeLength = await this.loadCapsule(contractAddress, baseSlot, jobId, scope); const length = maybeLength ? maybeLength[0].toBigInt() : 0n; const values: Fr[][] = []; // Read each capsule at consecutive slots after baseSlot for (let i = 0; i < length; i++) { - const currentValue = await this.loadCapsule(contractAddress, arraySlot(baseSlot, i), jobId); + const currentValue = await this.loadCapsule(contractAddress, arraySlot(baseSlot, i), jobId, scope); if (currentValue == undefined) { throw new Error( `Expected non-empty value at capsule array in base slot ${baseSlot} at index ${i} for contract ${contractAddress}`, @@ -276,7 +284,7 @@ export class CapsuleStore implements StagedStore { }); } - setCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, content: Fr[][], jobId: string) { + setCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, content: Fr[][], jobId: string, scope: AztecAddress) { // This transactional context in theory isn't so critical now because we aren't // writing to DB so if there's exceptions midway and it blows up, no visible impact // to persistent storage will happen. @@ -287,27 +295,27 @@ export class CapsuleStore implements StagedStore { // of jobs: different calls running concurrently on the same contract may cause trouble. return this.#store.transactionAsync(async () => { // Load current length, defaulting to 0 if not found - const maybeLength = await this.loadCapsule(contractAddress, baseSlot, jobId); + const maybeLength = await this.loadCapsule(contractAddress, baseSlot, jobId, scope); const originalLength = maybeLength ? maybeLength[0].toNumber() : 0; // Set the new length - this.storeCapsule(contractAddress, baseSlot, [new Fr(content.length)], jobId); + this.storeCapsule(contractAddress, baseSlot, [new Fr(content.length)], jobId, scope); // Store the new content, possibly overwriting existing values for (let i = 0; i < content.length; i++) { - this.storeCapsule(contractAddress, arraySlot(baseSlot, i), content[i], jobId); + this.storeCapsule(contractAddress, arraySlot(baseSlot, i), content[i], jobId, scope); } // Clear any stragglers for (let i = content.length; i < originalLength; i++) { - this.deleteCapsule(contractAddress, arraySlot(baseSlot, i), jobId); + this.deleteCapsule(contractAddress, arraySlot(baseSlot, i), jobId, scope); } }); } } -function dbSlotToKey(contractAddress: AztecAddress, slot: Fr): string { - return `${contractAddress.toString()}:${slot.toString()}`; +function dbSlotToKey(contractAddress: AztecAddress, slot: Fr, scope: AztecAddress): string { + return [contractAddress.toString(), scope.toString(), slot.toString()].join(':'); } function arraySlot(baseSlot: Fr, index: number) { diff --git a/yarn-project/pxe/src/storage/metadata.ts b/yarn-project/pxe/src/storage/metadata.ts index 826f90735b91..aa63404894b5 100644 --- a/yarn-project/pxe/src/storage/metadata.ts +++ b/yarn-project/pxe/src/storage/metadata.ts @@ -1 +1 @@ -export const PXE_DATA_SCHEMA_VERSION = 4; +export const PXE_DATA_SCHEMA_VERSION = 5; diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts index 284b83c3870b..52f007683fb2 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts @@ -821,9 +821,14 @@ export class SequencerPublisher { attestationsAndSignersSignature: Signature, options: { forcePendingCheckpointNumber?: CheckpointNumber }, ): Promise { - // Anchor the simulation timestamp to the checkpoint's own slot start time - // rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late. - const ts = checkpoint.header.timestamp; + // When pipelining, the checkpoint targets the next slot so its timestamp is in the future. + // Without pipelining, the checkpoint targets the current slot so its timestamp is in the past + // by the time we simulate (~24s of build time), causing eth_simulateV1 to reject it. + // In that case, use the latest L1 block timestamp + one ethereum slot, which is just ahead + // of L1 and still within the same L2 slot. + const ts = this.epochCache.isProposerPipeliningEnabled() + ? checkpoint.header.timestamp + : (await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration; const blobFields = checkpoint.toBlobFields(); const blobs = await getBlobsPerL1Block(blobFields); const blobInput = getPrefixedEthBlobCommitments(blobs); diff --git a/yarn-project/simulator/src/public/avm/avm_simulator.test.ts b/yarn-project/simulator/src/public/avm/avm_simulator.test.ts index eaeb53080746..a9354bfe30bf 100644 --- a/yarn-project/simulator/src/public/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/public/avm/avm_simulator.test.ts @@ -766,6 +766,8 @@ describe('AVM simulator: transpiled Noir contracts', () => { const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); + // emit_public_log_unsafe prepends a tag at fields[0]. The test contract passes 0 as the tag. + const withTag = (fields: Fr[]) => [Fr.ZERO, ...fields]; const expectedFields = [new Fr(10), new Fr(20), new Fr(30)]; const expectedString = 'Hello, world!'.split('').map(c => new Fr(c.charCodeAt(0))); const expectedCompressedString = [ @@ -775,10 +777,10 @@ describe('AVM simulator: transpiled Noir contracts', () => { const expectedLargeLog = Array.from({ length: 42 }, (_, i) => new Fr(i + 1)); expect(trace.tracePublicLog).toHaveBeenCalledTimes(4); - expect(trace.tracePublicLog).toHaveBeenCalledWith(address, expectedFields); - expect(trace.tracePublicLog).toHaveBeenCalledWith(address, expectedString); - expect(trace.tracePublicLog).toHaveBeenCalledWith(address, expectedCompressedString); - expect(trace.tracePublicLog).toHaveBeenCalledWith(address, expectedLargeLog); + expect(trace.tracePublicLog).toHaveBeenCalledWith(address, withTag(expectedFields)); + expect(trace.tracePublicLog).toHaveBeenCalledWith(address, withTag(expectedString)); + expect(trace.tracePublicLog).toHaveBeenCalledWith(address, withTag(expectedCompressedString)); + expect(trace.tracePublicLog).toHaveBeenCalledWith(address, withTag(expectedLargeLog)); }); }); diff --git a/yarn-project/stdlib/src/abi/decoder.test.ts b/yarn-project/stdlib/src/abi/decoder.test.ts index 03bd867cd98f..6a19c8483296 100644 --- a/yarn-project/stdlib/src/abi/decoder.test.ts +++ b/yarn-project/stdlib/src/abi/decoder.test.ts @@ -1,8 +1,10 @@ import { Fr } from '@aztec/foundation/curves/bn254'; +import { EthAddress } from '@aztec/foundation/eth-address'; import { AztecAddress } from '../aztec-address/index.js'; import type { ABIParameterVisibility, FunctionArtifact } from './abi.js'; -import { decodeFromAbi, decodeFunctionSignature, decodeFunctionSignatureWithParameterNames } from './decoder.js'; +import { decodeFromAbi } from './decoder.js'; +import { decodeFunctionSignature, decodeFunctionSignatureWithParameterNames } from './function_signature_decoder.js'; describe('abi/decoder', () => { // Copied from noir-contracts/contracts/test_contract/target/Test.json @@ -325,4 +327,68 @@ describe('decoder', () => { expect(decoded).toBeUndefined(); }); + + it('decodes EthAddress struct as EthAddress instance', () => { + const field = new Fr(0xdeadbeefn); + const decoded = decodeFromAbi( + [ + { + kind: 'struct', + path: 'aztec::protocol_types::address::EthAddress', + fields: [{ name: 'inner', type: { kind: 'field' } }], + }, + ], + [field], + ); + + expect(decoded).toBeInstanceOf(EthAddress); + expect(decoded).toEqual(EthAddress.fromField(field)); + }); + + it('decodes wrapped field struct as Fr', () => { + const field = new Fr(42n); + const decoded = decodeFromAbi( + [ + { + kind: 'struct', + path: 'some::custom::WrappedType', + fields: [{ name: 'inner', type: { kind: 'field' } }], + }, + ], + [field], + ); + + expect(decoded).toBeInstanceOf(Fr); + expect(decoded).toEqual(field); + }); + + it('decodes EthAddress inside a larger struct', () => { + const addressField = new Fr(0x1234n); + const amountField = new Fr(100n); + const decoded = decodeFromAbi( + [ + { + kind: 'struct', + path: 'MyContract::MyEvent', + fields: [ + { + name: 'recipient', + type: { + kind: 'struct', + path: 'aztec::protocol_types::address::EthAddress', + fields: [{ name: 'inner', type: { kind: 'field' } }], + }, + }, + { name: 'amount', type: { kind: 'field' } }, + ], + }, + ], + [addressField, amountField], + ); + + expect(decoded).toEqual({ + recipient: EthAddress.fromField(addressField), + amount: 100n, + }); + }); }); diff --git a/yarn-project/stdlib/src/abi/decoder.ts b/yarn-project/stdlib/src/abi/decoder.ts index a373a8ac05e1..fb027c407db6 100644 --- a/yarn-project/stdlib/src/abi/decoder.ts +++ b/yarn-project/stdlib/src/abi/decoder.ts @@ -1,8 +1,17 @@ import { Fr } from '@aztec/foundation/curves/bn254'; +import { EthAddress } from '@aztec/foundation/eth-address'; import { AztecAddress } from '../aztec-address/index.js'; -import type { ABIParameter, ABIVariable, AbiType } from './abi.js'; -import { isAztecAddressStruct, isOptionStruct, parseSignedInt } from './utils.js'; +import type { AbiType } from './abi.js'; +import { FunctionSelector } from './function_selector.js'; +import { + isAztecAddressStruct, + isEthAddressStruct, + isFunctionSelectorStruct, + isOptionStruct, + isWrappedFieldStruct, + parseSignedInt, +} from './utils.js'; /** * The type of our decoded ABI. @@ -12,6 +21,9 @@ export type AbiDecoded = | boolean | string | AztecAddress + | EthAddress + | FunctionSelector + | Fr | AbiDecoded[] | { [key: string]: AbiDecoded } | undefined; @@ -58,6 +70,15 @@ class AbiDecoder { if (isAztecAddressStruct(abiType)) { return new AztecAddress(this.getNextField().toBuffer()); } + if (isEthAddressStruct(abiType)) { + return EthAddress.fromField(this.getNextField()); + } + if (isFunctionSelectorStruct(abiType)) { + return FunctionSelector.fromField(this.getNextField()); + } + if (isWrappedFieldStruct(abiType)) { + return this.getNextField(); + } if (isOptionStruct(abiType)) { const isSome = this.decodeNext(abiType.fields[0].type); const value = this.decodeNext(abiType.fields[1].type); @@ -123,79 +144,3 @@ class AbiDecoder { export function decodeFromAbi(typ: AbiType[], buffer: Fr[]) { return new AbiDecoder(typ, buffer.slice()).decode(); } - -/** - * Decodes the signature of a function from the name and parameters. - */ -export class FunctionSignatureDecoder { - private separator: string; - constructor( - private name: string, - private parameters: ABIParameter[], - private includeNames = false, - ) { - this.separator = includeNames ? ', ' : ','; - } - - /** - * Decodes a single function parameter type for the function signature. - * @param param - The parameter type to decode. - * @returns A string representing the parameter type. - */ - private getParameterType(param: AbiType): string { - switch (param.kind) { - case 'field': - return 'Field'; - case 'integer': - return param.sign === 'signed' ? `i${param.width}` : `u${param.width}`; - case 'boolean': - return 'bool'; - case 'array': - return `[${this.getParameterType(param.type)};${param.length}]`; - case 'string': - return `str<${param.length}>`; - case 'struct': - return `(${param.fields.map(field => `${this.decodeParameter(field)}`).join(this.separator)})`; - default: - throw new Error(`Unsupported type: ${param.kind}`); - } - } - - /** - * Decodes a single function parameter for the function signature. - * @param param - The parameter to decode. - * @returns A string representing the parameter type and optionally its name. - */ - private decodeParameter(param: ABIVariable): string { - const type = this.getParameterType(param.type); - return this.includeNames ? `${param.name}: ${type}` : type; - } - - /** - * Decodes all the parameters and build the function signature - * @returns The function signature. - */ - public decode(): string { - return `${this.name}(${this.parameters.map(param => this.decodeParameter(param)).join(this.separator)})`; - } -} - -/** - * Decodes a function signature from the name and parameters. - * @param name - The name of the function. - * @param parameters - The parameters of the function. - * @returns - The function signature. - */ -export function decodeFunctionSignature(name: string, parameters: ABIParameter[]) { - return new FunctionSignatureDecoder(name, parameters).decode(); -} - -/** - * Decodes a function signature from the name and parameters including parameter names. - * @param name - The name of the function. - * @param parameters - The parameters of the function. - * @returns - The user-friendly function signature. - */ -export function decodeFunctionSignatureWithParameterNames(name: string, parameters: ABIParameter[]) { - return new FunctionSignatureDecoder(name, parameters, true).decode(); -} diff --git a/yarn-project/stdlib/src/abi/function_selector.ts b/yarn-project/stdlib/src/abi/function_selector.ts index 860e11cf4713..3110681ea155 100644 --- a/yarn-project/stdlib/src/abi/function_selector.ts +++ b/yarn-project/stdlib/src/abi/function_selector.ts @@ -6,7 +6,7 @@ import { type ZodFor, hexSchemaFor } from '@aztec/foundation/schemas'; import { BufferReader, FieldReader, TypeRegistry } from '@aztec/foundation/serialize'; import type { ABIParameter } from './abi.js'; -import { decodeFunctionSignature } from './decoder.js'; +import { decodeFunctionSignature } from './function_signature_decoder.js'; import { Selector } from './selector.js'; /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ diff --git a/yarn-project/stdlib/src/abi/function_signature_decoder.ts b/yarn-project/stdlib/src/abi/function_signature_decoder.ts new file mode 100644 index 000000000000..0a0358312d5e --- /dev/null +++ b/yarn-project/stdlib/src/abi/function_signature_decoder.ts @@ -0,0 +1,77 @@ +import type { ABIParameter, ABIVariable, AbiType } from './abi.js'; + +/** + * Decodes the signature of a function from the name and parameters. + */ +export class FunctionSignatureDecoder { + private separator: string; + constructor( + private name: string, + private parameters: ABIParameter[], + private includeNames = false, + ) { + this.separator = includeNames ? ', ' : ','; + } + + /** + * Decodes a single function parameter type for the function signature. + * @param param - The parameter type to decode. + * @returns A string representing the parameter type. + */ + private getParameterType(param: AbiType): string { + switch (param.kind) { + case 'field': + return 'Field'; + case 'integer': + return param.sign === 'signed' ? `i${param.width}` : `u${param.width}`; + case 'boolean': + return 'bool'; + case 'array': + return `[${this.getParameterType(param.type)};${param.length}]`; + case 'string': + return `str<${param.length}>`; + case 'struct': + return `(${param.fields.map(field => `${this.decodeParameter(field)}`).join(this.separator)})`; + default: + throw new Error(`Unsupported type: ${param.kind}`); + } + } + + /** + * Decodes a single function parameter for the function signature. + * @param param - The parameter to decode. + * @returns A string representing the parameter type and optionally its name. + */ + private decodeParameter(param: ABIVariable): string { + const type = this.getParameterType(param.type); + return this.includeNames ? `${param.name}: ${type}` : type; + } + + /** + * Decodes all the parameters and build the function signature + * @returns The function signature. + */ + public decode(): string { + return `${this.name}(${this.parameters.map(param => this.decodeParameter(param)).join(this.separator)})`; + } +} + +/** + * Decodes a function signature from the name and parameters. + * @param name - The name of the function. + * @param parameters - The parameters of the function. + * @returns - The function signature. + */ +export function decodeFunctionSignature(name: string, parameters: ABIParameter[]) { + return new FunctionSignatureDecoder(name, parameters).decode(); +} + +/** + * Decodes a function signature from the name and parameters including parameter names. + * @param name - The name of the function. + * @param parameters - The parameters of the function. + * @returns - The user-friendly function signature. + */ +export function decodeFunctionSignatureWithParameterNames(name: string, parameters: ABIParameter[]) { + return new FunctionSignatureDecoder(name, parameters, true).decode(); +} diff --git a/yarn-project/stdlib/src/abi/index.ts b/yarn-project/stdlib/src/abi/index.ts index bdfd1ea92dd7..54f68f25efb7 100644 --- a/yarn-project/stdlib/src/abi/index.ts +++ b/yarn-project/stdlib/src/abi/index.ts @@ -1,6 +1,7 @@ export * from './abi.js'; export * from './buffer.js'; export * from './decoder.js'; +export * from './function_signature_decoder.js'; export * from './encoder.js'; export * from './authorization_selector.js'; export * from './event_metadata_definition.js'; diff --git a/yarn-project/stdlib/src/hash/hash.ts b/yarn-project/stdlib/src/hash/hash.ts index afc80031b1a4..5b5b882df57c 100644 --- a/yarn-project/stdlib/src/hash/hash.ts +++ b/yarn-project/stdlib/src/hash/hash.ts @@ -98,6 +98,11 @@ export function computeProtocolNullifier(txRequestHash: Fr): Promise { return siloNullifier(AztecAddress.fromBigInt(NULL_MSG_SENDER_CONTRACT_ADDRESS), txRequestHash); } +/** Domain-separates a raw log tag with the given domain separator. */ +export function computeLogTag(rawTag: number | bigint | boolean | Fr | Buffer, domSep: DomainSeparator): Promise { + return poseidon2HashWithSeparator([new Fr(rawTag)], domSep); +} + export function computeSiloedPrivateLogFirstField(contract: AztecAddress, field: Fr): Promise { return poseidon2HashWithSeparator([contract, field], DomainSeparator.PRIVATE_LOG_FIRST_FIELD); } diff --git a/yarn-project/stdlib/src/logs/message_context.test.ts b/yarn-project/stdlib/src/logs/message_context.test.ts index 2193985f2259..474342f6129b 100644 --- a/yarn-project/stdlib/src/logs/message_context.test.ts +++ b/yarn-project/stdlib/src/logs/message_context.test.ts @@ -1,7 +1,6 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { updateInlineTestData } from '@aztec/foundation/testing/files'; -import { AztecAddress } from '../aztec-address/index.js'; import { TxHash } from '../tx/tx_hash.js'; import { MessageContext } from './message_context.js'; @@ -11,10 +10,9 @@ describe('MessageContext', () => { const txHash = new TxHash(new Fr(123n)); const uniqueNoteHashes = [new Fr(4n), new Fr(5n)]; const firstNullifier = new Fr(6n); - const recipient = AztecAddress.fromField(new Fr(789n)); // Create a MessageContext instance - const messageContext = new MessageContext(txHash, uniqueNoteHashes, firstNullifier, recipient); + const messageContext = new MessageContext(txHash, uniqueNoteHashes, firstNullifier); // Serialize the message context const serialized = messageContext.toFields(); @@ -89,17 +87,41 @@ describe('MessageContext', () => { "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000315", ] `); - // Optionally update Noir test data - const fieldArrayStr = `[${serialized.map(f => f.toString()).join(',')}]`; // Run with AZTEC_GENERATE_TEST_DATA=1 to update noir test data + const fieldArrayStr = `[${serialized.map(f => f.toString()).join(',')}]`; updateInlineTestData( 'noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr', 'serialized_message_context_from_typescript', fieldArrayStr, ); }); + + it('serialization of some option matches snapshot', () => { + const txHash = new TxHash(new Fr(123)); + const uniqueNoteHashes = [new Fr(4n), new Fr(5n)]; + const firstNullifier = new Fr(6n); + const ctx = new MessageContext(txHash, uniqueNoteHashes, firstNullifier); + const serialized = MessageContext.toSerializedOption(ctx); + // is_some flag + fields + expect(serialized[0]).toEqual(new Fr(1)); + expect(serialized.length).toEqual(1 + ctx.toFields().length); + }); + + it('serialization of none option matches snapshot', () => { + const serialized = MessageContext.toSerializedOption(null); + expect(serialized[0]).toEqual(new Fr(0)); + // All fields should be zero + for (const f of serialized) { + expect(f).toEqual(Fr.zero()); + } + }); + + it('serialization length of empty matches some', () => { + const txHash = new TxHash(new Fr(123)); + const ctx = new MessageContext(txHash, [new Fr(4n), new Fr(5n)], new Fr(6n)); + expect(ctx.toFields().length).toEqual(MessageContext.toEmptyFields().length); + }); }); diff --git a/yarn-project/stdlib/src/logs/message_context.ts b/yarn-project/stdlib/src/logs/message_context.ts index e4e0c2286090..99570a49613f 100644 --- a/yarn-project/stdlib/src/logs/message_context.ts +++ b/yarn-project/stdlib/src/logs/message_context.ts @@ -1,8 +1,7 @@ import { MAX_NOTE_HASHES_PER_TX } from '@aztec/constants'; +import { range } from '@aztec/foundation/array'; import { Fr } from '@aztec/foundation/curves/bn254'; -import type { AztecAddress } from '../aztec-address/index.js'; -import type { TxEffect } from '../tx/tx_effect.js'; import type { TxHash } from '../tx/tx_hash.js'; /** @@ -19,7 +18,6 @@ export class MessageContext { public txHash: TxHash, public uniqueNoteHashesInTx: Fr[], public firstNullifierInTx: Fr, - public recipient: AztecAddress, ) {} toFields(): Fr[] { @@ -27,7 +25,6 @@ export class MessageContext { this.txHash.hash, ...serializeBoundedVec(this.uniqueNoteHashesInTx, MAX_NOTE_HASHES_PER_TX), this.firstNullifierInTx, - this.recipient.toField(), ]; } @@ -37,13 +34,22 @@ export class MessageContext { tx_hash: this.txHash.hash, unique_note_hashes_in_tx: this.uniqueNoteHashesInTx, first_nullifier_in_tx: this.firstNullifierInTx, - recipient: this.recipient, }; /* eslint-enable camelcase */ } - static fromTxEffectAndRecipient(txEffect: TxEffect, recipient: AztecAddress): MessageContext { - return new MessageContext(txEffect.txHash, txEffect.noteHashes, txEffect.nullifiers[0], recipient); + static toEmptyFields(): Fr[] { + const serializationLen = + 1 /* txHash */ + MAX_NOTE_HASHES_PER_TX + 1 /* uniqueNoteHashesInTx BVec */ + 1; /* firstNullifierInTx */ + return range(serializationLen).map(_ => Fr.zero()); + } + + static toSerializedOption(response: MessageContext | null): Fr[] { + if (response) { + return [new Fr(1), ...response.toFields()]; + } else { + return [new Fr(0), ...MessageContext.toEmptyFields()]; + } } } @@ -55,6 +61,10 @@ export class MessageContext { * @dev Copied over from pending_tagged_log.ts. */ function serializeBoundedVec(values: Fr[], maxLength: number): Fr[] { + if (values.length > maxLength) { + throw new Error(`Attempted to serialize ${values} values into a BoundedVec with max length ${maxLength}`); + } + const lengthDiff = maxLength - values.length; const zeroPaddingArray = Array(lengthDiff).fill(Fr.ZERO); const storage = values.concat(zeroPaddingArray); diff --git a/yarn-project/stdlib/src/logs/pending_tagged_log.test.ts b/yarn-project/stdlib/src/logs/pending_tagged_log.test.ts index 30b023c9b0c4..b327fc5237fa 100644 --- a/yarn-project/stdlib/src/logs/pending_tagged_log.test.ts +++ b/yarn-project/stdlib/src/logs/pending_tagged_log.test.ts @@ -1,7 +1,6 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { updateInlineTestData } from '@aztec/foundation/testing/files'; -import { AztecAddress } from '../aztec-address/index.js'; import { TxHash } from '../tx/tx_hash.js'; import { PendingTaggedLog } from './pending_tagged_log.js'; @@ -11,9 +10,8 @@ describe('PendingTaggedLog', () => { const txHash = new TxHash(new Fr(123n)); const uniqueNoteHashes = [new Fr(4n), new Fr(5n)]; const firstNullifier = new Fr(6n); - const recipient = AztecAddress.fromField(new Fr(789n)); - const pendingLog = new PendingTaggedLog(log, txHash, uniqueNoteHashes, firstNullifier, recipient); + const pendingLog = new PendingTaggedLog(log, txHash, uniqueNoteHashes, firstNullifier); const serialized = pendingLog.toFields(); // Test against snapshot @@ -103,7 +101,6 @@ describe('PendingTaggedLog', () => { "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000315", ] `); diff --git a/yarn-project/stdlib/src/logs/pending_tagged_log.ts b/yarn-project/stdlib/src/logs/pending_tagged_log.ts index 1921736e0aa4..70b0f85ecac1 100644 --- a/yarn-project/stdlib/src/logs/pending_tagged_log.ts +++ b/yarn-project/stdlib/src/logs/pending_tagged_log.ts @@ -1,7 +1,6 @@ import { PRIVATE_LOG_SIZE_IN_FIELDS } from '@aztec/constants'; import { Fr } from '@aztec/foundation/curves/bn254'; -import type { AztecAddress } from '../aztec-address/index.js'; import type { TxHash } from '../tx/tx_hash.js'; import { MessageContext } from './message_context.js'; @@ -17,9 +16,8 @@ export class PendingTaggedLog { txHash: TxHash, uniqueNoteHashesInTx: Fr[], firstNullifierInTx: Fr, - recipient: AztecAddress, ) { - this.context = new MessageContext(txHash, uniqueNoteHashesInTx, firstNullifierInTx, recipient); + this.context = new MessageContext(txHash, uniqueNoteHashesInTx, firstNullifierInTx); } toFields(): Fr[] { diff --git a/yarn-project/stdlib/src/logs/shared_secret_derivation.ts b/yarn-project/stdlib/src/logs/shared_secret_derivation.ts index 0ffcdd2b88ff..ff4e3bb8b1f2 100644 --- a/yarn-project/stdlib/src/logs/shared_secret_derivation.ts +++ b/yarn-project/stdlib/src/logs/shared_secret_derivation.ts @@ -1,26 +1,37 @@ +import { DomainSeparator } from '@aztec/constants'; import { Grumpkin } from '@aztec/foundation/crypto/grumpkin'; -import type { GrumpkinScalar, Point } from '@aztec/foundation/curves/grumpkin'; +import { poseidon2HashWithSeparator } from '@aztec/foundation/crypto/poseidon'; +import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; +import type { AztecAddress } from '../aztec-address/index.js'; import type { PublicKey } from '../keys/public_key.js'; /** - * Derive an Elliptic Curve Diffie-Hellman (ECDH) Shared Secret. - * The function takes in an ECDH public key, a private key, and a Grumpkin instance to compute - * the shared secret. + * Derives an app-siloed ECDH shared secret. + * + * Computes the raw ECDH shared secret `S = secretKey * publicKey`, then app-silos it: + * `s_app = h(DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, S.x, S.y, contractAddress)` * * @param secretKey - The secret key used to derive shared secret. * @param publicKey - The public key used to derive shared secret. - * @returns A derived shared secret. + * @param contractAddress - The address of the calling contract, used for app-siloing. + * @returns The app-siloed shared secret as a Field. * @throws If the publicKey is zero. - * - * TODO(#12656): This function is kept around because of the utilityGetSharedSecret oracle. Nuke this once returning - * the app-siloed secret. */ -export function deriveEcdhSharedSecret(secretKey: GrumpkinScalar, publicKey: PublicKey): Promise { +export async function deriveAppSiloedSharedSecret( + secretKey: GrumpkinScalar, + publicKey: PublicKey, + contractAddress: AztecAddress, +): Promise { if (publicKey.isZero()) { throw new Error( `Attempting to derive a shared secret with a zero public key. You have probably passed a zero public key in your Noir code somewhere thinking that the note won't be broadcast... but it was.`, ); } - return Grumpkin.mul(publicKey, secretKey); + const rawSharedSecret = await Grumpkin.mul(publicKey, secretKey); + return poseidon2HashWithSeparator( + [rawSharedSecret.x, rawSharedSecret.y, contractAddress], + DomainSeparator.APP_SILOED_ECDH_SHARED_SECRET, + ); } diff --git a/yarn-project/stdlib/src/logs/siloed_tag.ts b/yarn-project/stdlib/src/logs/siloed_tag.ts index 0710d3e91fe7..a51b6c643989 100644 --- a/yarn-project/stdlib/src/logs/siloed_tag.ts +++ b/yarn-project/stdlib/src/logs/siloed_tag.ts @@ -1,8 +1,9 @@ +import { DomainSeparator } from '@aztec/constants'; import type { Fr } from '@aztec/foundation/curves/bn254'; import type { ZodFor } from '@aztec/foundation/schemas'; import type { AztecAddress } from '../aztec-address/index.js'; -import { computeSiloedPrivateLogFirstField } from '../hash/hash.js'; +import { computeLogTag, computeSiloedPrivateLogFirstField } from '../hash/hash.js'; import { schemas } from '../schemas/schemas.js'; import type { PreTag } from './pre_tag.js'; import { Tag } from './tag.js'; @@ -24,9 +25,13 @@ export class SiloedTag { static async compute(preTag: PreTag): Promise { const tag = await Tag.compute(preTag); - return SiloedTag.computeFromTagAndApp(tag, preTag.extendedSecret.app); + const logTag = await computeLogTag(tag.value, DomainSeparator.UNCONSTRAINED_MSG_LOG_TAG); + return SiloedTag.computeFromTagAndApp(new Tag(logTag), preTag.extendedSecret.app); } + /** + * Unlike `compute`, this expects a tag whose value is already domain-separated. + */ static async computeFromTagAndApp(tag: Tag, app: AztecAddress): Promise { const siloedTag = await computeSiloedPrivateLogFirstField(app, tag.value); return new SiloedTag(siloedTag); diff --git a/yarn-project/stdlib/src/messaging/l1_to_l2_message.ts b/yarn-project/stdlib/src/messaging/l1_to_l2_message.ts index a7e7392e4084..67590af73d66 100644 --- a/yarn-project/stdlib/src/messaging/l1_to_l2_message.ts +++ b/yarn-project/stdlib/src/messaging/l1_to_l2_message.ts @@ -6,6 +6,7 @@ import { bufferToHex } from '@aztec/foundation/string'; import { SiblingPath } from '@aztec/foundation/trees'; import type { AztecAddress } from '../aztec-address/index.js'; +import type { BlockParameter } from '../block/block_parameter.js'; import { computeL1ToL2MessageNullifier } from '../hash/hash.js'; import type { AztecNode } from '../interfaces/aztec-node.js'; import { MerkleTreeId } from '../trees/merkle_tree_id.js'; @@ -79,20 +80,22 @@ export async function getNonNullifiedL1ToL2MessageWitness( contractAddress: AztecAddress, messageHash: Fr, secret: Fr, + referenceBlock: BlockParameter = 'latest', ): Promise<[bigint, SiblingPath]> { - const response = await node.getL1ToL2MessageMembershipWitness('latest', messageHash); - if (!response) { - throw new Error(`No L1 to L2 message found for message hash ${messageHash.toString()}`); - } + const messageNullifier = await computeL1ToL2MessageNullifier(contractAddress, messageHash, secret); - const [messageIndex, siblingPath] = response; + const [l1ToL2Response, nullifierResponse] = await Promise.all([ + node.getL1ToL2MessageMembershipWitness(referenceBlock, messageHash), + node.findLeavesIndexes(referenceBlock, MerkleTreeId.NULLIFIER_TREE, [messageNullifier]), + ]); - const messageNullifier = await computeL1ToL2MessageNullifier(contractAddress, messageHash, secret); + if (!l1ToL2Response) { + throw new Error(`No L1 to L2 message found for message hash ${messageHash.toString()}`); + } - const [nullifierIndex] = await node.findLeavesIndexes('latest', MerkleTreeId.NULLIFIER_TREE, [messageNullifier]); - if (nullifierIndex !== undefined) { + if (nullifierResponse[0] !== undefined) { throw new Error(`No non-nullified L1 to L2 message found for message hash ${messageHash.toString()}`); } - return [messageIndex, siblingPath]; + return l1ToL2Response; } diff --git a/yarn-project/stdlib/src/proofs/chonk_proof.test.ts b/yarn-project/stdlib/src/proofs/chonk_proof.test.ts index 29de21616733..cd8767a98a15 100644 --- a/yarn-project/stdlib/src/proofs/chonk_proof.test.ts +++ b/yarn-project/stdlib/src/proofs/chonk_proof.test.ts @@ -1,5 +1,6 @@ import { CHONK_PROOF_LENGTH } from '@aztec/constants'; import { Fr } from '@aztec/foundation/curves/bn254'; +import { numToUInt32BE } from '@aztec/foundation/serialize'; import { ChonkProof, ChonkProofWithPublicInputs } from './chonk_proof.js'; @@ -42,6 +43,49 @@ describe('ChonkProof', () => { expect(withPublicInputs.fieldsWithPublicInputs[0]).toEqual(publicInput); expect(withPublicInputs.fieldsWithPublicInputs.slice(1)).toEqual(proof.fields); }); + + describe('compressed serialization format', () => { + const fakeCompressedBytes = Buffer.from([0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03]); + + it('serializes in compressed format when compressedProof is set', () => { + const proof = ChonkProof.random(); + proof.compressedProof = fakeCompressedBytes; + + const buf = proof.toBuffer(); + // First uint32 should be the compressed byte count (not CHONK_PROOF_LENGTH) + expect(buf.readUInt32BE(0)).toBe(fakeCompressedBytes.length); + // Then the compressed bytes themselves + expect(buf.subarray(4)).toEqual(fakeCompressedBytes); + }); + + it('serializes in uncompressed format when compressedProof is undefined', () => { + const proof = ChonkProof.random(); + expect(proof.compressedProof).toBeUndefined(); + + const buf = proof.toBuffer(); + expect(buf.readUInt32BE(0)).toBe(CHONK_PROOF_LENGTH); + }); + + it('stripping compressedProof switches to uncompressed format', () => { + const proof = ChonkProof.random(); + proof.compressedProof = fakeCompressedBytes; + + proof.compressedProof = undefined; + const buf = proof.toBuffer(); + expect(buf.readUInt32BE(0)).toBe(CHONK_PROOF_LENGTH); + }); + + it('detects compressed format by size (first uint32 != CHONK_PROOF_LENGTH)', () => { + // Construct a buffer with compressed format: [byte_count: uint32] [compressed_bytes] + const compressedPayload = Buffer.from([0x01, 0x02, 0x03]); + const buf = Buffer.concat([numToUInt32BE(compressedPayload.length), compressedPayload]); + + // fromBuffer should detect this as compressed format (first uint32 != 1632) + // and attempt decompression. Since we're using fake bytes, BarretenbergSync + // will throw, but the format detection itself works. + expect(() => ChonkProof.fromBuffer(buf)).toThrow(); + }); + }); }); describe('ChonkProofWithPublicInputs', () => { diff --git a/yarn-project/stdlib/src/proofs/chonk_proof.ts b/yarn-project/stdlib/src/proofs/chonk_proof.ts index bb22a6187fa0..0669cd276a09 100644 --- a/yarn-project/stdlib/src/proofs/chonk_proof.ts +++ b/yarn-project/stdlib/src/proofs/chonk_proof.ts @@ -1,21 +1,41 @@ +import { BarretenbergSync, flattenChonkProofFields } from '@aztec/bb.js'; import { CHONK_PROOF_LENGTH } from '@aztec/constants'; import { times } from '@aztec/foundation/collection'; import { randomBytes } from '@aztec/foundation/crypto/random'; import { Fr } from '@aztec/foundation/curves/bn254'; import { bufferSchemaFor } from '@aztec/foundation/schemas'; -import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; +import { BufferReader, numToUInt32BE, serializeToBuffer } from '@aztec/foundation/serialize'; + +/** + * Serialization format detection for ChonkProof is size-based: + * - UNCOMPRESSED (legacy): [field_count=1632: uint32] [fields...] → total ≈ 52KB (>= 40KB) + * - COMPRESSED: [byte_count: uint32] [compressed_bytes] → total ≈ 35KB (< 40KB) + * + * Detection: if the first uint32 equals CHONK_PROOF_LENGTH (1632), it's legacy format + * (field count). Otherwise, it's compressed format (byte count). The old uncompressed + * format is never smaller than 40KB; compressed proofs are always smaller than 40KB. + */ // CHONK: "Client Honk" - An UltraHonk variant with incremental folding and delayed non-native arithmetic. export class ChonkProof { + /** + * Optional compressed proof bytes from chonk compression (point compression + u256 encoding). + * When set, toBuffer() will serialize in compressed format (~1.7x smaller). + * When reading from compressed format, this is populated and fields are decompressed on demand. + */ + public compressedProof?: Buffer; + constructor( // The proof fields. // For native verification, attach public inputs via `attachPublicInputs(publicInputs)`. // Not using Tuple here due to the length being too high. public fields: Fr[], + compressedProof?: Buffer, ) { if (fields.length !== CHONK_PROOF_LENGTH) { throw new Error(`Invalid ChonkProof length: ${fields.length}`); } + this.compressedProof = compressedProof; } public attachPublicInputs(publicInputs: Fr[]) { @@ -53,19 +73,84 @@ export class ChonkProof { return this.toBuffer(); } + /** + * Deserialize a ChonkProof from a buffer. + * Supports both legacy (field elements) and compressed (chonk compression) formats. + * + * Size-based format detection: + * - First uint32 == CHONK_PROOF_LENGTH (1632): legacy format, read field elements + * Total proof data ≈ 52KB (always >= 40KB) + * - Otherwise: compressed format, first uint32 is byte count of compressed data + * Total proof data ≈ 35KB (always < 40KB) + */ static fromBuffer(buffer: Buffer | BufferReader): ChonkProof { const reader = BufferReader.asReader(buffer); - const proofLength = reader.readNumber(); - const proof = reader.readArray(proofLength, Fr); - return new ChonkProof(proof); + const firstUint32 = reader.readNumber(); + + if (firstUint32 === CHONK_PROOF_LENGTH) { + // Legacy format: firstUint32 is the field count (1632) + // Widen to `number` to prevent TS from narrowing to literal 1632, + // which would cause Tuple to exceed the recursion limit. + const fieldCount: number = firstUint32; + const proof = reader.readArray(fieldCount, Fr); + return new ChonkProof(proof); + } + + // Compressed format: firstUint32 is the compressed byte count + const compressedBytes = reader.readBytes(firstUint32); + return ChonkProof.fromCompressedBytes(Buffer.from(compressedBytes)); + } + + /** + * Create a ChonkProof from compressed bytes by decompressing via the BarretenbergSync API. + * The compressed format uses point compression and u256 encoding (from PR #20645). + * + * @param compressed - Compressed proof bytes from chonk compression + * @returns ChonkProof with both fields and compressed bytes populated + */ + static fromCompressedBytes(compressed: Buffer): ChonkProof { + const api = BarretenbergSync.getSingleton(); + const result = api.chonkDecompressProof({ compressedProof: new Uint8Array(compressed) }); + + // Flatten the structured bb.js ChonkProof into flat Fr[] field elements + const flatFields = flattenChonkProofFields(result.proof); + const fields = flatFields.map(f => Fr.fromBuffer(Buffer.from(f))); + + // The decompressed proof includes public inputs in hidingOinkProof. + // Since ChonkProof stores fields WITHOUT public inputs, strip them. + // The number of public inputs = total fields - CHONK_PROOF_LENGTH + if (fields.length > CHONK_PROOF_LENGTH) { + const numPubInputs = fields.length - CHONK_PROOF_LENGTH; + const proofFields = fields.slice(numPubInputs); + return new ChonkProof(proofFields, compressed); + } + + return new ChonkProof(fields, compressed); } + /** + * Serialize the proof to a buffer. + * If compressed bytes are available, uses the compressed format (~1.7x smaller). + * Otherwise falls back to legacy field element format. + */ public toBuffer() { + if (this.compressedProof) { + // Compressed format: [compressed_byte_count: uint32] [compressed_bytes] + return Buffer.concat([numToUInt32BE(this.compressedProof.length), this.compressedProof]); + } + // Legacy format: [field_count=1632: uint32] [fields...] return serializeToBuffer(this.fields.length, this.fields); } } export class ChonkProofWithPublicInputs { + /** + * Optional compressed proof bytes (covers the full proof WITH public inputs). + * Set by the prover when using chonk compression. Flows through to ChonkProof + * via removePublicInputs() so the Tx can serialize in compressed format. + */ + public compressedProof?: Buffer; + constructor( // The proof fields with public inputs. // For recursive verification, use without public inputs via `removePublicInputs()`. @@ -83,7 +168,8 @@ export class ChonkProofWithPublicInputs { public removePublicInputs() { const numPublicInputs = this.fieldsWithPublicInputs.length - CHONK_PROOF_LENGTH; - return new ChonkProof(this.fieldsWithPublicInputs.slice(numPublicInputs)); + // Flow compressed proof bytes through so the ChonkProof can serialize efficiently + return new ChonkProof(this.fieldsWithPublicInputs.slice(numPublicInputs), this.compressedProof); } public isEmpty() { diff --git a/yarn-project/stdlib/src/tx/capsule.ts b/yarn-project/stdlib/src/tx/capsule.ts index 8efeee24983f..eaff72c8c56a 100644 --- a/yarn-project/stdlib/src/tx/capsule.ts +++ b/yarn-project/stdlib/src/tx/capsule.ts @@ -19,6 +19,8 @@ export class Capsule { public readonly storageSlot: Fr, /** Data passed to the contract */ public readonly data: Fr[], + /** Optional namespace for the capsule contents */ + public readonly scope?: AztecAddress, ) {} static get schema() { @@ -30,12 +32,18 @@ export class Capsule { } toBuffer() { - return serializeToBuffer(this.contractAddress, this.storageSlot, new Vector(this.data)); + return this.scope + ? serializeToBuffer(this.contractAddress, this.storageSlot, new Vector(this.data), true, this.scope) + : serializeToBuffer(this.contractAddress, this.storageSlot, new Vector(this.data), false); } static fromBuffer(buffer: Buffer | BufferReader): Capsule { const reader = BufferReader.asReader(buffer); - return new Capsule(AztecAddress.fromBuffer(reader), Fr.fromBuffer(reader), reader.readVector(Fr)); + const contractAddress = AztecAddress.fromBuffer(reader); + const storageSlot = Fr.fromBuffer(reader); + const data = reader.readVector(Fr); + const hasScope = reader.readBoolean(); + return new Capsule(contractAddress, storageSlot, data, hasScope ? AztecAddress.fromBuffer(reader) : undefined); } toString() { diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index 9b062a46aafd..f8c7c1bd74a0 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -120,8 +120,12 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl assertCompatibleOracleVersion(version: number): void { if (version !== ORACLE_VERSION) { + const hint = + version > ORACLE_VERSION + ? 'The contract was compiled with a newer version of Aztec.nr than this aztec cli version supports. Upgrade your aztec cli version to a compatible version.' + : 'The contract was compiled with an older version of Aztec.nr than this aztec cli version supports. Recompile the contract with a compatible version of Aztec.nr.'; throw new Error( - `Incompatible oracle version. TXE is using version '${ORACLE_VERSION}', but got a request for '${version}'.`, + `Incompatible aztec cli version: ${hint} See https://docs.aztec.network/errors/8 (expected oracle version ${ORACLE_VERSION}, got ${version})`, ); } } diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index f7bfb4b67c1b..f6701d3b58b5 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -339,7 +339,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - aztec_prv_storeInExecutionCache(foreignValues: ForeignCallArray, foreignHash: ForeignCallSingle) { + aztec_prv_setHashPreimage(foreignValues: ForeignCallArray, foreignHash: ForeignCallSingle) { const values = fromArray(foreignValues); const hash = fromSingle(foreignHash); @@ -349,7 +349,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_prv_loadFromExecutionCache(foreignHash: ForeignCallSingle) { + async aztec_prv_getHashPreimage(foreignHash: ForeignCallSingle) { const hash = fromSingle(foreignHash); const returns = await this.handlerAsPrivate().loadFromExecutionCache(hash); @@ -378,7 +378,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_storageRead( + async aztec_utl_getFromPublicStorage( foreignBlockHash: ForeignCallSingle, foreignContractAddress: ForeignCallSingle, foreignStartStorageSlot: ForeignCallSingle, @@ -556,7 +556,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_checkNullifierExists(foreignInnerNullifier: ForeignCallSingle) { + async aztec_utl_doesNullifierExist(foreignInnerNullifier: ForeignCallSingle) { const innerNullifier = fromSingle(foreignInnerNullifier); const exists = await this.handlerAsUtility().checkNullifierExists(innerNullifier); @@ -582,7 +582,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_tryGetPublicKeysAndPartialAddress(foreignAddress: ForeignCallSingle) { + async aztec_utl_getPublicKeysAndPartialAddress(foreignAddress: ForeignCallSingle) { const address = addressFromSingle(foreignAddress); const result = await this.handlerAsUtility().tryGetPublicKeysAndPartialAddress(address); @@ -652,7 +652,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - public aztec_prv_validatePublicCalldata(_foreignCalldataHash: ForeignCallSingle) { + public aztec_prv_assertValidPublicCalldata(_foreignCalldataHash: ForeignCallSingle) { throw new Error('Enqueueing public calls is not supported in TestEnvironment::private_context'); } @@ -662,7 +662,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - public async aztec_prv_inRevertiblePhase(foreignSideEffectCounter: ForeignCallSingle) { + public async aztec_prv_isExecutionInRevertiblePhase(foreignSideEffectCounter: ForeignCallSingle) { const sideEffectCounter = fromSingle(foreignSideEffectCounter).toNumber(); const isRevertible = await this.handlerAsPrivate().inRevertiblePhase(sideEffectCounter); return toForeignCallResult([toSingle(new Fr(isRevertible))]); @@ -738,10 +738,14 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_fetchTaggedLogs(foreignPendingTaggedLogArrayBaseSlot: ForeignCallSingle) { + async aztec_utl_getPendingTaggedLogs( + foreignPendingTaggedLogArrayBaseSlot: ForeignCallSingle, + foreignScope: ForeignCallSingle, + ) { const pendingTaggedLogArrayBaseSlot = fromSingle(foreignPendingTaggedLogArrayBaseSlot); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); - await this.handlerAsUtility().fetchTaggedLogs(pendingTaggedLogArrayBaseSlot); + await this.handlerAsUtility().fetchTaggedLogs(pendingTaggedLogArrayBaseSlot, scope); return toForeignCallResult([]); } @@ -753,12 +757,14 @@ export class RPCTranslator { foreignEventValidationRequestsArrayBaseSlot: ForeignCallSingle, foreignMaxNotePackedLen: ForeignCallSingle, foreignMaxEventSerializedLen: ForeignCallSingle, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const noteValidationRequestsArrayBaseSlot = fromSingle(foreignNoteValidationRequestsArrayBaseSlot); const eventValidationRequestsArrayBaseSlot = fromSingle(foreignEventValidationRequestsArrayBaseSlot); const maxNotePackedLen = fromSingle(foreignMaxNotePackedLen).toNumber(); const maxEventSerializedLen = fromSingle(foreignMaxEventSerializedLen).toNumber(); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); await this.handlerAsUtility().validateAndStoreEnqueuedNotesAndEvents( contractAddress, @@ -766,75 +772,86 @@ export class RPCTranslator { eventValidationRequestsArrayBaseSlot, maxNotePackedLen, maxEventSerializedLen, + scope, ); return toForeignCallResult([]); } // eslint-disable-next-line camelcase - public async aztec_utl_bulkRetrieveLogs( + public async aztec_utl_getLogsByTag( foreignContractAddress: ForeignCallSingle, foreignLogRetrievalRequestsArrayBaseSlot: ForeignCallSingle, foreignLogRetrievalResponsesArrayBaseSlot: ForeignCallSingle, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const logRetrievalRequestsArrayBaseSlot = fromSingle(foreignLogRetrievalRequestsArrayBaseSlot); const logRetrievalResponsesArrayBaseSlot = fromSingle(foreignLogRetrievalResponsesArrayBaseSlot); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); await this.handlerAsUtility().bulkRetrieveLogs( contractAddress, logRetrievalRequestsArrayBaseSlot, logRetrievalResponsesArrayBaseSlot, + scope, ); return toForeignCallResult([]); } // eslint-disable-next-line camelcase - public async aztec_utl_utilityResolveMessageContexts( + public async aztec_utl_getMessageContextsByTxHash( foreignContractAddress: ForeignCallSingle, foreignMessageContextRequestsArrayBaseSlot: ForeignCallSingle, foreignMessageContextResponsesArrayBaseSlot: ForeignCallSingle, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const messageContextRequestsArrayBaseSlot = fromSingle(foreignMessageContextRequestsArrayBaseSlot); const messageContextResponsesArrayBaseSlot = fromSingle(foreignMessageContextResponsesArrayBaseSlot); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); await this.handlerAsUtility().utilityResolveMessageContexts( contractAddress, messageContextRequestsArrayBaseSlot, messageContextResponsesArrayBaseSlot, + scope, ); return toForeignCallResult([]); } // eslint-disable-next-line camelcase - async aztec_utl_storeCapsule( + aztec_utl_setCapsule( foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle, foreignCapsule: ForeignCallArray, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const slot = fromSingle(foreignSlot); const capsule = fromArray(foreignCapsule); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); - await this.handlerAsUtility().storeCapsule(contractAddress, slot, capsule); + this.handlerAsUtility().storeCapsule(contractAddress, slot, capsule, scope); return toForeignCallResult([]); } // eslint-disable-next-line camelcase - async aztec_utl_loadCapsule( + async aztec_utl_getCapsule( foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle, foreignTSize: ForeignCallSingle, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const slot = fromSingle(foreignSlot); const tSize = fromSingle(foreignTSize).toNumber(); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); - const values = await this.handlerAsUtility().loadCapsule(contractAddress, slot); + const values = await this.handlerAsUtility().loadCapsule(contractAddress, slot, scope); // We are going to return a Noir Option struct to represent the possibility of null values. Options are a struct // with two fields: `some` (a boolean) and `value` (a field array in this case). @@ -848,11 +865,16 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_deleteCapsule(foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle) { + aztec_utl_deleteCapsule( + foreignContractAddress: ForeignCallSingle, + foreignSlot: ForeignCallSingle, + foreignScope: ForeignCallSingle, + ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const slot = fromSingle(foreignSlot); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); - await this.handlerAsUtility().deleteCapsule(contractAddress, slot); + this.handlerAsUtility().deleteCapsule(contractAddress, slot, scope); return toForeignCallResult([]); } @@ -863,13 +885,15 @@ export class RPCTranslator { foreignSrcSlot: ForeignCallSingle, foreignDstSlot: ForeignCallSingle, foreignNumEntries: ForeignCallSingle, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const srcSlot = fromSingle(foreignSrcSlot); const dstSlot = fromSingle(foreignDstSlot); const numEntries = fromSingle(foreignNumEntries).toNumber(); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); - await this.handlerAsUtility().copyCapsule(contractAddress, srcSlot, dstSlot, numEntries); + await this.handlerAsUtility().copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, scope); return toForeignCallResult([]); } @@ -879,7 +903,7 @@ export class RPCTranslator { // to implement this function here. Isn't there a way to programmatically identify that this is missing, given the // existence of a txe_oracle method? // eslint-disable-next-line camelcase - async aztec_utl_tryAes128Decrypt( + async aztec_utl_decryptAes128( foreignCiphertextBVecStorage: ForeignCallArray, foreignCiphertextLength: ForeignCallSingle, foreignIv: ForeignCallArray, @@ -909,6 +933,7 @@ export class RPCTranslator { foreignEphPKField0: ForeignCallSingle, foreignEphPKField1: ForeignCallSingle, foreignEphPKField2: ForeignCallSingle, + foreignContractAddress: ForeignCallSingle, ) { const address = AztecAddress.fromField(fromSingle(foreignAddress)); const ephPK = Point.fromFields([ @@ -916,14 +941,15 @@ export class RPCTranslator { fromSingle(foreignEphPKField1), fromSingle(foreignEphPKField2), ]); + const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); - const secret = await this.handlerAsUtility().getSharedSecret(address, ephPK); + const secret = await this.handlerAsUtility().getSharedSecret(address, ephPK, contractAddress); - return toForeignCallResult(secret.toFields().map(toSingle)); + return toForeignCallResult([toSingle(secret)]); } // eslint-disable-next-line camelcase - aztec_utl_invalidateContractSyncCache( + aztec_utl_setContractSyncCacheInvalid( foreignContractAddress: ForeignCallSingle, foreignScopes: ForeignCallArray, foreignScopeCount: ForeignCallSingle, diff --git a/yarn-project/txe/src/state_machine/index.ts b/yarn-project/txe/src/state_machine/index.ts index ca44362beee2..32ff8348b4f0 100644 --- a/yarn-project/txe/src/state_machine/index.ts +++ b/yarn-project/txe/src/state_machine/index.ts @@ -3,6 +3,7 @@ import { TestCircuitVerifier } from '@aztec/bb-prover/test'; import { CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; +import type { KeyStore } from '@aztec/key-store'; import { type AnchorBlockStore, type ContractStore, ContractSyncService, type NoteStore } from '@aztec/pxe/server'; import { MessageContextService } from '@aztec/pxe/simulator'; import { L2Block } from '@aztec/stdlib/block'; @@ -35,6 +36,7 @@ export class TXEStateMachine { anchorBlockStore: AnchorBlockStore, contractStore: ContractStore, noteStore: NoteStore, + keyStore: KeyStore, ) { const synchronizer = await TXESynchronizer.create(); const aztecNodeConfig = {} as AztecNodeConfig; @@ -68,6 +70,7 @@ export class TXEStateMachine { node, contractStore, noteStore, + () => keyStore.getAccounts(), createLogger('txe:contract_sync'), ); diff --git a/yarn-project/txe/src/txe_session.ts b/yarn-project/txe/src/txe_session.ts index 38e447b73153..71958ef8740b 100644 --- a/yarn-project/txe/src/txe_session.ts +++ b/yarn-project/txe/src/txe_session.ts @@ -179,7 +179,7 @@ export class TXESession implements TXESessionStateHandler { const archiver = new TXEArchiver(store); const anchorBlockStore = new AnchorBlockStore(store); - const stateMachine = await TXEStateMachine.create(archiver, anchorBlockStore, contractStore, noteStore); + const stateMachine = await TXEStateMachine.create(archiver, anchorBlockStore, contractStore, noteStore, keyStore); const nextBlockTimestamp = BigInt(Math.floor(new Date().getTime() / 1000)); const version = new Fr(await stateMachine.node.getVersion()); @@ -188,7 +188,13 @@ export class TXESession implements TXESessionStateHandler { const initialJobId = jobCoordinator.beginJob(); const logger = createLogger('txe:session'); - const contractSyncService = new ContractSyncService(stateMachine.node, contractStore, noteStore, logger); + const contractSyncService = new ContractSyncService( + stateMachine.node, + contractStore, + noteStore, + () => keyStore.getAccounts(), + logger, + ); const topLevelOracleHandler = new TXEOracleTopLevelContext( stateMachine, diff --git a/yarn-project/wallet-sdk/package.json b/yarn-project/wallet-sdk/package.json index b48c7af514f4..1c1f2927d540 100644 --- a/yarn-project/wallet-sdk/package.json +++ b/yarn-project/wallet-sdk/package.json @@ -7,6 +7,8 @@ "./base-wallet": "./dest/base-wallet/index.js", "./extension/handlers": "./dest/extension/handlers/index.js", "./extension/provider": "./dest/extension/provider/index.js", + "./iframe/handlers": "./dest/iframe/handlers/index.js", + "./iframe/provider": "./dest/iframe/provider/index.js", "./crypto": "./dest/crypto.js", "./types": "./dest/types.js", "./manager": "./dest/manager/index.js" @@ -16,6 +18,8 @@ "./src/base-wallet/index.ts", "./src/extension/handlers/index.ts", "./src/extension/provider/index.ts", + "./src/iframe/handlers/index.ts", + "./src/iframe/provider/index.ts", "./src/crypto.ts", "./src/types.ts", "./src/manager/index.ts" diff --git a/yarn-project/wallet-sdk/src/crypto.ts b/yarn-project/wallet-sdk/src/crypto.ts index 976628b50618..8b721b6f7a18 100644 --- a/yarn-project/wallet-sdk/src/crypto.ts +++ b/yarn-project/wallet-sdk/src/crypto.ts @@ -497,3 +497,107 @@ export function hashToEmoji(hash: string, count: number = DEFAULT_EMOJI_GRID_SIZ } return emojis.join(''); } + +// ─── Passphrase-based encryption (PBKDF2 + AES-256-GCM) ─────────────────── + +/** Default PBKDF2 iteration count. High to compensate for short PINs (~1-2s on modern hardware). */ +const DEFAULT_PBKDF2_ITERATIONS = 2_000_000; +const PBKDF2_SALT_BYTES = 16; +const PBKDF2_IV_BYTES = 12; + +/** + * Derives an AES-256-GCM key from a passphrase using PBKDF2-SHA256. + * + * @param passphrase - The user-provided passphrase or PIN + * @param salt - Random salt bytes + * @param iterations - PBKDF2 iteration count (default: 2,000,000) + * @returns An AES-256-GCM CryptoKey + */ +export async function deriveKeyFromPassphrase( + passphrase: string, + salt: Uint8Array, + iterations: number = DEFAULT_PBKDF2_ITERATIONS, +): Promise { + const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(passphrase), 'PBKDF2', false, [ + 'deriveKey', + ]); + return crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: salt as BufferSource, iterations, hash: 'SHA-256' }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'], + ); +} + +/** + * Encrypts arbitrary bytes with a passphrase using PBKDF2 + AES-256-GCM. + * + * Output layout: `[salt (16)] [iv (12)] [ciphertext (...)]` + * + * @param plaintext - Data to encrypt + * @param passphrase - User passphrase or PIN + * @param iterations - PBKDF2 iteration count (default: 2,000,000) + * @returns A Uint8Array containing salt + iv + ciphertext + */ +export async function encryptWithPassphrase( + plaintext: Uint8Array, + passphrase: string, + iterations: number = DEFAULT_PBKDF2_ITERATIONS, +): Promise { + const salt = crypto.getRandomValues(new Uint8Array(PBKDF2_SALT_BYTES)); + const iv = crypto.getRandomValues(new Uint8Array(PBKDF2_IV_BYTES)); + const key = await deriveKeyFromPassphrase(passphrase, salt, iterations); + const ciphertext = new Uint8Array( + await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext as BufferSource), + ); + const result = new Uint8Array(PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES + ciphertext.length); + result.set(salt, 0); + result.set(iv, PBKDF2_SALT_BYTES); + result.set(ciphertext, PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES); + return result; +} + +/** + * Decrypts data produced by {@link encryptWithPassphrase}. + * + * @param data - The encrypted blob (salt + iv + ciphertext) + * @param passphrase - The passphrase used during encryption + * @param iterations - PBKDF2 iteration count (must match encryption) + * @returns The decrypted plaintext bytes + * @throws On wrong passphrase (AES-GCM auth tag mismatch) + */ +export async function decryptWithPassphrase( + data: Uint8Array, + passphrase: string, + iterations: number = DEFAULT_PBKDF2_ITERATIONS, +): Promise { + const salt = data.slice(0, PBKDF2_SALT_BYTES); + const iv = data.slice(PBKDF2_SALT_BYTES, PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES); + const ciphertext = data.slice(PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES); + const key = await deriveKeyFromPassphrase(passphrase, salt, iterations); + return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext as BufferSource)); +} + +/** + * Converts a Uint8Array to a base64 string. + */ +export function uint8ToBase64(bytes: Uint8Array): string { + let binary = ''; + for (const b of bytes) { + binary += String.fromCharCode(b); + } + return btoa(binary); +} + +/** + * Converts a base64 string to a Uint8Array. + */ +export function base64ToUint8(b64: string): Uint8Array { + const binary = atob(b64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} diff --git a/yarn-project/wallet-sdk/src/extension/provider/extension_wallet.ts b/yarn-project/wallet-sdk/src/extension/provider/extension_wallet.ts index b2b59db2635f..35cdbf4bea68 100644 --- a/yarn-project/wallet-sdk/src/extension/provider/extension_wallet.ts +++ b/yarn-project/wallet-sdk/src/extension/provider/extension_wallet.ts @@ -6,7 +6,7 @@ import { schemaHasMethod } from '@aztec/foundation/schemas'; import type { FunctionsOf } from '@aztec/foundation/types'; import { type EncryptedPayload, decrypt, encrypt } from '../../crypto.js'; -import { type WalletMessage, WalletMessageType, type WalletResponse } from '../../types.js'; +import { type DisconnectCallback, type WalletMessage, WalletMessageType, type WalletResponse } from '../../types.js'; /** * Internal type representing a wallet method call before encryption. @@ -19,11 +19,6 @@ type WalletMethodCall = { args: unknown[]; }; -/** - * Callback type for wallet disconnect events. - */ -export type DisconnectCallback = () => void; - /** * A wallet implementation that communicates with browser extension wallets * using an encrypted MessageChannel. diff --git a/yarn-project/wallet-sdk/src/extension/provider/index.ts b/yarn-project/wallet-sdk/src/extension/provider/index.ts index 9df529c0598b..a611e183477b 100644 --- a/yarn-project/wallet-sdk/src/extension/provider/index.ts +++ b/yarn-project/wallet-sdk/src/extension/provider/index.ts @@ -1,4 +1,4 @@ -export { ExtensionWallet, type DisconnectCallback } from './extension_wallet.js'; +export { ExtensionWallet } from './extension_wallet.js'; export { ExtensionProvider, type DiscoveredWallet, diff --git a/yarn-project/wallet-sdk/src/iframe/handlers/iframe_connection_handler.ts b/yarn-project/wallet-sdk/src/iframe/handlers/iframe_connection_handler.ts new file mode 100644 index 000000000000..5605206fa985 --- /dev/null +++ b/yarn-project/wallet-sdk/src/iframe/handlers/iframe_connection_handler.ts @@ -0,0 +1,328 @@ +/** + * IframeConnectionHandler — wallet-side of the cross-origin iframe protocol. + * + * This mirrors {@link BackgroundConnectionHandler} from `@aztec/wallet-sdk/extension/handlers` + * but uses `window.postMessage` instead of browser.runtime messaging. + * + * Message flow (wallet receives): + * parent → DISCOVERY → show approval UI → send DISCOVERY_RESPONSE + * parent → KEY_EXCHANGE_REQUEST → ECDH → send KEY_EXCHANGE_RESPONSE + * parent → SECURE_MESSAGE → decrypt → Wallet → encrypt → SECURE_RESPONSE + * parent → DISCONNECT → terminate session + * + * The wallet announces itself by posting WALLET_READY as soon as the handler starts, + * so the dApp knows it can send a discovery request. + */ +import type { ChainInfo } from '@aztec/aztec.js/account'; +import { createLogger } from '@aztec/aztec.js/log'; +import type { Wallet } from '@aztec/aztec.js/wallet'; +import { WalletSchema } from '@aztec/aztec.js/wallet'; +import { jsonStringify } from '@aztec/foundation/json-rpc'; +import { parseWithOptionals, schemaHasMethod } from '@aztec/foundation/schemas'; + +import { + type EncryptedPayload, + decrypt, + deriveSessionKeys, + encrypt, + exportPublicKey, + generateKeyPair, + importPublicKey, +} from '../../crypto.js'; +import { type WalletMessage, WalletMessageType, type WalletResponse } from '../../types.js'; + +/** + * A pending discovery request from a dApp (before user approval). + */ +export interface PendingSession { + /** Unique request identifier */ + requestId: string; + /** Application identifier */ + appId: string; + /** Origin URL of the requesting page */ + origin: string; + /** Approval status */ + status: 'pending' | 'approved'; +} + +/** + * An active session (after key exchange). + */ +export interface ActiveSession { + /** Session identifier (same as the discovery requestId) */ + sessionId: string; + /** AES-256-GCM shared key for this session */ + sharedKey: CryptoKey; + /** Verification hash for emoji display */ + verificationHash: string; + /** Origin URL of the connected dApp */ + origin: string; + /** Application identifier */ + appId: string; +} + +/** + * Configuration for the iframe connection handler. + */ +export interface IframeConnectionConfig { + /** Unique wallet identifier */ + walletId: string; + /** Display name for the wallet */ + walletName: string; + /** Wallet version string */ + walletVersion: string; + /** Optional wallet icon URL */ + walletIcon?: string; + /** Origins allowed to connect. If empty or undefined, all origins are allowed (dev mode). */ + allowedOrigins?: string[]; +} + +/** + * Event callbacks for the iframe connection handler. + */ +export interface IframeConnectionCallbacks { + /** Called when a new discovery request arrives — wallet can show approval UI */ + onPendingDiscovery?: (session: PendingSession) => void; + /** Called when a session is established (key exchange complete) */ + onSessionEstablished?: (session: ActiveSession) => void; + /** Called when a session is terminated */ + onSessionTerminated?: (sessionId: string) => void; + /** Called when a key exchange completes — show verificationHash as emojis to the user */ + onVerificationHash?: (verificationHash: string) => void; + /** + * Resolves the Wallet instance to use for a given dApp and chain. + * Called when an encrypted message arrives and needs to be dispatched. + */ + getWallet: (appId: string, chainInfo: ChainInfo) => Promise; +} + +/** + * Handles the wallet side of the cross-origin iframe protocol. + * + * Manages the full lifecycle: discovery, ECDH key exchange, encrypted message + * dispatch to a {@link Wallet} instance, and session termination. + * + * @example + * ```typescript + * const handler = new IframeConnectionHandler( + * { walletId: 'my-wallet', walletName: 'My Wallet', walletVersion: '1.0.0' }, + * { + * onPendingDiscovery: (session) => showApprovalUI(session), + * getWallet: (appId, chainInfo) => createWalletForApp(appId, chainInfo), + * }, + * ); + * handler.start(); + * ``` + */ +export class IframeConnectionHandler { + private pendingSessions = new Map(); + private activeSessions = new Map(); + private log = createLogger('wallet:iframe-handler'); + + constructor( + private config: IframeConnectionConfig, + private callbacks: IframeConnectionCallbacks, + ) {} + + start(): void { + window.addEventListener('message', this.handleMessage); + this.postToParent({ type: WalletMessageType.WALLET_READY }); + this.log.info('IframeConnectionHandler started, posted WALLET_READY'); + } + + stop(): void { + window.removeEventListener('message', this.handleMessage); + } + + approveDiscovery(requestId: string): void { + const pending = this.pendingSessions.get(requestId); + if (!pending || pending.status !== 'pending') { + return; + } + + pending.status = 'approved'; + this.postToOrigin(pending.origin, { + type: WalletMessageType.DISCOVERY_RESPONSE, + requestId, + walletInfo: { + id: this.config.walletId, + name: this.config.walletName, + version: this.config.walletVersion, + icon: this.config.walletIcon, + }, + }); + this.log.info(`Discovery approved for requestId=${requestId}`); + } + + rejectDiscovery(requestId: string): void { + this.pendingSessions.delete(requestId); + } + + terminateSession(sessionId: string): void { + const session = this.activeSessions.get(sessionId); + if (session) { + this.postToOrigin(session.origin, { + type: WalletMessageType.SESSION_DISCONNECTED, + sessionId, + }); + this.activeSessions.delete(sessionId); + this.callbacks.onSessionTerminated?.(sessionId); + } + } + + getPendingSessions(): PendingSession[] { + return Array.from(this.pendingSessions.values()).filter(s => s.status === 'pending'); + } + + private handleMessage = (event: MessageEvent): void => { + void this.handleMessageAsync(event); + }; + + private async handleMessageAsync(event: MessageEvent): Promise { + if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) { + if (!this.config.allowedOrigins.includes(event.origin)) { + return; + } + } + + const msg = event.data; + if (!msg || typeof msg !== 'object' || !msg.type) { + return; + } + + switch (msg.type) { + case WalletMessageType.DISCOVERY: + this.handleDiscoveryRequest(msg, event.origin); + break; + case WalletMessageType.KEY_EXCHANGE_REQUEST: + await this.handleKeyExchangeRequest(msg, event.origin); + break; + case WalletMessageType.SECURE_MESSAGE: + await this.handleSecureMessage(msg); + break; + case WalletMessageType.DISCONNECT: + this.terminateSession(msg.sessionId); + break; + } + } + + private handleDiscoveryRequest(msg: Record, origin: string): void { + // eslint-disable-next-line jsdoc/require-jsdoc + const { requestId, appId } = msg as { requestId: string; appId: string }; + const pending: PendingSession = { requestId, appId, origin, status: 'pending' }; + this.pendingSessions.set(requestId, pending); + this.log.info(`Discovery request from appId=${appId} origin=${origin}`); + this.callbacks.onPendingDiscovery?.(pending); + } + + private async handleKeyExchangeRequest(msg: Record, origin: string): Promise { + const { requestId, publicKey: appPublicKeyRaw } = msg as { + // eslint-disable-next-line jsdoc/require-jsdoc + requestId: string; + // eslint-disable-next-line jsdoc/require-jsdoc + publicKey: { kty: string; crv: string; x: string; y: string }; + }; + const pending = this.pendingSessions.get(requestId); + if (!pending || pending.status !== 'approved') { + this.log.warn(`Key exchange for unknown/unapproved requestId=${requestId}`); + return; + } + + try { + const keyPair = await generateKeyPair(); + const walletPublicKey = await exportPublicKey(keyPair.publicKey); + const appPublicKey = await importPublicKey(appPublicKeyRaw); + const sessionKeys = await deriveSessionKeys(keyPair, appPublicKey, false); + + const session: ActiveSession = { + sessionId: requestId, + sharedKey: sessionKeys.encryptionKey, + verificationHash: sessionKeys.verificationHash, + origin: pending.origin, + appId: pending.appId, + }; + + this.activeSessions.set(requestId, session); + this.pendingSessions.delete(requestId); + + this.postToOrigin(origin, { + type: WalletMessageType.KEY_EXCHANGE_RESPONSE, + requestId, + publicKey: walletPublicKey, + verificationHash: sessionKeys.verificationHash, + }); + + this.callbacks.onVerificationHash?.(sessionKeys.verificationHash); + this.callbacks.onSessionEstablished?.(session); + this.log.info(`Key exchange complete, sessionId=${requestId}`); + } catch (err) { + this.log.error(`Key exchange failed: ${err}`); + } + } + + private async handleSecureMessage(msg: Record): Promise { + // eslint-disable-next-line jsdoc/require-jsdoc + const { sessionId, encrypted } = msg as { sessionId: string; encrypted: EncryptedPayload }; + const session = this.activeSessions.get(sessionId); + if (!session) { + return; + } + + let walletMessage: WalletMessage; + try { + walletMessage = await decrypt(session.sharedKey, encrypted); + } catch { + this.log.warn(`Decryption failed for sessionId=${sessionId}`); + return; + } + + const { messageId, type, args, chainInfo, appId } = walletMessage; + + let result: unknown; + let error: string | undefined; + + try { + const wallet = await this.callbacks.getWallet(appId, chainInfo); + + if (!schemaHasMethod(WalletSchema, type)) { + throw new Error(`Unknown wallet method: ${type}`); + } + // Zod's AnyZodTuple rejects optional tuple items typed as `T | undefined` + const sanitizedArgs = await parseWithOptionals(args, WalletSchema[type].parameters() as any); + result = await (wallet as Record Promise>)[type](...sanitizedArgs); + } catch (err: unknown) { + error = err instanceof Error ? err.message : String(err); + this.log.error(`Error handling ${type}: ${error}`); + } + + const response: WalletResponse = { + messageId, + walletId: this.config.walletId, + result, + error, + }; + + try { + const encryptedResponse = await encrypt(session.sharedKey, jsonStringify(response)); + this.postToOrigin(session.origin, { + type: WalletMessageType.SECURE_RESPONSE, + sessionId, + encrypted: encryptedResponse, + }); + } catch (err) { + this.log.error(`Encryption of response failed: ${err}`); + } + } + + private postToParent(msg: object): void { + if (window.parent !== window) { + window.parent.postMessage(msg, '*'); + } + } + + private postToOrigin(origin: string, msg: object): void { + if (window.parent !== window) { + window.parent.postMessage(msg, origin); + } + } +} diff --git a/yarn-project/wallet-sdk/src/iframe/handlers/index.ts b/yarn-project/wallet-sdk/src/iframe/handlers/index.ts new file mode 100644 index 000000000000..8207ad9d58ca --- /dev/null +++ b/yarn-project/wallet-sdk/src/iframe/handlers/index.ts @@ -0,0 +1,7 @@ +export { + IframeConnectionHandler, + type IframeConnectionConfig, + type IframeConnectionCallbacks, + type PendingSession, + type ActiveSession, +} from './iframe_connection_handler.js'; diff --git a/yarn-project/wallet-sdk/src/iframe/provider/iframe_discovery.ts b/yarn-project/wallet-sdk/src/iframe/provider/iframe_discovery.ts new file mode 100644 index 000000000000..e81684861e0e --- /dev/null +++ b/yarn-project/wallet-sdk/src/iframe/provider/iframe_discovery.ts @@ -0,0 +1,185 @@ +/** + * Web wallet discovery — creates {@link IframeWalletProvider} instances from a list of URLs. + * + * For each configured URL we probe the wallet by loading a tiny invisible iframe, + * waiting for WALLET_READY, then sending a DISCOVERY request. On a successful + * DISCOVERY_RESPONSE we emit an IframeWalletProvider to the caller. + * + * This is intentionally lightweight (no key exchange yet) — key exchange happens + * later when the user selects the wallet and calls `provider.establishSecureChannel()`. + */ +import type { ChainInfo } from '@aztec/aztec.js/account'; +import { promiseWithResolvers } from '@aztec/foundation/promise'; + +import type { DiscoverySession, WalletProvider } from '../../manager/types.js'; +import { type WalletInfo, WalletMessageType } from '../../types.js'; +import { IframeWalletProvider } from './iframe_provider.js'; + +const PROBE_TIMEOUT_MS = 10_000; + +/** + * Probes a list of web wallet URLs and returns a {@link DiscoverySession} compatible + * with WalletManager's `getAvailableWallets()` interface. + * + * Discovered {@link IframeWalletProvider} instances are yielded asynchronously as each + * wallet responds to the probe. + * + * @param walletUrls - URLs of web wallets to probe + * @param chainInfo - Network information to pass during discovery + * @returns A cancellable discovery session + */ +export function discoverWebWallets(walletUrls: string[], chainInfo: ChainInfo): DiscoverySession { + const { promise: donePromise, resolve: resolveDone } = promiseWithResolvers(); + + /* eslint-disable jsdoc/require-jsdoc */ + type IteratorState = + | { status: 'discovering'; resolve: ((result: IteratorResult) => void) | null } + | { status: 'done' }; + /* eslint-enable jsdoc/require-jsdoc */ + + let state: IteratorState = { status: 'discovering', resolve: null }; + const pendingProviders: WalletProvider[] = []; + + // eslint-disable-next-line jsdoc/require-jsdoc + function emit(provider: WalletProvider) { + if (state.status !== 'discovering') { + return; + } + if (state.resolve) { + const resolve = state.resolve; + state.resolve = null; + resolve({ value: provider, done: false }); + } else { + pendingProviders.push(provider); + } + } + + // eslint-disable-next-line jsdoc/require-jsdoc + function markComplete() { + if (state.status !== 'discovering') { + return; + } + const pendingResolve = state.resolve; + state = { status: 'done' }; + resolveDone(); + if (pendingResolve) { + pendingResolve({ value: undefined as unknown as WalletProvider, done: true }); + } + } + + // Probe all URLs in parallel + const probes = walletUrls.map(url => + probeWallet(url, chainInfo, PROBE_TIMEOUT_MS).then( + provider => { + if (provider) { + emit(provider); + } + }, + () => { + // ignore probe errors + }, + ), + ); + + void Promise.all(probes).then(markComplete); + + const wallets: AsyncIterable = { + // eslint-disable-next-line jsdoc/require-jsdoc + [Symbol.asyncIterator](): AsyncIterator { + return { + // eslint-disable-next-line jsdoc/require-jsdoc + next(): Promise> { + if (pendingProviders.length > 0) { + return Promise.resolve({ value: pendingProviders.shift()!, done: false }); + } + if (state.status === 'done') { + return Promise.resolve({ value: undefined as unknown as WalletProvider, done: true }); + } + return new Promise(resolve => { + if (state.status === 'discovering') { + state.resolve = resolve; + } + }); + }, + // eslint-disable-next-line jsdoc/require-jsdoc + return(): Promise> { + markComplete(); + return Promise.resolve({ value: undefined as unknown as WalletProvider, done: true }); + }, + }; + }, + }; + + return { + wallets, + done: donePromise, + cancel: markComplete, + }; +} + +/** + * Probes a single web wallet URL. + * Creates a temporary hidden iframe, waits for WALLET_READY, sends DISCOVERY_REQUEST. + * Returns an IframeWalletProvider on success, null on timeout/failure. + * @internal + */ +function probeWallet(walletUrl: string, chainInfo: ChainInfo, timeoutMs: number): Promise { + const walletOrigin = new URL(walletUrl).origin; + const iframe = document.createElement('iframe'); + iframe.src = walletUrl; + iframe.style.cssText = 'display:none;width:0;height:0;border:none;position:absolute;top:-9999px;'; + iframe.allow = 'storage-access; cross-origin-isolated'; + let timer: ReturnType; + + // Register listener BEFORE appending to DOM to avoid race with WALLET_READY + const result = new Promise(resolve => { + const cleanup = () => { + if (iframe.parentNode) { + iframe.parentNode.removeChild(iframe); + } + window.removeEventListener('message', handler); + clearTimeout(timer); + }; + + timer = setTimeout(() => { + cleanup(); + resolve(null); + }, timeoutMs); + + let step: 'waiting-ready' | 'waiting-discovery' = 'waiting-ready'; + const requestId = globalThis.crypto.randomUUID(); + + // eslint-disable-next-line jsdoc/require-jsdoc + function handler(event: MessageEvent) { + if (event.origin !== walletOrigin) { + return; + } + const msg = event.data; + if (!msg || typeof msg !== 'object') { + return; + } + + if (step === 'waiting-ready' && msg.type === WalletMessageType.WALLET_READY) { + step = 'waiting-discovery'; + iframe.contentWindow?.postMessage( + { type: WalletMessageType.DISCOVERY, requestId, appId: 'discovery-probe' }, + walletOrigin, + ); + } else if ( + step === 'waiting-discovery' && + msg.type === WalletMessageType.DISCOVERY_RESPONSE && + msg.requestId === requestId + ) { + const info = msg.walletInfo as WalletInfo; + cleanup(); + resolve(new IframeWalletProvider(info.id, info.name, info.icon, walletUrl, chainInfo)); + } + } + + window.addEventListener('message', handler); + }); + + document.body.appendChild(iframe); + + return result; +} diff --git a/yarn-project/wallet-sdk/src/iframe/provider/iframe_provider.ts b/yarn-project/wallet-sdk/src/iframe/provider/iframe_provider.ts new file mode 100644 index 000000000000..ddddec2e339d --- /dev/null +++ b/yarn-project/wallet-sdk/src/iframe/provider/iframe_provider.ts @@ -0,0 +1,331 @@ +/** + * IframeWalletProvider — implements {@link WalletProvider} for web wallets loaded in iframes. + * + * Flow (mirrors ExtensionProvider): + * 1. Creates an `