diff --git a/noir-projects/protocol-fuzzer/README.md b/noir-projects/protocol-fuzzer/README.md index 6116998ff2d9..a3f6103ef6c5 100644 --- a/noir-projects/protocol-fuzzer/README.md +++ b/noir-projects/protocol-fuzzer/README.md @@ -1,5 +1,5 @@ A state-machine fuzzer for Aztec contract interactions. It talks to a running -sandbox via a persistent Node.js HTTP bridge (`bridge.mjs`), compares the +sandbox via a persistent Node.js HTTP bridge (`wallet-bridge.mjs`), compares the sandbox's behavior to an in-memory model, and asserts on any divergence. Two machines are available: @@ -19,16 +19,22 @@ it uses the standard `Token` contract that ships with the wallet CLI: cargo run -- token --max-steps 100 ``` -The **side-effect** machine requires the **nightly** sandbox because it deploys custom -contracts compiled against the nightly's aztec-nr. Use `setup-nightly-sandbox.sh` to -automate the full setup (defaults to the last tested nightly tag; pass `--latest` to -try the newest one). See `SANDBOX_INSTRUCTIONS.md` for manual steps. +The **side-effect** machine requires custom contracts. There are two ways to set it up: +**Local setup** (no Docker, uses your repo build): +``` +bash setup-local.sh # compiles contracts, starts anvil + node + bridge +cargo run -- side-effect --artifacts-dir contracts/target --max-steps 100 ``` -bash setup-nightly-sandbox.sh +**Nightly Docker setup** (defaults to the last tested nightly tag; pass `--latest` for newest): +``` +bash setup-nightly-sandbox.sh cargo run -- side-effect --max-steps 100 ``` +The nightly script places artifacts at `/tmp/` inside the container (the default `--artifacts-dir`). + +See `SANDBOX_INSTRUCTIONS.md` for manual nightly steps and troubleshooting. To replay a specific failure seed: @@ -44,8 +50,15 @@ cargo run -- side-effect --max-steps 100000 --seed 0x5a7211231dcd6500 --seed 0xHEX Replay a specific seed --max-steps N Max fuzzing steps (default: 400) --max-batch-size N Max parallel sends per batch (default: 8) +--artifacts-dir DIR Contract artifact directory (side-effect only, default: /tmp) ``` +> **Note:** `--artifacts-dir` is resolved on the host and sent as-is to the bridge. +> For **local** setup the bridge runs on the host, so use the real path (e.g. +> `contracts/target`). For **nightly Docker** the bridge runs inside the container, +> so the path must be valid inside it — the default `/tmp` works because the nightly +> script places artifacts at `/tmp/*.json` inside the container. + ### Parallel batching Consecutive non-conflicting state-changing commands are batched and fired concurrently, @@ -64,12 +77,16 @@ Conflict rules (conservative -- false positives only reduce batch size): To verify that the sandbox is running correctly, run the integration smoke tests: ``` -cargo test -- --ignored --nocapture +ARTIFACTS_DIR=contracts/target cargo test -- --ignored --nocapture ``` These are `#[ignore]`d by default because they require a running sandbox. With bridge + fast slots, a full suite run takes ~1-2 minutes (~5-13s per transaction). +Environment variables for tests: +- `ARTIFACTS_DIR` -- contract artifact directory (default: `/tmp`) +- `BRIDGE_URL` -- bridge server URL (default: `http://localhost:8089`) + ## Contracts Contract sources live in `contracts/` within this crate, not in `noir-contracts/`. They @@ -82,9 +99,9 @@ with oracle calls (like `utilityLog`) that the nightly PXE doesn't support. - **Parent** (`contracts/parent_contract/`) -- forwards calls to SideEffect for cross-contract call testing -Artifacts are built by `setup-nightly-sandbox.sh` inside the nightly container and -placed in `contracts/target/` (not checked into git). +Artifacts are built by `setup-local.sh` or `setup-nightly-sandbox.sh` and placed in +`contracts/target/`. Pre-built artifacts are checked into git for convenience. -The setup script auto-detects the nightly commit by matching the container's nargo -hash against `origin/next`. See `SANDBOX_INSTRUCTIONS.md` for the full build pipeline, -version matrix, and troubleshooting. +The nightly setup script auto-detects the nightly commit by matching the container's +nargo hash against `origin/next`. See `SANDBOX_INSTRUCTIONS.md` for the full build +pipeline, version matrix, and troubleshooting. diff --git a/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md b/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md index d1ec0be540d5..ac5c0c95dbe6 100644 --- a/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md +++ b/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md @@ -1,5 +1,9 @@ # Protocol Fuzzer: Running with Nightly Docker Sandbox +> **For local development** (no Docker), use `setup-local.sh` instead. It starts +> anvil, the Aztec node, compiles contracts, and launches the bridge — all on the +> host. See `README.md` for quick-start instructions. + ## Overview The protocol fuzzer has two state machines: @@ -9,7 +13,7 @@ The protocol fuzzer has two state machines: - **side-effect**: Fuzzes note lifecycle, nullifier emission, and cross-contract calls via custom `SideEffect` and `Parent` contracts. **Requires the nightly sandbox.** -Both machines talk to the sandbox via a persistent Node.js HTTP bridge (`bridge.mjs`) +Both machines talk to the sandbox via a persistent Node.js HTTP bridge (`wallet-bridge.mjs`) that keeps a single CLIWallet instance alive across requests. ## Why the nightly sandbox? @@ -69,7 +73,7 @@ for the next block. Four things bring per-transaction time from ~35s down to ~4- 1. **Fast slots.** The setup script starts the sandbox with 5-second L1/L2 slot durations (default 36s/12s) and disables sequencer timetable enforcement. -2. **Persistent bridge.** `bridge.mjs` keeps a single Node.js wallet instance alive inside +2. **Persistent bridge.** `wallet-bridge.mjs` keeps a single Node.js wallet instance alive inside the container. Without it, each operation would shell out to the CLI wallet, paying a ~1.5s Node.js cold-start every time. 3. **Parallel batching.** The fuzzer buffers consecutive non-conflicting sends and fires @@ -218,14 +222,14 @@ done ### 5. Start the bridge server -The bridge server (`bridge.mjs`) runs inside the container and provides a persistent +The bridge server (`wallet-bridge.mjs`) runs inside the container and provides a persistent HTTP API that the fuzzer calls: ```bash -docker cp bridge.mjs aztec-sandbox-nightly:/usr/src/yarn-project/bridge.mjs +docker cp wallet-bridge.mjs aztec-sandbox-nightly:/usr/src/yarn-project/wallet-bridge.mjs docker exec -d aztec-sandbox-nightly \ - bash -c 'cd /usr/src/yarn-project && exec node --no-warnings bridge.mjs > /tmp/bridge.log 2>&1' + bash -c 'cd /usr/src/yarn-project && exec node --no-warnings wallet-bridge.mjs > /tmp/bridge.log 2>&1' # Wait for it to start curl -s http://localhost:8089/health # {"ok":true} @@ -319,7 +323,7 @@ on every operation. ### How the bridge works -`bridge.mjs` runs inside the container and lazily initializes a `CLIWallet` instance +`wallet-bridge.mjs` runs inside the container and lazily initializes a `CLIWallet` instance on the first request. The Rust fuzzer resolves aliases (`accounts:test0`, `contracts:test0`) to hex addresses before sending them to the bridge via HTTP POST. @@ -347,7 +351,7 @@ on the first request. The Rust fuzzer resolves aliases (`accounts:test0`, | wallet CLI | `node --no-warnings /usr/src/yarn-project/cli-wallet/dest/bin/index.js` | | sandbox CLI | `node --no-warnings /usr/src/yarn-project/aztec/dest/bin/index.js` | | anvil | `/opt/foundry/bin/anvil` | -| bridge server | `/usr/src/yarn-project/bridge.mjs` | +| bridge server | `/usr/src/yarn-project/wallet-bridge.mjs` | | bridge log | `/tmp/bridge.log` | ## Stopping diff --git a/noir-projects/protocol-fuzzer/setup-local.sh b/noir-projects/protocol-fuzzer/setup-local.sh new file mode 100755 index 000000000000..0a139491a29a --- /dev/null +++ b/noir-projects/protocol-fuzzer/setup-local.sh @@ -0,0 +1,302 @@ +#!/usr/bin/env bash +# +# Sets up a local Aztec sandbox for use with the protocol fuzzer. +# No Docker required — everything runs on the host. +# +# Prerequisites: +# cd $REPO_ROOT && ./bootstrap.sh build yarn-project +# +# What this script does: +# 1. Checks prerequisites (nargo, bb, anvil, node, jq, yarn-project build) +# 2. Kills any existing processes on ports 8545/8080/8089 +# 3. Starts anvil (L1) and the Aztec node + PXE +# 4. Compiles contracts (nargo + transpile + VK generation) +# 5. Starts the bridge server +# +# Options: +# --skip-compile Skip contract compilation (reuse existing artifacts) +# +set -euo pipefail + +SKIP_COMPILE=false +for arg in "$@"; do + case "$arg" in + --skip-compile) SKIP_COMPILE=true ;; + *) echo "Unknown option: $arg" >&2; exit 1 ;; + esac +done + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONTRACTS_DIR="${SCRIPT_DIR}/contracts" + +# Binaries (overridable via env vars) +NARGO="${NARGO:-${REPO_ROOT}/noir/noir-repo/target/release/nargo}" +BB="${BB:-${REPO_ROOT}/barretenberg/cpp/build/bin/bb}" +ANVIL="${ANVIL:-/opt/foundry/bin/anvil}" +TRANSPILER="${TRANSPILER:-${REPO_ROOT}/avm-transpiler/target/release/avm-transpiler}" + +# --------------------------------------------------------------------------- # +# Helpers +# --------------------------------------------------------------------------- # + +log() { echo "==> $*"; } +die() { echo "ERROR: $*" >&2; exit 1; } + +# PIDs of background processes we start — killed if the script fails partway through. +BG_PIDS=() +SETUP_COMPLETE=false +cleanup() { + if [ "$SETUP_COMPLETE" = true ]; then + return + fi + if [ ${#BG_PIDS[@]} -gt 0 ]; then + log "Setup failed — stopping background processes: ${BG_PIDS[*]}" + kill "${BG_PIDS[@]}" 2>/dev/null || true + wait "${BG_PIDS[@]}" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# kill_on_port PORT — kill any process listening on this TCP port +kill_on_port() { + local port=$1 pids + pids=$(lsof -ti "tcp:${port}" 2>/dev/null || true) + if [ -n "$pids" ]; then + log "Killing process(es) on port ${port}: ${pids}" + echo "$pids" | xargs kill -9 2>/dev/null || true + # Wait for the port to be fully released (up to 10s). + local elapsed=0 + while lsof -ti "tcp:${port}" >/dev/null 2>&1 && [ $elapsed -lt 10 ]; do + sleep 1 + elapsed=$((elapsed + 1)) + done + fi +} + +# wait_for_url PID URL MAX_SECONDS LOG_FILE +# Polls URL every 2s. Bails early if PID dies, printing last 30 lines of LOG_FILE. +wait_for_url() { + local pid=$1 url=$2 max=$3 logfile=$4 elapsed=0 + while [ $elapsed -lt "$max" ]; do + if ! kill -0 "$pid" 2>/dev/null; then + echo "--- last 30 lines of ${logfile} ---" + tail -30 "$logfile" + die "Process ${pid} exited unexpectedly" + fi + local code + code=$(curl -so /dev/null -w '%{http_code}' "$url" 2>/dev/null || true) + [ -n "$code" ] && [ "$code" != "000" ] && return 0 + sleep 2 + elapsed=$((elapsed + 2)) + done + die "Timed out after ${max}s waiting for ${url}. Check ${logfile}" +} + +# --------------------------------------------------------------------------- # +# 1. Check prerequisites +# --------------------------------------------------------------------------- # + +log "Checking prerequisites..." + +missing=() +[ -x "$NARGO" ] || missing+=("nargo (expected at ${NARGO})") +[ -x "$BB" ] || missing+=("bb (expected at ${BB})") +[ -x "$ANVIL" ] || missing+=("anvil (expected at ${ANVIL})") +command -v node >/dev/null || missing+=("node") +command -v jq >/dev/null || missing+=("jq") +command -v curl >/dev/null || missing+=("curl") + +if [ ${#missing[@]} -gt 0 ]; then + die "Missing prerequisites: ${missing[*]} + Run the full bootstrap to build everything: + cd ${REPO_ROOT} && ./bootstrap.sh build yarn-project" +fi + +# Determine which tool to use for artifact processing (transpilation + prefix stripping). +# bb aztec_process (new) does it all in one step; avm-transpiler (old) needs separate steps. +USE_BB_AZTEC_PROCESS=false +if "$BB" aztec_process 2>&1 | grep -q "contract artifact"; then + USE_BB_AZTEC_PROCESS=true + log "Using: bb aztec_process" +elif [ -x "$TRANSPILER" ]; then + log "Using: avm-transpiler + jq prefix strip + bb write_vk" +else + die "No artifact processor found. + Option A: Rebuild bb with AVM support + Option B: Build the avm-transpiler: + cd ${REPO_ROOT}/avm-transpiler && cargo build --release" +fi + +# Verify yarn-project is properly built. A partial build (swc-only, no generate steps) +# will have dest/ dirs but missing generated files, causing runtime crashes. +YP="${REPO_ROOT}/yarn-project" +missing_build=() +[ -d "${YP}/cli-wallet/dest" ] || missing_build+=("cli-wallet/dest") +[ -f "${YP}/cli/dest/config/generated/networks.js" ] || missing_build+=("cli/generated/networks.js") +[ -f "${YP}/ethereum/dest/generated/l1-contracts-defaults.js" ] || missing_build+=("ethereum/generated/l1-contracts-defaults.js") +[ -f "${YP}/noir-protocol-circuits-types/dest/vk_tree.js" ] || missing_build+=("noir-protocol-circuits-types/vk_tree.js") +if [ ${#missing_build[@]} -gt 0 ]; then + die "yarn-project is not fully built (missing: ${missing_build[*]}). + Run the full bootstrap first: + cd ${REPO_ROOT} && ./bootstrap.sh build yarn-project" +fi + +log "All prerequisites OK" + +# --------------------------------------------------------------------------- # +# 2. Kill existing processes on our ports +# --------------------------------------------------------------------------- # + +log "Checking for port conflicts..." +kill_on_port 8545 +kill_on_port 8080 +kill_on_port 8089 + +# --------------------------------------------------------------------------- # +# 3. Start anvil +# --------------------------------------------------------------------------- # + +log "Starting anvil on port 8545..." +"$ANVIL" --host 0.0.0.0 --port 8545 > /tmp/anvil-local.log 2>&1 & +ANVIL_PID=$! +BG_PIDS+=("$ANVIL_PID") +sleep 1 +if ! kill -0 "$ANVIL_PID" 2>/dev/null; then + die "Anvil failed to start. Check /tmp/anvil-local.log" +fi +log "Anvil running (PID ${ANVIL_PID})" + +# --------------------------------------------------------------------------- # +# 4. Start Aztec node and wait for PXE +# --------------------------------------------------------------------------- # + +log "Starting Aztec node on port 8080..." +( + cd "${REPO_ROOT}/yarn-project" + ETHEREUM_SLOT_DURATION=5 \ + AZTEC_SLOT_DURATION=5 \ + AZTEC_EPOCH_DURATION=4 \ + SEQ_ENFORCE_TIME_TABLE=false \ + LOG_LEVEL=info \ + node --no-warnings ./aztec/dest/bin/index.js start \ + --local-network \ + --l1-rpc-urls http://127.0.0.1:8545 \ + > /tmp/aztec-node-local.log 2>&1 +) & +NODE_PID=$! +BG_PIDS+=("$NODE_PID") + +log "Waiting for PXE on port 8080 (up to 300s)..." +wait_for_url "$NODE_PID" http://localhost:8080 300 /tmp/aztec-node-local.log +log "PXE is ready" + +# --------------------------------------------------------------------------- # +# 5. Compile contracts +# --------------------------------------------------------------------------- # + +# Map package names to artifact base names (nargo uses the contract name, not the package name) +declare -A ARTIFACT_NAMES=( + [side_effect_contract]="side_effect_contract-SideEffect" + [parent_contract]="parent_contract-Parent" +) + +if [ "$SKIP_COMPILE" = true ]; then + log "Skipping contract compilation (--skip-compile)" + for contract_pkg in side_effect_contract parent_contract; do + artifact="${ARTIFACT_NAMES[$contract_pkg]}" + json_path="${CONTRACTS_DIR}/target/${artifact}.json" + [ -f "$json_path" ] || die "Artifact not found: ${json_path} (cannot --skip-compile without prior build)" + done +else + mkdir -p "${CONTRACTS_DIR}/target" + + for contract_pkg in side_effect_contract parent_contract; do + artifact="${ARTIFACT_NAMES[$contract_pkg]}" + json_path="${CONTRACTS_DIR}/target/${artifact}.json" + + log "Compiling ${contract_pkg}..." + (cd "$CONTRACTS_DIR" && "$NARGO" compile --silence-warnings --inliner-aggressiveness 0 --package "$contract_pkg") + + if [ "$USE_BB_AZTEC_PROCESS" = true ]; then + log "Processing ${artifact} with bb aztec_process..." + "$BB" aztec_process -i "$json_path" + else + log "Transpiling ${artifact}..." + "$TRANSPILER" "$json_path" "$json_path" + + log "Stripping __aztec_nr_internals__ prefix..." + jq '.functions |= map(.name |= sub("^__aztec_nr_internals__"; ""))' "$json_path" > "${json_path}.tmp" + mv "${json_path}.tmp" "$json_path" + + # Generate verification keys for private functions. + log "Generating VKs for private functions..." + vk_tmp_dir=$(mktemp -d) + func_count=$(jq '.functions | length' "$json_path") + for (( i=0; i/dev/null || true) + if [ "$make_vk" = "true" ]; then + fname=$(jq -r ".functions[$i].name" "$json_path") + log " VK: ${fname}" + jq -r ".functions[$i].bytecode" "$json_path" \ + | base64 -d | gunzip \ + | "$BB" write_vk --scheme chonk -b - -o "$vk_tmp_dir" -v 2>/dev/null + vk_b64=$(base64 -w 0 < "$vk_tmp_dir/vk") + jq --arg vk "$vk_b64" --argjson idx "$i" \ + '.functions[$idx].verification_key = $vk' "$json_path" > "${json_path}.tmp" + mv "${json_path}.tmp" "$json_path" + fi + done + rm -rf "$vk_tmp_dir" + fi + + log "Built ${artifact}" + done +fi + +# --------------------------------------------------------------------------- # +# 6. Start bridge +# --------------------------------------------------------------------------- # + +# Verify node is still alive after compilation +if ! kill -0 "$NODE_PID" 2>/dev/null; then + die "Aztec node died during contract compilation. Check /tmp/aztec-node-local.log" +fi + +# Clear stale wallet state from previous runs (avoids "block hash not found" errors). +rm -rf "${HOME}/.aztec/wallet" + +# Symlink wallet-bridge.mjs into yarn-project/ so @aztec/* resolves from its node_modules. +# --preserve-symlinks(-main) makes Node resolve from the symlink location, not the target. +ln -sf "../noir-projects/protocol-fuzzer/wallet-bridge.mjs" "${REPO_ROOT}/yarn-project/wallet-bridge.mjs" + +log "Starting bridge server on port 8089..." +(cd "${REPO_ROOT}/yarn-project" && exec node --preserve-symlinks --preserve-symlinks-main --no-warnings wallet-bridge.mjs > /tmp/bridge-local.log 2>&1) & +BRIDGE_PID=$! +BG_PIDS+=("$BRIDGE_PID") + +wait_for_url "$BRIDGE_PID" http://localhost:8089/health 60 /tmp/bridge-local.log +log "Bridge is ready (PID ${BRIDGE_PID})" + +# --------------------------------------------------------------------------- # +# Done +# --------------------------------------------------------------------------- # + +echo "" +log "Local sandbox is ready!" +echo "" +echo " Anvil: http://localhost:8545 (PID ${ANVIL_PID}, log: /tmp/anvil-local.log)" +echo " Aztec node: http://localhost:8080 (PID ${NODE_PID}, log: /tmp/aztec-node-local.log)" +echo " Bridge: http://localhost:8089 (PID ${BRIDGE_PID}, log: /tmp/bridge-local.log)" +echo " Slot time: 5s" +echo "" +echo "Run the fuzzer:" +echo "" +echo " cd ${SCRIPT_DIR}" +echo " RUST_LOG=debug cargo run -- side-effect --artifacts-dir ${CONTRACTS_DIR}/target --max-steps 5" +echo "" +echo "To stop all services:" +echo " kill ${ANVIL_PID} ${NODE_PID} ${BRIDGE_PID}" +echo "" + +SETUP_COMPLETE=true diff --git a/noir-projects/protocol-fuzzer/setup-nightly-sandbox.sh b/noir-projects/protocol-fuzzer/setup-nightly-sandbox.sh index 7fa98441b9f3..853e5fbc41ab 100755 --- a/noir-projects/protocol-fuzzer/setup-nightly-sandbox.sh +++ b/noir-projects/protocol-fuzzer/setup-nightly-sandbox.sh @@ -16,7 +16,7 @@ set -euo pipefail CONTAINER_NAME="aztec-sandbox-nightly" # Last nightly tag verified to work with the current contract source code. -KNOWN_GOOD_TAG="5.0.0-nightly.20260224" +KNOWN_GOOD_TAG="5.0.0-nightly.20260402" WRAPPER_DIR="${HOME}/.local/bin" WRAPPER_PATH="${WRAPPER_DIR}/aztec-wallet" REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" @@ -225,15 +225,15 @@ done log "Waiting for Aztec Server HTTP endpoint to be ready..." wait_for_http http://localhost:8080 120 || die "PXE HTTP endpoint did not recover" -BRIDGE_SRC="${REPO_ROOT}/noir-projects/protocol-fuzzer/bridge.mjs" +BRIDGE_SRC="${REPO_ROOT}/noir-projects/protocol-fuzzer/wallet-bridge.mjs" if [ ! -f "$BRIDGE_SRC" ]; then die "Bridge source not found: ${BRIDGE_SRC}" fi log "Starting bridge server..." -docker cp "$BRIDGE_SRC" "${CONTAINER_NAME}:/usr/src/yarn-project/bridge.mjs" +docker cp "$BRIDGE_SRC" "${CONTAINER_NAME}:/usr/src/yarn-project/wallet-bridge.mjs" docker exec -d "$CONTAINER_NAME" \ - bash -c 'cd /usr/src/yarn-project && exec node --no-warnings bridge.mjs > /tmp/bridge.log 2>&1' + bash -c 'cd /usr/src/yarn-project && exec node --no-warnings wallet-bridge.mjs > /tmp/bridge.log 2>&1' if wait_for_http http://localhost:8089/health 60 2; then log "Bridge is ready on port 8089" diff --git a/noir-projects/protocol-fuzzer/src/main.rs b/noir-projects/protocol-fuzzer/src/main.rs index 2daa74e96c57..c9427275899c 100644 --- a/noir-projects/protocol-fuzzer/src/main.rs +++ b/noir-projects/protocol-fuzzer/src/main.rs @@ -60,6 +60,9 @@ struct SideEffectArgs { common: CommonArgs, #[arg(long, default_value_t = 5)] storage_slots: usize, + /// Directory containing compiled contract artifacts. + #[arg(long, default_value = "/tmp")] + artifacts_dir: String, } fn parse_hex_u64(s: &str) -> Result { @@ -123,6 +126,7 @@ fn main() { let mut machine = side_effect::SideEffectMachine { storage_slots: se_args.storage_slots, bridge: Some(&bridge), + artifacts_dir: se_args.artifacts_dir.clone(), }; log::debug!( "Starting side-effect machine with parameters: {:?}", @@ -164,6 +168,10 @@ mod integration_tests { &BRIDGE } + fn artifacts_dir() -> String { + std::env::var("ARTIFACTS_DIR").unwrap_or_else(|_| "/tmp".to_string()) + } + /// Verifies the sandbox is reachable and test accounts can be imported. /// Run this first to diagnose setup issues before running heavier tests. /// Prefixed with `_0` so it sorts first alphabetically (`_` < `a` in @@ -205,6 +213,7 @@ mod integration_tests { let mut machine = side_effect::SideEffectMachine { storage_slots: 2, bridge: Some(bridge), + artifacts_dir: artifacts_dir(), }; smt::fixed_size_builder(1024).run(|u| smt::run(u, &mut machine, 5)) } @@ -271,6 +280,7 @@ mod integration_tests { let mut machine = side_effect::SideEffectMachine { storage_slots: 1, bridge: Some(bridge), + artifacts_dir: artifacts_dir(), }; let state = side_effect::machine::SideEffectState { accounts: vec![0, 1, 2], @@ -327,6 +337,7 @@ mod integration_tests { let mut machine = side_effect::SideEffectMachine { storage_slots: 3, bridge: None, + artifacts_dir: artifacts_dir(), }; let mut state = machine.gen_state(&mut u).unwrap(); let mut commands = Vec::new(); diff --git a/noir-projects/protocol-fuzzer/src/side_effect/machine.rs b/noir-projects/protocol-fuzzer/src/side_effect/machine.rs index c1b236825ece..f5b74ec0b78c 100644 --- a/noir-projects/protocol-fuzzer/src/side_effect/machine.rs +++ b/noir-projects/protocol-fuzzer/src/side_effect/machine.rs @@ -21,6 +21,9 @@ pub struct SideEffectMachine<'a> { /// Required for `new_system()` (deploy + import). `None` is fine for /// model-only tests that never call `new_system`. pub bridge: Option<&'a Bridge>, + /// Directory containing compiled contract JSON artifacts (resolved to + /// an absolute path in `SideEffectSystem::new`). + pub artifacts_dir: String, } #[derive(Debug, Clone)] @@ -466,7 +469,7 @@ impl<'a> smt::StateMachine for SideEffectMachine<'a> { bridge .import_test_accounts() .expect("could not import test accounts"); - let system = SideEffectSystem::new(bridge); + let system = SideEffectSystem::new(bridge, &self.artifacts_dir); system .deploy_side_effect_contract(0) .expect("side-effect contract could not be deployed"); @@ -804,6 +807,7 @@ mod tests { SideEffectMachine { storage_slots: 5, bridge: None, + artifacts_dir: "/tmp".into(), } } diff --git a/noir-projects/protocol-fuzzer/src/side_effect/system.rs b/noir-projects/protocol-fuzzer/src/side_effect/system.rs index aa60b2b36268..b53cdad84865 100644 --- a/noir-projects/protocol-fuzzer/src/side_effect/system.rs +++ b/noir-projects/protocol-fuzzer/src/side_effect/system.rs @@ -2,8 +2,8 @@ use super::machine::SideEffectCommand; use crate::wallet::{AccountId, Bridge, WalletCommand}; pub struct SideEffectSystem<'a> { - side_effect_artifact: &'static str, - parent_artifact: &'static str, + side_effect_artifact: String, + parent_artifact: String, bridge: &'a Bridge, } @@ -144,7 +144,7 @@ impl<'a> SideEffectSystem<'a> { pub(crate) fn deploy_side_effect_contract(&self, account: AccountId) -> anyhow::Result { self.bridge.deploy( - self.side_effect_artifact, + &self.side_effect_artifact, &format!("accounts:test{account}"), "test0", Some("initialize"), @@ -154,7 +154,7 @@ impl<'a> SideEffectSystem<'a> { pub(crate) fn deploy_parent_contract(&self, account: AccountId) -> anyhow::Result { self.bridge.deploy( - self.parent_artifact, + &self.parent_artifact, &format!("accounts:test{account}"), "parent0", Some("initialize"), @@ -162,10 +162,14 @@ impl<'a> SideEffectSystem<'a> { ) } - pub(crate) fn new(bridge: &'a Bridge) -> Self { + pub(crate) fn new(bridge: &'a Bridge, artifacts_dir: &str) -> Self { + let dir = std::path::Path::new(artifacts_dir) + .canonicalize() + .unwrap_or_else(|e| panic!("cannot resolve artifacts dir {artifacts_dir:?}: {e}")); + let dir = dir.display(); Self { - side_effect_artifact: "/tmp/side_effect_contract-SideEffect.json", - parent_artifact: "/tmp/parent_contract-Parent.json", + side_effect_artifact: format!("{dir}/side_effect_contract-SideEffect.json"), + parent_artifact: format!("{dir}/parent_contract-Parent.json"), bridge, } } diff --git a/noir-projects/protocol-fuzzer/bridge.mjs b/noir-projects/protocol-fuzzer/wallet-bridge.mjs similarity index 78% rename from noir-projects/protocol-fuzzer/bridge.mjs rename to noir-projects/protocol-fuzzer/wallet-bridge.mjs index 112d4239466a..02db976dffd7 100644 --- a/noir-projects/protocol-fuzzer/bridge.mjs +++ b/noir-projects/protocol-fuzzer/wallet-bridge.mjs @@ -1,14 +1,18 @@ #!/usr/bin/env node -// bridge.mjs -- Persistent HTTP bridge for the Aztec protocol fuzzer. +// wallet-bridge.mjs -- Persistent HTTP bridge for the Aztec protocol fuzzer. // -// Runs inside the nightly sandbox container. Reuses CLIWallet to avoid -// the ~1.5s cold-start of spawning a new Node process per call. +// Works both inside the nightly sandbox container and on the host against a +// locally-built yarn-project. The CLI path is auto-detected: if the container +// path exists we use it, otherwise we resolve relative to the repo root. +// Must be started from yarn-project/ so @aztec/* imports resolve. // All addresses arrive as raw 0x hex strings (resolved by the Rust fuzzer). import { createServer } from 'node:http'; -import { join } from 'node:path'; +import { join, resolve, dirname } from 'node:path'; +import { existsSync } from 'node:fs'; import { homedir } from 'node:os'; import { format } from 'node:util'; +import { fileURLToPath } from 'node:url'; const PORT = parseInt(process.env.BRIDGE_PORT || '8089', 10); const NODE_URL = process.env.AZTEC_NODE_URL || 'http://localhost:8080'; @@ -17,7 +21,16 @@ const DATA_DIR = process.env.WALLET_DATA_DIRECTORY || join(homedir(), '.aztec/wa const { createAztecNodeClient } = await import('@aztec/aztec.js/node'); const { AztecAddress } = await import('@aztec/aztec.js/addresses'); const { openStoreAt } = await import('@aztec/kv-store/lmdb-v2'); -const CLI = '/usr/src/yarn-project/cli-wallet/dest'; + +// Auto-detect CLI path: try container path, then common local locations. +const __dirname = dirname(fileURLToPath(import.meta.url)); +const candidates = [ + '/usr/src/yarn-project/cli-wallet/dest', // nightly container + resolve(__dirname, 'cli-wallet/dest'), // symlinked into yarn-project/ + resolve(__dirname, '../../yarn-project/cli-wallet/dest'), // original location in protocol-fuzzer/ +]; +const CLI = candidates.find(p => existsSync(p)); +if (!CLI) throw new Error('Cannot find cli-wallet/dest in any known location'); const { CLIWallet } = await import(`${CLI}/utils/wallet.js`); const { WalletDB } = await import(`${CLI}/storage/wallet_db.js`); const { importTestAccounts } = await import(`${CLI}/cmds/import_test_accounts.js`); @@ -80,7 +93,9 @@ const handlers = { false, /* skipClassPublication */ false, /* skipInitialization */ true, /* wait */ - feeOpts, false, 120, /* fee, verbose, timeout */ + feeOpts, /* fee */ + 'mined', /* waitForStatus */ + false, 120, /* verbose, timeout */ { debug: noop, error: noop }, log, /* debugLogger, log */ ), ); @@ -94,7 +109,7 @@ const handlers = { const callArgs = args || []; const { stdout } = await capturing(log => verb === 'send' - ? send(w, node, sender, method, callArgs, artifact, target, true, false, feeOpts, [], false, log) + ? send(w, node, sender, method, callArgs, artifact, target, true, false, feeOpts, [], 'mined', false, log) : simulate(w, node, sender, method, callArgs, artifact, target, feeOpts, [], false, log), ); return { ok: true, stdout }; diff --git a/yarn-project/.gitignore b/yarn-project/.gitignore index 399ef4765c67..7aaf70e341e8 100644 --- a/yarn-project/.gitignore +++ b/yarn-project/.gitignore @@ -61,6 +61,8 @@ cli/src/config/generated ethereum/src/generated slasher/src/generated +# Symlink created by noir-projects/protocol-fuzzer/setup-local.sh +wallet-bridge.mjs .claude/settings.local.json .yarn/* !.yarn/patches