diff --git a/.githooks/pre-push b/.githooks/pre-push
new file mode 100755
index 00000000..dcee850e
--- /dev/null
+++ b/.githooks/pre-push
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+# Pre-push gate for zk-coins/node.
+#
+# Activation (one-time per clone):
+# git config core.hooksPath .githooks
+#
+# This hook catches lint regressions in seconds — there is no point
+# waiting for CI to flag a missing import or a misformatted file.
+#
+# The authoritative test + coverage gate runs in CI on a self-hosted
+# M3 Ultra runner (issue #40, .github/workflows/ci.yaml). The hook
+# does *not* re-run those — locally on a laptop they take 60-90 min,
+# and a developer waiting that long on every push is exactly what
+# issue #40 removed.
+#
+# Wall budgets (warm cache, M3 Ultra) — kept in sync with the matching
+# table in CONTRIBUTING.md § Setup:
+# - cold cache: < 2 min
+# - warm cache: < 30 s
+#
+# Bypass: `git push --no-verify` works. CI is the real gate, so a
+# bypassed lint failure surfaces at the PR check level instead.
+set -euo pipefail
+
+echo "[pre-push] cargo fmt --all --check"
+cargo fmt --all --check
+
+echo "[pre-push] cargo clippy -p node -p shared (MVP feature set)"
+cargo clippy -p node -p shared -- -D warnings
+
+echo "[pre-push] cargo clippy -p node --all-features (self-host opt-in build)"
+cargo clippy -p node --all-features -- -D warnings
+
+echo "[pre-push] cargo clippy -p zkcoins-program-plonky2 -p zkcoins-prover-plonky2 --lib"
+cargo clippy -p zkcoins-program-plonky2 -p zkcoins-prover-plonky2 --lib -- -D warnings
+
+echo "[pre-push] cargo check --workspace --all-features"
+cargo check --workspace --all-features
+
+echo "[pre-push] all checks passed."
diff --git a/.github/workflows/auto-release-pr.yaml b/.github/workflows/auto-release-pr.yaml
index d7c452fc..44d1feba 100644
--- a/.github/workflows/auto-release-pr.yaml
+++ b/.github/workflows/auto-release-pr.yaml
@@ -8,6 +8,7 @@ on:
permissions:
contents: read
pull-requests: write
+ issues: write # required by `gh label create`
concurrency:
group: auto-release-pr
@@ -58,8 +59,18 @@ jobs:
"- [ ] Merge when ready for production" \
> /tmp/pr-body.md
+ # `ci:full` opts the PR into the heavy M3 Ultra test + coverage
+ # jobs (see ci.yaml). Release PRs are exactly when we want the
+ # authoritative gate, so apply it on creation rather than
+ # relying on a human to remember the click.
+ gh label create ci:full \
+ --color FFA500 \
+ --description "Run heavy M3 Ultra test + coverage jobs on this PR" \
+ 2>/dev/null || true
+
gh pr create \
--base main \
--head develop \
--title "Release: develop -> main" \
+ --label ci:full \
--body-file /tmp/pr-body.md
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index e6ea9e6c..791cbfa1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -1,20 +1,66 @@
name: CI
on:
- push:
- branches: [develop]
- # Only trigger on PRs targeting develop (feature → develop). The
- # release PR (develop → main) is opened automatically and would
- # otherwise fire a second CI run for every push to develop — those
- # duplicate runs surfaced as "fail" entries on the release PR's
- # check list whenever the concurrency block cancelled the older
- # one. The push event already covers develop, and its run is
- # associated with the same SHA on the release PR.
+ # CI runs on every pull request regardless of target branch. This
+ # makes the default safe for stacked PRs (PR-A → PR-B → PR-C where
+ # each PR's base is the previous PR's branch) and any other workflow
+ # that opens a PR against a non-`develop` branch — previously such
+ # PRs were silently skipped because `branches: [develop]` filtered
+ # them out, and the only fix was to hand-edit ci.yaml on each new
+ # feature stack. Letting every PR trigger CI is cheap (the heavy
+ # M3 Ultra jobs are still gated behind the `ci:full` label below)
+ # and matches what most repos default to.
+ #
+ # `push: develop` is intentionally absent. Every commit reaching
+ # `develop` is already covered by the open Release PR (`Release:
+ # develop -> main`, created by auto-release-pr.yaml) — that PR's
+ # `synchronize` event runs CI on the new HEAD, and because the
+ # Release PR carries the `ci:full` label the heavy gate runs too.
+ # Adding `on: push: branches: [develop]` would queue a second
+ # workflow instance on the same SHA, doubling self-hosted-runner
+ # load on a check the Release PR's `synchronize` already provides.
+ # (Under the PR-number grouping in the concurrency block below the
+ # two runs would land in DIFFERENT groups — push keyed by
+ # `refs/heads/develop`, PR keyed by the Release PR's number — so
+ # the block would not deduplicate them.)
+ #
+ # `ready_for_review` is added so the workflow fires the moment a
+ # draft PR is marked ready — drafts themselves skip CI via the
+ # `if:` guard on each job (saves self-hosted-runner time while
+ # work is still in progress).
+ #
+ # `labeled` / `unlabeled` are added so toggling the `ci:full`
+ # label triggers (or removes) the heavy self-hosted-runner jobs
+ # on demand — see the `node-tests` job below.
pull_request:
- branches: [develop]
+ types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
concurrency:
- group: ci-${{ github.event.pull_request.head.sha || github.sha }}
+ # Group by PR number so a new push to the same PR cancels the
+ # in-flight Heavy run on the outdated commit. The m3-ultra pool
+ # (6 runner agents on dfx01) is shared with every other open PR —
+ # letting an obsolete 60-90-min run finish wastes a slot another
+ # PR could use. Grouping by SHA (the previous approach) put every
+ # commit in its own group, so `cancel-in-progress: true` never
+ # fired and back-to-back pushes queued sequentially.
+ # Falls back to `github.ref` for push/dispatch events (where there
+ # is no `pull_request.number`), so e.g. a `workflow_dispatch` on
+ # the same ref serializes too.
+ #
+ # Label events (`labeled` / `unlabeled`) get their own isolated
+ # group keyed by `run_id`, so toggling a label on a PR does NOT
+ # cancel an in-flight 60-90-min Heavy run on the same PR — most
+ # label toggles are unrelated (`bug`, `priority/*`, …) and killing
+ # the Heavy run for them would be a footgun. Trade-off: removing
+ # `ci:full` mid-run does NOT auto-stop a Heavy run that is already
+ # executing; cancel it manually with `gh run cancel` if you really
+ # need to free an agent.
+ group: >-
+ ${{
+ (github.event.action == 'labeled' || github.event.action == 'unlabeled')
+ && format('ci-{0}-label-{1}', github.workflow, github.run_id)
+ || format('ci-{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)
+ }}
cancel-in-progress: true
permissions:
@@ -22,20 +68,25 @@ permissions:
env:
CARGO_TERM_COLOR: always
- # Force Esplora broadcasts to fail fast in CI. Some unit tests
- # exercise the commit pipeline that ends in a real HTTP broadcast;
- # without this, the runs against the public Mutinynet API can take
- # >60 s per test and tip the job over the timeout.
- ESPLORA_URL: "http://127.0.0.1:1/api"
- # Force the SP1 mock prover for every test in this workflow. The
- # default prover targets real Groth16/Plonk circuits and a single
- # send_coin/receive_coin test then takes ~20+ minutes on an x86_64
- # runner. Mock proofs return instantly and exercise the same plumbing.
- SP1_PROVER: mock
+# `lint-and-build` catches what GitHub-hosted Linux can cheaply catch:
+# cross-platform compile bitrot and lint regressions.
+#
+# `node-tests` + `coverage` are the authoritative test + coverage gate.
+# They run on the m3-ultra self-hosted runner pool (label `m3-ultra`,
+# 6 agents on dfx01) — the documented hardware target (CONTRIBUTING.md
+# § "Working on the Plonky2 Migration"). On `ubuntu-latest` the full
+# suite repeatedly hit the 75-min timeout (issue #30); on the M3 Ultra
+# it is ~60-90 min for a Rust change. Moving the gate into CI rather
+# than the developer's laptop unblocks the developer on push
+# (issue #40).
+#
+# Runner ops: see scripts/ci-runner/README.md.
jobs:
lint-and-build:
name: Lint & Build
+ # Skip on draft PRs; downstream `needs:` jobs inherit the skip.
+ if: github.event_name == 'push' || github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
@@ -62,107 +113,276 @@ jobs:
- name: Check formatting
run: cargo fmt --all --check
- - name: Run clippy (server + shared, MVP feature set)
- run: cargo clippy -p server -p shared -- -D warnings
+ - name: Run clippy (node + shared, MVP feature set)
+ run: cargo clippy -p node -p shared -- -D warnings
- - name: Run clippy (server, all features)
- run: cargo clippy -p server --all-features -- -D warnings
+ - name: Run clippy (node, all features)
+ run: cargo clippy -p node --all-features -- -D warnings
- - name: Run clippy (program lib)
- run: cargo clippy -p zkcoins-program --lib -- -D warnings
+ - name: Run clippy (program + prover libs)
+ run: cargo clippy -p zkcoins-program-plonky2 -p zkcoins-prover-plonky2 --lib -- -D warnings
- - name: Build server (MVP feature set — the PRD image)
- run: cargo build -p server
+ # Issue #84: the chain-tip wait path and the publisher's
+ # commit→reveal propagation wait must be event-driven (WS /
+ # ZMQ), not polled. The grep below fails the build if a
+ # `tokio::time::{sleep,sleep_until,interval}` or
+ # `std::thread::sleep` call sneaks back into the scanner /
+ # publisher modules without the documented opt-out marker. See
+ # CONTRIBUTING.md § "No polling — events only" for the per-line
+ # `scanner-polling-ok:` escape hatch and the rationale for each
+ # currently-grandfathered occurrence. The marker is a plain
+ # comment token (not an `#[allow(...)]` attribute) so future
+ # contributors cannot mistake it for a real lint suppression
+ # (issue #84 round-4 MINOR 4).
+ - name: Forbid polling patterns in scanner/publisher
+ run: |
+ set -e
+ FOUND=$(grep -rEn 'tokio::time::(sleep|sleep_until|interval)|std::thread::sleep' node/src/scanner.rs node/src/scanner_runtime.rs node/src/scanner_ws.rs node/src/scanner_ws_parse.rs node/src/publisher.rs 2>/dev/null | grep -v 'scanner-polling-ok:' || true)
+ if [ -n "$FOUND" ]; then
+ echo "::error::Polling pattern (tokio::time::sleep|sleep_until|interval or std::thread::sleep) detected in event-driven hot paths. See issue #84."
+ echo "$FOUND"
+ exit 1
+ fi
+ echo "Scanner/publisher polling check: OK"
- - name: Build server (all features — the DEV image)
- run: cargo build -p server --all-features
+ - name: Build node (MVP feature set — the DEV + PRD image)
+ run: cargo build -p node
- tests:
- name: Tests
- runs-on: ubuntu-latest
- timeout-minutes: 30
+ - name: Build node (all features — self-host opt-in build)
+ run: cargo build -p node --all-features
+
+ node-tests:
+ name: Node + Shared Tests (M3 Ultra)
+ # Heavy job (~60-90 min on a self-hosted M3 Ultra runner — one of
+ # 6 agents on dfx01 sharing the host's 96 GB / 28 cores). Gated
+ # behind the `ci:full` label so we don't burn runner time on every
+ # speculative PR — apply the label when the PR is ready for the
+ # authoritative test+coverage gate. The Release PR
+ # (`develop -> main`) gets the label applied automatically by
+ # auto-release-pr.yaml. (See `coverage` job below for why the same
+ # guard is repeated there.)
+ if: contains(github.event.pull_request.labels.*.name, 'ci:full')
+ needs: lint-and-build
+ runs-on: [self-hosted, m3-ultra]
+ timeout-minutes: 120
+ env:
+ # Force Esplora broadcasts to fail fast. Some unit tests exercise
+ # the commit pipeline that ends in a real HTTP broadcast; without
+ # this, runs against the public Mutinynet API can take >60 s per
+ # test. Mirrors the pre-push hook.
+ ESPLORA_URL: http://127.0.0.1:1/api
+ # `USERNAME_DOMAIN` is required by the server bootstrap (no
+ # default — see node/src/main.rs and issue #95). The test value
+ # is irrelevant for the `info_returns_*` assertions (they only
+ # check non-empty + shape).
+ USERNAME_DOMAIN: test.zkcoins.local
+ # `PUBLISHER_KEY` is required on every network (no default — see
+ # `node/src/lib.rs`). The previous `1234567890abcdef…` fallback
+ # was a publicly-known test key that drainer bots swept within
+ # minutes of any on-chain top-up; the fallback was removed
+ # network-wide in the "require PUBLISHER_KEY on every network"
+ # hardening. The value below is a syntactically valid 32-byte
+ # hex placeholder (`0000…0001`) chosen so a future grep for the
+ # burned `1234…` key returns empty across the repo + CI config;
+ # it is NOT a secret and MUST NEVER be reused on any chain that
+ # holds value. The same value is hard-coded in the test mocks at
+ # `node/src/router_tests.rs` so the wiremock'd publisher address
+ # path matches the lazy_static-derived `PUBLISHER_ADDRESS`.
+ PUBLISHER_KEY: "0000000000000000000000000000000000000000000000000000000000000001"
+ # `db_tests` use the `testcontainers` crate, which talks to the local
+ # Docker daemon. The self-hosted runner runs Colima (not Docker
+ # Desktop), whose socket lives under the runner user's home
+ # directory. `testcontainers` defaults to `/var/run/docker.sock`,
+ # which does not exist on Colima, so the `Set DOCKER_HOST` step
+ # below points it at the real socket via `$HOME` — same value the
+ # `docker info` step picks up implicitly via the default `docker`
+ # context.
+ # `sccache` wraps `rustc` and caches compiled crates across CI
+ # runs. The M3 Ultra runner agents are self-hosted, so the cache
+ # lives on local disk and survives between jobs — the speedup is
+ # biggest for PR pushes that re-touch the same dependency set.
+ RUSTC_WRAPPER: sccache
+ # Bump cache cap above sccache's 10-GiB default. The cache is
+ # user-level (~/Library/Caches/Mozilla.sccache) and shared by every
+ # m3-ultra agent on the host; with 3+ parallel agents the 10-GiB
+ # default thrashed — writes from one agent evicted hits another
+ # had not consumed yet. 50 GiB fits the current working set with
+ # room to grow; the host has >600 GiB free disk. The server only
+ # reads SCCACHE_CACHE_SIZE at start, so the install step below
+ # restarts it when the running cap differs from this value.
+ SCCACHE_CACHE_SIZE: "50G"
steps:
- name: Checkout
uses: actions/checkout@v4
- - name: Install Rust 1.81.0
- uses: dtolnay/rust-toolchain@master
- with:
- toolchain: "1.81.0"
+ # The launchd-spawned runner agent inherits a minimal PATH that
+ # includes /opt/homebrew/bin (where a stable Rust lives) but not
+ # ~/.cargo/bin (where rustup proxies live). Without this step,
+ # `cargo` resolves to Homebrew's stable cargo, the rust-toolchain
+ # file pinning nightly is ignored, and dependencies that need
+ # `#![feature(...)]` (e.g. plonky2_field) fail to compile. Prepend
+ # ~/.cargo/bin so the rustup proxy is found first and reads the
+ # workspace rust-toolchain.
+ - name: Prepend ~/.cargo/bin to PATH (use rustup proxy, not Homebrew Rust)
+ run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- - name: Cache cargo registry and build
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/registry
- ~/.cargo/git
- target
- key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- restore-keys: |
- ${{ runner.os }}-cargo-
+ # Point `testcontainers` at the Colima socket under the runner
+ # user's home; see the `DOCKER_HOST` comment in the job env block
+ # above. Set in a step (not the static `env:` block) so the path
+ # resolves from `$HOME` at runtime instead of being hard-coded.
+ - name: Set DOCKER_HOST for Colima socket
+ run: echo "DOCKER_HOST=unix://$HOME/.colima/default/docker.sock" >> "$GITHUB_ENV"
+
+ # `sccache` (compile cache) and `cargo-nextest` (test runner) are
+ # installed once per runner via Homebrew. Re-running on a host
+ # where they already exist is a no-op. Start sccache's server
+ # explicitly so the first compile step has a warm cache daemon
+ # and print stats up-front for visibility in the run log.
+ #
+ # If a server is already running with a different cap than the
+ # requested SCCACHE_CACHE_SIZE (e.g. carried over from a previous
+ # workflow version), stop it so the next --start-server picks up
+ # the new env value. The on-disk cache files survive the restart.
+ - name: Ensure sccache + cargo-nextest are installed
+ run: |
+ command -v sccache >/dev/null || brew install sccache
+ command -v cargo-nextest >/dev/null || brew install cargo-nextest
+ if ! sccache --show-stats 2>/dev/null | grep -qE "Max cache size +50 GiB"; then
+ sccache --stop-server >/dev/null 2>&1 || true
+ fi
+ sccache --start-server >/dev/null 2>&1 || true
+ sccache --show-stats
+
+ # The `db_tests` added in PR-A1 use testcontainers to spin up a
+ # real Postgres 17 per test. The runner host has Docker (via
+ # Colima) available on PATH; fail fast with a readable error
+ # if it ever goes away, instead of letting the test suite die
+ # 5 minutes into the run with a hard-to-read bollard error.
+ - name: Verify Docker is reachable (testcontainers dependency)
+ run: docker info > /dev/null
- # `--test-threads=1` is mandatory: multiple test binaries each load the
- # SP1 mock prover ELF (~1.5 GB resident) and running them in parallel
- # on a 7 GB GitHub-hosted runner OOM-kills the job (exit 143). The
- # account_server::tests group runs the real SP1 prover and is skipped
- # here — it is only exercised in the coverage job, which is also
- # single-threaded.
- - name: Run tests (server + shared, all features, skip slow SP1 prover tests)
- run: cargo test -p server -p shared --all-features -- --test-threads=1 --skip account_server::tests
+ # `cargo nextest` replaces `cargo test`: process-per-test isolation
+ # plus smart scheduling (slow tests start first). `--test-threads 1`
+ # is preserved — the repo invariant is that tests run serially to
+ # avoid testcontainers port races and shared-state pollution.
+ # `api_remote` is the live-DEV-server verification integration test
+ # (node/tests/api_remote.rs). It targets `https://dev-api.zkcoins.app`
+ # by default and is meant to run AFTER a deploy, from the `api-e2e`
+ # job in deploy-dev.yaml — not against whatever DEV currently runs
+ # while a PR is still open. Excluding it here keeps `node-tests`
+ # hermetic: only unit + non-remote integration tests run; remote
+ # verification fires post-deploy as the merge-then-deploy gate.
+ - name: Run node + shared tests (release, all features)
+ run: cargo nextest run -p node -p shared --release --all-features --test-threads 1 -E 'not binary(api_remote)'
- - name: Run tests (program lib)
- run: cargo test -p zkcoins-program --lib -- --test-threads=1
+ - name: sccache stats (post-build)
+ if: always()
+ run: sccache --show-stats
coverage:
- name: Coverage (MVP scope)
- runs-on: ubuntu-latest
- timeout-minutes: 30
+ name: Coverage Gate (100% lines + functions)
+ # Runs in parallel with `node-tests` (not after) — both jobs
+ # exercise the same suite (nextest vs. nextest-under-llvm-cov), so
+ # serializing them only doubled wall-clock on every Release PR.
+ # The `ci:full` label gate is duplicated explicitly here because the
+ # chain through `node-tests` (which carried the guard) is broken.
+ if: contains(github.event.pull_request.labels.*.name, 'ci:full')
+ needs: lint-and-build
+ runs-on: [self-hosted, m3-ultra]
+ timeout-minutes: 90
+ env:
+ ESPLORA_URL: http://127.0.0.1:1/api
+ USERNAME_DOMAIN: test.zkcoins.local
+ # `PUBLISHER_KEY` is required on every network (no default — see
+ # `node/src/lib.rs`); the value mirrors `node-tests` above and is
+ # a syntactically valid 32-byte hex placeholder, NOT a secret.
+ # MUST match `node/src/router_tests.rs` and the `node-tests` env
+ # block — the test mocks derive the wiremock'd publisher address
+ # from this key.
+ PUBLISHER_KEY: "0000000000000000000000000000000000000000000000000000000000000001"
+ # `db_tests` use the `testcontainers` crate; see `node-tests`
+ # above for the rationale. `DOCKER_HOST` is set in a step below
+ # so the Colima socket path resolves from `$HOME` at runtime.
+ # Same sccache wrapper as `node-tests`; reuses the same on-disk
+ # cache populated by the previous job in the same workflow run.
+ RUSTC_WRAPPER: sccache
+ # See `node-tests` env block above for the 50-GiB rationale.
+ SCCACHE_CACHE_SIZE: "50G"
steps:
- name: Checkout
uses: actions/checkout@v4
- - name: Install Rust 1.81.0
- uses: dtolnay/rust-toolchain@master
- with:
- toolchain: "1.81.0"
- components: llvm-tools-preview
+ - name: Prepend ~/.cargo/bin to PATH (use rustup proxy, not Homebrew Rust)
+ run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- - name: Cache cargo registry and build
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/registry
- ~/.cargo/git
- target
- key: ${{ runner.os }}-cargo-llvm-cov-${{ hashFiles('**/Cargo.lock') }}
- restore-keys: |
- ${{ runner.os }}-cargo-llvm-cov-
+ # See `node-tests` job above for the rationale; resolves the
+ # Colima socket path from `$HOME` at runtime.
+ - name: Set DOCKER_HOST for Colima socket
+ run: echo "DOCKER_HOST=unix://$HOME/.colima/default/docker.sock" >> "$GITHUB_ENV"
- - name: Install cargo-llvm-cov
- uses: taiki-e/install-action@v2
- with:
- tool: cargo-llvm-cov
+ # Same install gate as `node-tests`. Idempotent: no-op on a
+ # warm runner where both tools already exist. See `node-tests`
+ # for why we conditionally restart the sccache server.
+ - name: Ensure sccache + cargo-nextest are installed
+ run: |
+ command -v sccache >/dev/null || brew install sccache
+ command -v cargo-nextest >/dev/null || brew install cargo-nextest
+ if ! sccache --show-stats 2>/dev/null | grep -qE "Max cache size +50 GiB"; then
+ sccache --stop-server >/dev/null 2>&1 || true
+ fi
+ sccache --start-server >/dev/null 2>&1 || true
+ sccache --show-stats
+
+ # Coverage runs the same `db_tests` as `node-tests` and so
+ # needs Docker reachable for testcontainers. See the matching
+ # check in the `node-tests` job for the rationale.
+ - name: Verify Docker is reachable (testcontainers dependency)
+ run: docker info > /dev/null
- # Coverage is measured on the MVP build only: no Cargo features
- # enabled. Code behind a Cargo feature (address-list / faucet /
- # usernames / lnurl) is excluded from the binary at compile time
- # and is therefore not part of the measured surface.
+ # `cargo llvm-cov nextest` is the nextest-aware coverage subcommand:
+ # collects llvm-cov data while driving the suite through nextest,
+ # so the 100% line/function gate and the test execution share a
+ # single binary run (same as the old `cargo llvm-cov -- ...` form).
#
- # main.rs (runtime bootstrap) and publisher.rs (Bitcoin commit /
- # reveal broadcasting that needs a signet/regtest node) are
- # genuinely not exercisable in unit tests and are excluded at the
- # file level via --ignore-filename-regex.
- # Threshold is the current MVP baseline with main.rs (bootstrap)
- # and publisher.rs (Bitcoin commit/reveal broadcasting that needs a
- # signet/regtest node) excluded. The goal is 100% on this scope;
- # each lifting PR ratchets the threshold upward.
- # All tests must run for the coverage measurement to reflect the
- # true exercised production surface — account_server tests are slow
- # under SP1=mock but exercise large parts of the file.
- - name: Run cargo-llvm-cov (MVP scope, regression guard)
+ # The `api_remote` integration test (node/tests/api_remote.rs)
+ # is excluded for the same reason as in `node-tests` above: it
+ # targets the live DEV server and belongs in the post-deploy
+ # `api-e2e` job, not the hermetic coverage gate. The MVP coverage
+ # scope is measured by the rest of the suite, which covers the
+ # in-process axum handlers via oneshot().
+ - name: Run llvm-cov (MVP scope, 100% line + function gate)
run: |
- cargo llvm-cov -p server --show-missing-lines \
- --ignore-filename-regex 'main\.rs|publisher\.rs|server_runtime\.rs|scanner_runtime\.rs|_tests\.rs$' \
+ cargo llvm-cov nextest --release -p node --show-missing-lines \
+ --ignore-filename-regex 'main\.rs|lib\.rs|publisher\.rs|runtime\.rs|scanner_runtime\.rs|scanner_ws\.rs|_tests\.rs$|bin/.*\.rs$' \
--fail-under-lines 100 \
--fail-under-functions 100 \
- -- --test-threads=1
+ --test-threads 1 \
+ -E 'not binary(api_remote)'
+
+ - name: sccache stats (post-build)
+ if: always()
+ run: sccache --show-stats
+
+ # Telegram alert on workflow failure. Modelled as a separate job (not
+ # an inline step) so job-level failures — timeout, OOM, runner crash —
+ # still fire the alert. `if: failure()` evaluates against the whole
+ # `needs:` group: any listed job transitioning to `failure` triggers
+ # it, while skipped jobs (node-tests / coverage on a non-ci:full PR,
+ # or all jobs on a draft PR) and manual cancellation stay silent.
+ notify-failure:
+ name: Telegram alert on failure
+ needs: [lint-and-build, node-tests, coverage]
+ if: failure()
+ runs-on: ubuntu-latest
+ steps:
+ - name: Send Telegram alert
+ env:
+ TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
+ TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
+ run: |
+ TEXT=$'❌ '"${{ github.workflow }}"$' failed\nRepo: '"${{ github.repository }}"$'\nBranch: '"${{ github.ref_name }}"$'\nRun: '"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+ curl -sS -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
+ --data-urlencode "chat_id=${TG_CHAT}" \
+ --data-urlencode "text=${TEXT}" \
+ -d "parse_mode=HTML" \
+ -d "disable_web_page_preview=true"
diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml
index d0c772f6..12224d04 100644
--- a/.github/workflows/deploy-dev.yaml
+++ b/.github/workflows/deploy-dev.yaml
@@ -11,8 +11,20 @@ on:
type: boolean
default: false
+# Serialize DEV deploys per branch. Multiple develop pushes in quick
+# succession (e.g. three PRs merged back-to-back) used to fire three
+# parallel deploys that raced on `docker compose recreate` on the
+# host and left the zkcoins-node container half-renamed in
+# `Created` state, blocking the next `up -d` with a name conflict.
+# `cancel-in-progress: true` keeps the newest commit's deploy; the
+# older deploy is irrelevant the moment its commit is no longer the
+# branch tip.
+concurrency:
+ group: deploy-dev
+ cancel-in-progress: true
+
env:
- DOCKER_TAGS: zkcoin/server:beta
+ DOCKER_TAGS: zkcoins/node:beta
permissions:
contents: read
@@ -41,8 +53,18 @@ jobs:
push: true
tags: ${{ env.DOCKER_TAGS }}
platforms: linux/arm64
- build-args: |
- FEATURES=address-list,faucet,usernames,lnurl
+ # Registry-backed buildx cache. Same `zkcoins/node:buildcache`
+ # tag is reused by Deploy PRD — DEV and PRD compile the same
+ # Rust workspace so cache hits cross-deploy. `type=registry`
+ # over `type=gha` because GHA cache caps at 10 GB with LRU
+ # eviction; Docker Hub holds the tag indefinitely.
+ # Caveat: DEV's `cancel-in-progress: true` (above) can interrupt
+ # a concurrent DEV deploy mid-push to the cache manifest.
+ # BuildKit's `cache-from` tolerates partial manifests (falls back
+ # to a from-scratch build with a warning) so the race is
+ # self-healing on the next deploy.
+ cache-from: type=registry,ref=zkcoins/node:buildcache
+ cache-to: type=registry,ref=zkcoins/node:buildcache,mode=max
- name: Install cloudflared
run: |
@@ -56,12 +78,167 @@ jobs:
chmod 600 ~/.ssh/deploy_key
echo "${{ secrets.DEPLOY_DEV_SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- DEPLOY_CMD="zkcoins-server"
+ # The deploy host runs a forced-command restricted shell that only
+ # accepts whitelisted command names — arbitrary inline shell is
+ # rejected. Both branches must resolve to a single allowlisted
+ # command; the reset variant is implemented host-side.
+ DEPLOY_CMD="zkcoins-node"
if [ "${{ inputs.reset_state }}" == "true" ]; then
- DEPLOY_CMD="cd ~/zkcoins && docker compose stop zkcoins-server && docker compose rm -f zkcoins-server && docker volume rm zkcoins_server-data 2>/dev/null; zkcoins-server"
+ DEPLOY_CMD="reset-zkcoins-node"
fi
+ # ServerAlive* keep the session alive across long-running
+ # `docker compose recreate` steps where the remote command
+ # produces no stdout for >60s. Without keepalive the
+ # cloudflared tunnel (and the OpenSSH client) drop the
+ # session and exit 255 even though the host-side deploy
+ # script keeps running — observed on the PR #111 merge
+ # (run 26419696840). 30s interval × 8 retries = 4 min of
+ # network silence tolerated before the session is killed.
ssh -i ~/.ssh/deploy_key \
+ -o ServerAliveInterval=30 \
+ -o ServerAliveCountMax=8 \
-o ProxyCommand="cloudflared access ssh --hostname ${{ secrets.DEPLOY_DEV_HOST }}" \
${{ secrets.DEPLOY_DEV_USER }}@${{ secrets.DEPLOY_DEV_HOST }} \
"$DEPLOY_CMD"
+
+ # Post-deploy smoke test: hit the public endpoint until /api/info
+ # answers 200 or we give up. A green "Build and deploy to DEV"
+ # without this step was historically misleading — a runtime-bootstrap
+ # panic left the container Up-but-unresponsive while the workflow
+ # reported success. Failing this step blocks the auto-release PR
+ # from collecting a green check and surfaces the regression in CI.
+ - name: Smoke test public endpoint
+ run: |
+ set -euo pipefail
+ URL="https://dev-api.zkcoins.app/api/info"
+ for i in $(seq 1 30); do
+ code=$(curl -sS -o /tmp/info.json -w '%{http_code}' --max-time 10 "$URL" || echo "000")
+ if [ "$code" = "200" ]; then
+ echo "DEV /api/info responded 200 after ${i} attempt(s):"
+ cat /tmp/info.json
+ echo
+ exit 0
+ fi
+ echo "[$i/30] $URL -> ${code} (waiting 10 s)"
+ sleep 10
+ done
+ echo "::error::DEV /api/info never returned 200 within ~5 min after deploy"
+ exit 1
+
+ # Functional verification of the deployed DEV server.
+ #
+ # The smoke test in `build-and-deploy` only proves the HTTP listener
+ # is bound; this job exercises all 15 routes end-to-end (read-only,
+ # negative-path, full mint→send→commit and username-claim roundtrips
+ # against the live server). Runs on the same self-hosted M3 Ultra
+ # runner as `node-tests` / `coverage`, so sccache hits the warm
+ # cache populated by previous runs and the build itself stays
+ # well under a minute on a hot cache.
+ api-e2e:
+ name: API E2E against DEV
+ needs: build-and-deploy
+ runs-on: [self-hosted, m3-ultra]
+ timeout-minutes: 30
+ env:
+ RUSTC_WRAPPER: sccache
+ ZKCOINS_API_URL: https://dev-api.zkcoins.app
+ # The bootstrap `lazy_static`s panic if these are unset; the
+ # integration test only talks to the deployed server but the
+ # lib's panic-on-load behaviour is unconditional. Values are
+ # placeholders — nothing in the test path reads them.
+ USERNAME_DOMAIN: dev.zkcoins.app
+ ESPLORA_URL: http://127.0.0.1:1/api
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ # Self-hosted runner inherits a minimal PATH that hides rustup;
+ # see the matching step in `node-tests` for the rationale.
+ - name: Prepend ~/.cargo/bin to PATH (use rustup proxy, not Homebrew Rust)
+ run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
+
+ - name: Ensure sccache + cargo-nextest are installed
+ run: |
+ command -v sccache >/dev/null || brew install sccache
+ command -v cargo-nextest >/dev/null || brew install cargo-nextest
+ sccache --start-server >/dev/null 2>&1 || true
+ sccache --show-stats
+
+ # Operational preflight: hit /health/ready and /health/publisher
+ # BEFORE running the API E2E suite, so an empty publisher wallet
+ # or a non-ready DB fails THIS step with a clear "top up the
+ # publisher" / "DB not ready" message instead of cascading
+ # through the test suite as opaque 503s.
+ #
+ # Historically a green E2E run masked an empty publisher wallet
+ # because the suite silently dev_skip!()'d 5xx errors; PR
+ # "test: harden suite" (this PR) removed the masking and added
+ # this preflight as the load-bearing operational gate.
+ #
+ # 50_000 sats is a conservative floor: a single inscription
+ # commit + reveal pair at typical Mutinynet fee rates needs
+ # ~1_500 sats; 50_000 buys ~30 mints before the next top-up.
+ # Adjust upward if the suite grows.
+ - name: Ensure jq is installed (preflight dependency)
+ run: command -v jq >/dev/null || brew install jq
+
+ - name: Preflight — publisher wallet has UTXOs
+ env:
+ DEV_API: https://dev-api.zkcoins.app
+ run: |
+ set -euo pipefail
+ ready=$(curl -sS --max-time 10 "$DEV_API/health/ready")
+ if ! echo "$ready" | jq -e '.ready == true' > /dev/null; then
+ echo "::error::/health/ready not ready: $ready"
+ exit 1
+ fi
+ pub=$(curl -sS --max-time 15 -w '|%{http_code}' "$DEV_API/health/publisher")
+ code="${pub##*|}"
+ body="${pub%|*}"
+ if [ "$code" != "200" ]; then
+ echo "::error::/health/publisher returned $code: $body"
+ exit 1
+ fi
+ utxos=$(echo "$body" | jq -r '.utxo_count')
+ sats=$(echo "$body" | jq -r '.total_sats')
+ if [ "$utxos" -lt 1 ] || [ "$sats" -lt 50000 ]; then
+ echo "::error::publisher wallet too low (utxos=$utxos, sats=$sats) — top up before re-running"
+ exit 1
+ fi
+ echo "publisher OK: utxos=$utxos, sats=$sats"
+
+ - name: Run API E2E suite against DEV
+ env:
+ # DEV image is MVP-only by policy (see Dockerfile FEATURES
+ # arg); the gated address-list/lnurl tests skip cleanly
+ # instead of panicking the CI canary.
+ ZKCOINS_E2E_ALLOW_FEATURE_TRIMMED_SERVER: "true"
+ run: cargo test -p node --release --all-features --test api_remote -- --test-threads=1 --nocapture
+
+ - name: sccache stats (post-build)
+ if: always()
+ run: sccache --show-stats
+
+ # Telegram alert on workflow failure. Separate job (not an inline step)
+ # so job-level failures — timeout, OOM, runner crash — still fire the
+ # alert; runs on the cheapest runner since the curl never needs to touch
+ # the self-hosted M3 Ultra. See ci.yaml > notify-failure for the
+ # firing-matrix rationale.
+ notify-failure:
+ name: Telegram alert on failure
+ needs: [build-and-deploy, api-e2e]
+ if: failure()
+ runs-on: ubuntu-latest
+ steps:
+ - name: Send Telegram alert
+ env:
+ TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
+ TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
+ run: |
+ TEXT=$'❌ '"${{ github.workflow }}"$' failed\nRepo: '"${{ github.repository }}"$'\nBranch: '"${{ github.ref_name }}"$'\nRun: '"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+ curl -sS -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
+ --data-urlencode "chat_id=${TG_CHAT}" \
+ --data-urlencode "text=${TEXT}" \
+ -d "parse_mode=HTML" \
+ -d "disable_web_page_preview=true"
diff --git a/.github/workflows/deploy-prd.yaml b/.github/workflows/deploy-prd.yaml
index d23ee45e..8af19ffe 100644
--- a/.github/workflows/deploy-prd.yaml
+++ b/.github/workflows/deploy-prd.yaml
@@ -5,8 +5,17 @@ on:
branches: [main]
workflow_dispatch:
+# Serialize PRD deploys. Unlike Deploy DEV (cancel-in-progress: true,
+# "newest commit wins") production deploys must NEVER be killed mid-
+# flight: cancelling halfway through `docker compose recreate` is
+# exactly what produced the half-renamed Created-state container
+# that took DEV down. Queue subsequent deploys instead.
+concurrency:
+ group: deploy-prd
+ cancel-in-progress: false
+
env:
- DOCKER_TAGS: zkcoin/server:latest
+ DOCKER_TAGS: zkcoins/node:latest
permissions:
contents: read
@@ -35,6 +44,13 @@ jobs:
push: true
tags: ${{ env.DOCKER_TAGS }}
platforms: linux/arm64
+ # Registry-backed buildx cache. Same `zkcoins/node:buildcache`
+ # tag is shared with Deploy DEV — DEV and PRD compile the same
+ # Rust workspace so cache hits cross-deploy. `type=registry`
+ # over `type=gha` because GHA cache caps at 10 GB with LRU
+ # eviction; Docker Hub holds the tag indefinitely.
+ cache-from: type=registry,ref=zkcoins/node:buildcache
+ cache-to: type=registry,ref=zkcoins/node:buildcache,mode=max
- name: Install cloudflared
run: |
@@ -47,7 +63,103 @@ jobs:
echo "${{ secrets.DEPLOY_PRD_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
echo "${{ secrets.DEPLOY_PRD_SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
+ # ServerAlive* keep the session alive across long-running
+ # `docker compose recreate` steps where the remote command
+ # produces no stdout for >60s. Mirrors deploy-dev.yaml; see
+ # the comment there for the failure mode that motivated this
+ # (PR #111 merge run 26419696840 — SSH dropped mid-recreate,
+ # exit 255, container actually came up server-side).
ssh -i ~/.ssh/deploy_key \
+ -o ServerAliveInterval=30 \
+ -o ServerAliveCountMax=8 \
-o ProxyCommand="cloudflared access ssh --hostname ${{ secrets.DEPLOY_PRD_HOST }}" \
${{ secrets.DEPLOY_PRD_USER }}@${{ secrets.DEPLOY_PRD_HOST }} \
- "zkcoins-server"
+ "zkcoins-node"
+
+ # Post-deploy smoke test: hit the public PRD endpoint until
+ # /api/info answers 200 or we give up. Mirrors the Deploy DEV
+ # post-deploy probe. Without this a runtime-bootstrap panic
+ # leaves the container Up-but-unresponsive on PRD while the
+ # workflow reports success — the exact failure mode that took
+ # DEV down silently before the Plonky2 migration fix.
+ - name: Smoke test public PRD endpoint
+ run: |
+ set -euo pipefail
+ URL="https://api.zkcoins.app/api/info"
+ for i in $(seq 1 30); do
+ code=$(curl -sS -o /tmp/info.json -w '%{http_code}' --max-time 10 "$URL" || echo "000")
+ if [ "$code" = "200" ]; then
+ echo "PRD /api/info responded 200 after ${i} attempt(s):"
+ cat /tmp/info.json
+ echo
+ exit 0
+ fi
+ echo "[$i/30] $URL -> ${code} (waiting 10 s)"
+ sleep 10
+ done
+ echo "::error::PRD /api/info never returned 200 within ~5 min after deploy"
+ exit 1
+
+ # Functional verification of the deployed PRD server. Mirrors the
+ # Deploy DEV api-e2e job, but excludes the three roundtrip tests —
+ # they would consume real publisher UTXOs and write coins into the
+ # production SMT/MMR. `--skip _roundtrip_` is a substring match; the
+ # only test names matching are the three mint/send-commit/username
+ # roundtrips (verified via grep against the suite).
+ api-e2e:
+ name: API E2E against PRD (non-mutating subset)
+ needs: build-and-deploy
+ runs-on: [self-hosted, m3-ultra]
+ timeout-minutes: 30
+ env:
+ RUSTC_WRAPPER: sccache
+ ZKCOINS_API_URL: https://api.zkcoins.app
+ # The bootstrap `lazy_static`s panic if these are unset; the
+ # integration test only talks to the deployed server but the
+ # lib's panic-on-load behaviour is unconditional. Values are
+ # placeholders — nothing in the read-only test path reads them.
+ USERNAME_DOMAIN: zkcoins.app
+ ESPLORA_URL: http://127.0.0.1:1/api
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Prepend ~/.cargo/bin to PATH (use rustup proxy, not Homebrew Rust)
+ run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
+
+ - name: Ensure sccache + cargo-nextest are installed
+ run: |
+ command -v sccache >/dev/null || brew install sccache
+ command -v cargo-nextest >/dev/null || brew install cargo-nextest
+ sccache --start-server >/dev/null 2>&1 || true
+ sccache --show-stats
+
+ - name: Run API E2E suite against PRD (skip roundtrips)
+ run: cargo test -p node --release --all-features --test api_remote -- --test-threads=1 --nocapture --skip _roundtrip_
+
+ - name: sccache stats (post-build)
+ if: always()
+ run: sccache --show-stats
+
+ # Telegram alert on workflow failure. Separate job (not an inline step)
+ # so job-level failures — timeout, OOM, runner crash — still fire the
+ # alert; runs on the cheapest runner since the curl never needs to touch
+ # the self-hosted M3 Ultra. See ci.yaml > notify-failure for the
+ # firing-matrix rationale.
+ notify-failure:
+ name: Telegram alert on failure
+ needs: [build-and-deploy, api-e2e]
+ if: failure()
+ runs-on: ubuntu-latest
+ steps:
+ - name: Send Telegram alert
+ env:
+ TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
+ TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
+ run: |
+ TEXT=$'❌ '"${{ github.workflow }}"$' failed\nRepo: '"${{ github.repository }}"$'\nBranch: '"${{ github.ref_name }}"$'\nRun: '"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+ curl -sS -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
+ --data-urlencode "chat_id=${TG_CHAT}" \
+ --data-urlencode "text=${TEXT}" \
+ -d "parse_mode=HTML" \
+ -d "disable_web_page_preview=true"
diff --git a/.gitignore b/.gitignore
index 79ec20af..07d8e1cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@ target/
.env
*.pem
*.bin
-!server/minting_secret.bin
+!node/minting_secret.bin
.DS_Store
# accidentally-tracked tmp file
diff --git a/ARKADE_INTEGRATION.md b/ARKADE_INTEGRATION.md
new file mode 100644
index 00000000..d41207db
--- /dev/null
+++ b/ARKADE_INTEGRATION.md
@@ -0,0 +1,1114 @@
+# Arkade × zkCoins Integration — Design Document
+
+**Status:** Design draft. No code yet. Companion to
+[`SPEC.md`](./SPEC.md), [`MULTI_ASSET.md`](./MULTI_ASSET.md),
+[`BRIDGE_MVP.md`](./BRIDGE_MVP.md),
+[`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md), and
+[`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md).
+
+**Authoritative source for:** how Arkade (Ark protocol) and zkCoins
+(Shielded CSV protocol) compose; which integration paths are
+realistic on which horizons; the canonical Arkade ↔ zkCoins atomic-swap
+construction.
+
+**Audience:** Engineers and architects evaluating cross-protocol
+integration with Arkade. Presupposes [`SPEC.md`](./SPEC.md), the
+swap-design pattern in [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md),
+the bridge model in [`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md), and the
+multi-asset extension in [`MULTI_ASSET.md`](./MULTI_ASSET.md). Familiarity
+with the Ark litepaper (Argentieri, Avarikioti, Camilleri, Keer,
+Maffei — Ark Labs / TU Wien) and the Shielded CSV ePrint 2025/068
+(Nick, Eagen, Linus) is assumed.
+
+---
+
+## 0. Status
+
+Design draft only. The project today has no Arkade integration —
+zkCoins runs as documented in [`SPEC.md`](./SPEC.md); Arkade runs as
+documented at `docs.arkadeos.com`. The two systems coexist on Bitcoin
+L1 without interaction.
+
+[`MULTI_ASSET.md`](./MULTI_ASSET.md) §12.9 names cross-asset trading as
+out-of-protocol and points to the BitVM2 bridge
+([`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md)) and the Lightning atomic-swap
+layer ([`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md)) as the
+"canonical out-of-protocol paths." This document adds the **third** such
+path — Ark/Arkade — and analyses where the integration is real
+engineering, where it is research, and where it is wiring.
+
+This is not an implementation spec. It is an architectural map.
+Implementation specs for individual integration paths (e.g., the HTLC
+atomic swap of §7) live in follow-up documents once a path is locked
+in the ROADMAP.
+
+---
+
+## 1. Scope
+
+This document covers:
+
+- Protocol-mechanics comparison between Arkade VTXOs and zkCoins
+ coins (§5).
+- Six integration paths, arranged by maturity (§6).
+- The canonical HTLC atomic-swap construction between an Arkade VTXO
+ and a zkCoins shared account, with full protocol steps and
+ failure-mode analysis (§7).
+- Pipeline use — BTC onboarding via Arkade boarding, transacting
+ inside zkCoins, exit via Arkade settlement (§6.3).
+- Bridge convergence — sharing federation infrastructure between the
+ zkCoins BitVM2 bridge and an Arkade operator (§6.4).
+- Confidential VTXOs as open research (§6.5).
+- Cross-asset DEX (Arkade Assets ↔ zkCoins Assets) as the first
+ Bitcoin-native cross-protocol multi-asset swap (§6.6).
+- Trust-model stacking analysis (§8).
+- Honest 6-month / 2-year / research-only assessment (§9).
+
+It does **not** cover:
+
+- Modifications to the zkCoins protocol or circuit. None of the
+ integration paths in this document require a divergence from
+ [`SPEC.md`](./SPEC.md) §15.
+- Modifications to the Ark protocol. The HTLC atomic-swap path uses
+ Arkade Script primitives that already ship in `arkade-os/compiler`.
+- Implementation in any specific code base. Once a path is locked,
+ its implementation spec is a separate sibling document (mirroring
+ the relationship of [`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md) to
+ [`BRIDGE_MVP.md`](./BRIDGE_MVP.md)).
+- Generic cross-chain bridges (Liquid, RSK, sidechains). Different
+ trust model, different document.
+
+---
+
+## 2. Executive Summary
+
+The most realistic short-term Arkade × zkCoins integration is a
+**trustless HTLC atomic swap** between an Arkade VTXO and a zkCoins
+2-of-2 shared account. The construction is a direct adaptation of
+the Shielded CSV §A.1.2 atomic-swap pattern (also the basis of
+[`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md)) with the
+Bitcoin/Lightning side replaced by an Arkade VTXO carrying an
+HTLC script-path. Arkade's compiler ships HTLC as a built-in primitive.
+Both halves of the construction exist today; what is missing is
+wiring.
+
+Three structural facts shape every other path in this document:
+
+1. **Arkade is a Bitcoin-script L2.** A VTXO *is* a presigned
+ Bitcoin output with a Taproot lock; only the broadcasting
+ is deferred (Ark §4 Definition 4.1). Any Bitcoin-script
+ construction — HTLC, escrow, DLC, payment channel — composes
+ onto a VTXO with the single constraint that timelocks must
+ fit inside the batch expiry `T_e` (Ark §6).
+2. **Shielded CSV is not L2 in the same sense.** A zkCoins coin
+ has no script, no on-chain UTXO, no spending condition beyond
+ `coin.recipient == self.owner` (Shielded CSV §4.2;
+ `program/src/lib.rs::apply_coin`). The chain stores only
+ 64-byte aggregate nullifiers as an availability bulletin
+ board. Atomicity cannot live on the coin layer — this is
+ load-bearing for the protocol's "64 bytes per tx" property
+ and locked at [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) §5.
+3. **The two protocols share an institutional orbit but no
+ documented unified roadmap.** Robin Linus, Liam Eagen, Jonas
+ Nick (Shielded CSV authors) and Zeta Avarikioti, Matteo Maffei
+ (Ark co-authors) overlap on adjacent work — BitVM, Glock, Argo —
+ but neither paper mentions the other. Integration is implicit
+ in the personnel, not declared in the literature. Frame
+ accordingly in §9.
+
+The combined stack inherits the union of both protocols' trust
+assumptions. Today: Arkade rational-operator + zkCoins federation
+(Phase 1). 2026-2028 horizon: Arkade multi-operator + zkCoins BitVM2
+bridge (Phase 2). Neither protocol's headline trust-minimisation is
+production yet; the combined stack is bottlenecked on whichever
+reaches its Phase 2 last.
+
+---
+
+## 3. Decisions (locked)
+
+The decisions below are fixed for this design document. Reversing
+any of them is a design-level rethink, not a tweak.
+
+| # | Decision | Consequence |
+| - | -------- | ----------- |
+| **A1** | **First integration target is the HTLC atomic swap** (§6.2, §7). Hash-Time-Locked Contract preimage swap between an Arkade VTXO and a zkCoins 2-of-2 shared account. | This is the smallest construction that demonstrably uses both protocols for what they are good at, requires no new cryptography, and inherits independent trust assumptions in each leg. Pipeline use (§6.3) is a wallet-side convenience on top; it does not need its own primitive. |
+| **A2** | **No protocol changes to zkCoins or Arkade for A1.** The atomic-swap construction uses primitives both papers already specify: Shielded CSV §5.1 (shared accounts), §A.1.1 (time-locked nullifiers), §A.1.2 (atomic swap); Arkade Script HTLC template (`arkade-os/compiler`, `docs.arkadeos.com/learn/smart-contracts/hash-time-locked-contract`). | No 12th divergence to track in [`SPEC.md`](./SPEC.md) §15. No deviation from the Ark whitepaper. The integration adds wiring, not protocol changes. |
+| **A3** | **Arkade operator and zkCoins federation remain independent trust domains.** A user holding a VTXO trusts the Arkade operator's rationality (Ark §5 Table 1). A user holding a zkCoins coin pegged to BTC trusts the zkCoins bridge (Phase 1 federation or Phase 2 BitVM2 setup). The two assumptions do not collapse into one; an atomic-swap counterparty may simultaneously occupy both roles, but the trust analyses stay separate. | Operating both an Arkade `arkd` instance and a zkCoins bridge node in the same datacentre is permitted; the security argument tracks each role independently. §8 is the canonical reference for which assumption applies where. |
+| **A4** | **No confidential-VTXO work in the integration roadmap.** Bringing ZK privacy to Arkade VTXOs (§6.5) is genuine open research — Pedersen commitments + range proofs + redesigned forfeit mechanism + a PCD-style ZK validity proof per Arkade batch. Estimated 1–2 year paper-stage work; no existing protocol or implementation. | This document records confidential VTXOs as a research direction worth tracking but explicitly out-of-scope for any near-term zkCoins effort. If Arkade ships such a feature upstream, this section becomes a re-evaluation gate. |
+| **A5** | **Pipeline use (§6.3) is layered on top of A1, not a separate primitive.** "BTC → Arkade → zkCoins → Arkade → BTC" decomposes into: Arkade boarding (Ark §4.5), an HTLC swap into zkCoins (A1), zkCoins-internal transfers, an HTLC swap back out, Arkade exit. Each step is independently specified and the pipeline composes them. | No new design work for the pipeline as long as A1 lands. The wallet-side UX of routing a user through the pipeline is `zk-coins/app` work, not a server-side primitive. |
+| **A6** | **Cross-asset DEX (§6.6) is a v2 follow-up to A1.** A swap between an Arkade Asset (Arkade Labs' native-asset proposal) and a zkCoins asset is structurally identical to A1 with two field substitutions on each side. It does not require new crypto, but it does require the zkCoins multi-asset shared-account semantics from [`MULTI_ASSET.md`](./MULTI_ASSET.md) to be live, and Arkade Assets to be in production beyond beta. | Tracked as a v2 milestone; not in the initial A1 implementation scope. The first integration ships before chasing this. |
+
+These mirror the lockedness pattern of [`MULTI_ASSET.md`](./MULTI_ASSET.md) §2
+(decisions M1–M6) and [`BRIDGE_MVP.md`](./BRIDGE_MVP.md) §3 (Bridge
+locked technical decisions). Each is testable to the extent the
+integration is built; today most are documentation-level decisions
+that fix the design space.
+
+---
+
+## 4. Glossary additions
+
+Extends [`SPEC.md`](./SPEC.md) § Glossary and
+[`MULTI_ASSET.md`](./MULTI_ASSET.md) § Glossary additions.
+
+| Term | Expansion | Meaning |
+| ---- | --------- | ------- |
+| **VTXO** | Virtual UTXO | Ark's atomic ownership unit: a presigned Bitcoin tx output `(value, vtxoLockScript)` held off-chain by a VTXO holder, encumbered by a Taproot script with at least one collaborative path (`checkSig(pkO ⊕ pkA)`, user + operator MuSig2) and one unilateral exit path (`checkSig(pkA) ∧ relTimelock(t_v)`). Ark §4 Definition 4.1. |
+| **Arkade operator** | — | The coordinating party in an Ark instance. Provides liquidity (its own BTC funds commitments), batches user activity into `commitment_tx`, cosigns Ark transactions and VTXT virtual transactions. Single operator per Arkade instance today (Ark §7). |
+| **`commitment_tx`** | Commitment transaction | The single on-chain Bitcoin tx per Arkade batch that anchors a `batch` Taproot output (sweep path after `T_e`, unroll path enforcing the VTXT) and a `connector` Taproot output for the chain of anchor outputs used by forfeit transactions. Ark §4.4, Definition 4.9. |
+| **`forfeit_tx`** | Forfeit transaction | Ark batch-swap atomicity primitive: user-signed transaction with SIGHASH_ALL over `(old_vtxo, connector_anchor_ε)`, valid only if the `commitment_tx` containing the connector confirms. Lets the operator claim the old VTXO if the user double-spends. Ark §4.3, Transaction 4. |
+| **Batch expiry `T_e`** | — | Ark batch expiration time. After `T_e` the operator may sweep the batch output. Every script-level construction inside a VTXO (HTLC, escrow, DLC, channel) must use timelocks strictly shorter than `T_e` for the cooperative spending path to remain usable. Ark §6 caveat. |
+| **Arkade Script** | — | High-level language ([`arkade-os/compiler`](https://github.com/arkade-os/compiler)) compiling to an extended Bitcoin Script targeting Arkade VM. Supports `checkSig`, `checkMultiSig`, `sha256` preimage check, CLTV / CSV, transaction introspection, and automatic generation of cooperative + unilateral exit script paths. Ships HTLC, Escrow, Spilman channel, Dryja-Poon channel, Lightning channel/swap templates. |
+| **Arkade Asset** | — | Arkade Labs' native-asset proposal for issuing non-BTC tokens on Bitcoin via Ark batching. Encoded as TLV in `OP_RETURN` (`OP_RETURN <0x00> `); asset identifier is `(genesis_txid, group_index)`; transferred through VTXOs with operator awareness. Arkade Labs blog: *Native Assets on Bitcoin: Introducing Arkade Assets* (Oct 2025). |
+| **Confidential VTXO** | — | Hypothetical Arkade extension in which the operator cosigns commitments to amounts and recipients rather than plaintext, with a ZK proof of batch correctness. Open research as of 2026-05; no published proposal. See §6.5. |
+| **A1 – A6** | — | Locked design decisions for the Arkade integration (this document, §3). Mirrors the M1–M6 / D1–D11 numbering scheme of [`MULTI_ASSET.md`](./MULTI_ASSET.md) and [`MIGRATION_RESEARCH.md`](./MIGRATION_RESEARCH.md). |
+
+---
+
+## 5. Protocol-mechanics comparison
+
+The two protocols solve adjacent problems with structurally different
+primitives. This section is the side-by-side reference used throughout
+the rest of the document.
+
+### 5.1 Atomic unit
+
+| Aspect | Ark / Arkade | Shielded CSV / zkCoins |
+| ------ | ------------ | ---------------------- |
+| Unit | **VTXO** — `(value, vtxoLockScript)` (Ark §4 Definition 4.1). Mechanically a real Bitcoin output, Taproot-locked, key path unspendable, at least one collaborative + one unilateral exit script path. | **Coin** — `(CoinEssence{address, amount, idx}, tx_hash, nullifier_location, accumulator_value)` (Shielded CSV §4.2). No script, no UTXO, no on-chain output. |
+| Where it lives | Off-chain. Realisable on-chain via the unilateral exit script path. | Entirely off-chain. Chain stores only nullifiers (Schnorr half-aggregate, ~64 bytes/tx). |
+| Spending condition | Arbitrary Bitcoin Script via the Taproot script paths. Today's MuSig2 cosigning emulates a covenant (Ark §3.2). | None. `apply_coin`'s `coin.recipient == self.owner` is the only check ([`program/src/lib.rs:154`](./program-plonky2/src/circuit/main.rs)). |
+| Privacy from external observer | Operator-visible by construction (Ark §2.2). Amounts and recipients exposed to the operator and to anyone who sees the VTXT. | Hidden from everyone except sender and recipient (Shielded CSV §1.1, "Privacy"). PCD proof is zero-knowledge; only `(nullifier_pubkey, signature)` on-chain. |
+
+### 5.2 On-chain artifacts
+
+Per Arkade batch (Ark §4.4, Definition 4.9):
+
+- **`commitment_tx`** — one Bitcoin tx. Inputs: operator funds + any
+ boarding txs. Outputs: `batch` (Taproot — sweep after `T_e`, unroll
+ enforcing the VTXT), `connector` (Taproot enforcing the anchor-output
+ chain), optional outputs for users leaving the Ark.
+- **`forfeit_tx`** (off-chain unless needed) — signed by user with
+ SIGHASH_ALL over `(old_vtxo, connector_anchor_ε)`; valid only if the
+ `commitment_tx` confirms.
+- **Cadence** — operator-controlled. Whitepaper does not fix a number;
+ current Arkade deployments use sub-second preconfirmations with
+ periodic anchoring (typically minutes-to-hours).
+
+Per zkCoins transaction (Shielded CSV §4.2):
+
+- **One aggregate nullifier**: `(nullifier_pubkeys[], NISSHAC
+ half-aggregate signature, publisher_address)`. With Schnorr
+ half-aggregation, ~64 bytes per transaction regardless of input
+ count (Shielded CSV §1.1, Table 1).
+- **MVP implementation** wraps this in a Taproot inscription with
+ txid prefix `4242` carrying a `Commitment` payload over
+ `H(asth ‖ ocr)` ([`SPEC.md`](./SPEC.md) §11). The paper specifies
+ raw nullifiers; the wrapping is a deliberate divergence
+ ([`SPEC.md`](./SPEC.md) §15).
+
+| Artifact | Arkade | Shielded CSV |
+| -------- | ------ | ------------ |
+| Per-batch on-chain footprint | 1 `commitment_tx` (constant in #VTXOs in the optimistic case) | n × 64-byte aggregate nullifiers (one per transaction; publisher batches multiple senders' nullifiers into one inscription) |
+| Settlement cadence | Operator-controlled batch interval | Per transaction; bounded by aggregator's publication cadence |
+| Worst-case exit | `O(log t)` virtual txs for unilateral exit from a VTXT of `t` leaves (Ark §2.3, §4.1) | N/A — no exit, no per-coin on-chain footprint |
+| Bitcoin TPS ceiling | Bounded by `commitment_tx` size and frequency | ~100 TPS at current Bitcoin block-size limit (Shielded CSV §1.1) |
+
+### 5.3 Roles and trust
+
+| Role | Arkade operator | zkCoins publisher | zkCoins bridge |
+| ---- | --------------- | ----------------- | -------------- |
+| What they do | Liquidity provision, batching, MuSig2 cosigning per VTXO holder (Ark §2.2) | Collects nullifiers, half-aggregates, posts the aggregate as a Taproot inscription, claims fees (Shielded CSV §1.1, "Trustless Publishing"). **Permissionless** — anyone can be a publisher. | Custodies BTC against zkCoins-side credits. Phase 1: M-of-N federation multisig ([`BRIDGE_MVP.md`](./BRIDGE_MVP.md)). Phase 2: 1-of-N honesty BitVM2 setup ([`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md)). |
+| Centralisation | Single operator today (Ark §7, "Centralisation of Ark Operator" — explicitly named as a future-work axis) | None — anyone with a Bitcoin wallet can publish | Phase 1: M-of-N trusted. Phase 2: 1-of-N honesty at setup ceremony. |
+| Liveness assumption | Operator online ⇒ batch swaps and collaborative exits work. Operator offline ⇒ unilateral exit only. | Publisher offline ⇒ another publisher can take the same nullifier. No single point of failure. | Bridge stalls if no operator is willing to front a payout; the user keeps their zkCoins balance. |
+| Custody | **Never.** VTXOs are user + operator MuSig2; unilateral exit always available (Ark §2.3). | **Never.** Publisher sees nullifier data only, never plaintext coin data. | **Yes** in Phase 1 (federation holds BTC). **No** in Phase 2 (vault in N-of-N MuSig with pre-signed paths). |
+
+**Critical security property of Arkade:** Ark §5 Table 1 names six
+properties under "rational" vs. "malicious" operator. Under a
+*malicious* operator the protocol still satisfies onramp liveness
+(NL) and offramp liveness (FL); violations of safety properties (NS,
+AS, FS) "come only at the cost of the operator, not of users
+following the protocol." A malicious Arkade operator cannot steal
+user funds; they can only burn their own funds while users still
+exit.
+
+**Critical security property of Shielded CSV:** §1.1 ("Permissionless")
+— "the protocol does not rely on any trusted party for transaction
+execution. All necessary data is directly written to, and retrieved
+from, the blockchain." Censorship resistance reduces to Bitcoin's own
+censorship resistance. The single trust assumption is the bridging
+component, not the protocol.
+
+### 5.4 The fundamental asymmetry
+
+The point worth repeating: **Arkade is a Bitcoin-script L2** in the
+strong sense — VTXOs *are* Bitcoin outputs with locking scripts, just
+not yet broadcast. **Shielded CSV is not L2 in the same sense** —
+coins have no script and no on-chain footprint; the chain is a notary
+for ordering and uniqueness, nothing more.
+
+Every integration in §6 is shaped by this asymmetry. The Arkade side
+can carry arbitrary Bitcoin Script (HTLC, DLC, channels), and the
+zkCoins side cannot. Atomicity always lives on the Arkade VTXO or on
+the Bitcoin funding tx of the zkCoins inscription —
+[`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) §5 derives
+this for Lightning; the same logic applies here.
+
+---
+
+## 6. Integration paths
+
+Six paths, layered by maturity. Layer 0 is "today, no work." Layer 1
+is "this design doc's headline target — 6-12 months engineering."
+Layer 2 splits into three independent research directions of varying
+maturity.
+
+### 6.1 Layer 0 — independent systems
+
+A user holds an Arkade wallet pointing at some Arkade instance and a
+zkCoins wallet pointing at a zkCoins server. The wallets do not
+interoperate. The user manually converts between BTC and zkCoins via
+the bridge ([`BRIDGE_MVP.md`](./BRIDGE_MVP.md) or
+[`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md)) and between BTC and Arkade VTXOs
+via boarding/exit (Ark §4.5).
+
+**Cost:** zero engineering. Two wallets, manual juggling, two distinct
+BTC custody contexts.
+
+**When it makes sense:** today, for power users who want both privacy
+(zkCoins) and shared-UTXO economics (Arkade) without integration risk.
+
+**When it stops being enough:** as soon as a single user flow ("private
+payment from a long-term BTC store") needs both protocols. The user
+should not have to choose; the system should compose them.
+
+### 6.2 Layer 1 — HTLC atomic swap (the realistic short-term target)
+
+Direct preimage-based atomic swap between an Arkade VTXO carrying an
+HTLC encumbrance and a zkCoins 2-of-2 shared account. This is decision
+A1; it is detailed end-to-end in §7.
+
+**Why this is realistic in 6-12 months:**
+
+- Shielded CSV §A.1.2 already specifies the exact PTLC + 2-of-2
+ shared-account construction for Shielded CSV ↔ Bitcoin atomic
+ swaps. The construction is documented, not novel.
+- Arkade's compiler ships HTLC as a built-in primitive
+ (`arkade-os/compiler` README; `docs.arkadeos.com/learn/smart-contracts/hash-time-locked-contract`).
+ Hash-locked outputs on a VTXO are a one-template instantiation.
+- Replacing "Bitcoin PTLC" in the Shielded CSV recipe with "Arkade
+ VTXO with HTLC script-path" is mechanically straightforward.
+- Same engineering surface as [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md);
+ the lessons there apply with minimal adaptation.
+
+**What it ships:** a user who holds Arkade BTC can atomically convert
+to zkCoins, and vice versa, without either side trusting the other to
+honour the swap. The swap counterparty (a swap provider running both
+an Arkade wallet and a zkCoins shared account) faces the same
+incentive structure as a Boltz operator.
+
+**Failure modes** are exactly the failure modes in §7.5 — bounded by
+the `htlc_timeout < T_e` constraint (every script construction on a
+VTXO inherits batch expiry per Ark §6) and by the standard HTLC
+timing-coordination story.
+
+Three variants of the atomic swap, in order of preference:
+
+1. **Direct two-leg HTLC swap (recommended).** Section 7 below.
+2. **Federation-mediated swap.** A zkCoins federation node runs an
+ Arkade-watching service and credits zkCoins on observing specific
+ Arkade events. Strictly weaker than variant 1 (introduces
+ federation trust) without adding capability. Skip in v1.
+3. **Lightning hop.** Arkade ↔ Lightning ↔ zkCoins via two HTLC
+ rounds. Arkade ships Lightning swap support
+ ([`blog.arklabs.xyz` — *Closing the Lightning loop*](https://blog.arklabs.xyz/closing-the-lightning-loop-bitcoins-missing-layer-secretly-goes-live/));
+ zkCoins has its own LN design in
+ [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md).
+ Stacking them works but adds a hop. Useful if liquidity is on the
+ other side of the LN graph; otherwise variant 1 is one round
+ simpler.
+
+### 6.3 Pipeline use — BTC ↔ Arkade ↔ zkCoins ↔ Arkade ↔ BTC
+
+Composes Layer 1 with Arkade boarding and exit to give a full
+end-to-end user flow:
+
+```
+User holds BTC on-chain.
+↓ boarding_tx (Ark §4.5): Taproot(F, checkSig(pkO⊕pkA), checkSig(pkA)∧relTimelock(t_b))
+User holds a VTXO inside Arkade.
+↓ Layer 1 HTLC atomic swap (§7): VTXO encumbered by HTLC, zkCoins-side 2-of-2 shared account
+User holds shielded coins inside zkCoins.
+... user transacts privately at scale inside zkCoins (per-tx ~64 bytes on-chain) ...
+↓ Layer 1 HTLC atomic swap reversed: zkCoins burn → fresh Arkade VTXO
+User holds a fresh Arkade VTXO.
+↓ Arkade unilateral or collaborative exit (Ark §4.5, "Leaving the Ark")
+User holds BTC on-chain.
+```
+
+**Why this is the killer combination:**
+
+- **Cheap onboarding.** Arkade's `boarding_tx` is a shared
+ Taproot output. The on-chain cost of one user's onboarding is
+ amortised across a batch.
+- **Cheap per-tx scaling.** Inside zkCoins, every transaction
+ amortises to ~64 bytes on-chain regardless of value or input
+ count.
+- **Cheap settlement.** Arkade's `commitment_tx` is one Bitcoin
+ tx per batch, and an exit (collaborative) is one transaction.
+ Pessimistic exit is `O(log t)` virtual txs.
+
+Neither protocol alone achieves both cheap onboarding and cheap
+per-tx scaling. The combined pipeline does. This is the strongest
+narrative motivation for the integration; A1 is the protocol step
+that unlocks it.
+
+**On-chain footprint per pipeline traversal** (steady-state, ignoring
+the initial boarding):
+
+| Step | Bitcoin txs | Notes |
+| ---- | ----------- | ----- |
+| Boarding (once) | 1 (`boarding_tx`) | Shared, amortised |
+| Arkade Ark transaction | 0 | Lives inside Arkade until next `commitment_tx` |
+| Arkade `commitment_tx` (periodic) | 1 per batch, amortised across all batch members | — |
+| HTLC swap to zkCoins | 0 (uses existing Arkade primitives) + 1 zkCoins nullifier inscription (~64 bytes) | The HTLC sits inside the VTXO; the swap reveals the preimage but does not add an on-chain artifact beyond what zkCoins already publishes |
+| zkCoins-internal transaction | ~64 bytes nullifier (per-tx, batched by publisher) | — |
+| HTLC swap back to Arkade | 1 zkCoins nullifier (burn) + Arkade VTXO transfer (0 additional) | — |
+| Arkade exit (collaborative) | 1 collaborative exit tx via `commitment_tx` add-output (Ark §4.5) | — |
+| Arkade exit (unilateral) | `O(log t)` virtual txs | Only if operator stalls |
+
+**Trust assumptions per step:**
+
+- Onboarding / Arkade transfers / Arkade exit: Arkade rational
+ operator + 1-of-n MuSig honesty (Ark §5 Table 1).
+- HTLC swaps in either direction: standard HTLC trust model
+ (no custody handoff possible without preimage reveal), bounded by
+ `T_e` on the Arkade side and the publisher's nullifier-publication
+ cadence on the zkCoins side.
+- zkCoins-internal transfers: per [`SPEC.md`](./SPEC.md) — server-side
+ compute correctness + Schnorr signature security.
+
+§8 has the full trust-stacking analysis.
+
+### 6.4 Layer 2a — Ark-aware BitVM bridge (1-2 years)
+
+**[SPEC]** Speculative architectural sketch. Not in any roadmap as of
+2026-05.
+
+zkCoins Phase 2 ([`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md)) uses BitVM2 +
+Groth16 verification to prove "this operator's payout tx is included
+in a finalized Bitcoin chain" and authorise zkCoins-side mints from a
+Bitcoin Light Client gadget. Mechanically, the same federation
+infrastructure can also operate an Arkade instance:
+
+- The same N-of-N MuSig2 vault key construction works for any
+ custody role.
+- The same Bitcoin Light Client gadget that verifies "BTC is locked
+ in vault" can equally verify "the Arkade `commitment_tx` confirmed
+ with batch β."
+- An Arkade operator's liquidity-provision role overlaps with the
+ BitVM2 operator's "front BTC, get reimbursed later" role.
+
+The integration insight: peg-in becomes an Arkade boarding (cheap,
+amortised) instead of a direct BTC tx. Peg-out frontruns an Arkade
+VTXO transfer; user can unilateral-exit if the operator stalls. The
+bridge's on-chain footprint reduces; the trust model does not change.
+
+**Security model overlap.** Ark's rational-operator assumption gives
+onramp safety (NS), Ark safety (AS), offramp safety (FS) without users
+losing funds even under malice (Ark §5 Table 1). BitVM2's 1-of-N
+setup honesty gives "no operator coalition can spend the vault
+outside pre-signed paths" ([`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md) §3.2).
+These are **independent** assumptions — Ark's holds for Ark, BitVM2's
+holds for the peg. A federation that fails one role does not
+compromise the other unless the same key material is at risk.
+
+**Realistic horizon:** 1-2 years, gated on (a) BitVM2 production
+maturity and Glock/Argo cost reductions making it economical at scale,
+(b) Arkade multi-operator support reducing the operator-side
+centralisation risk, (c) demand exceeding what a Layer 1 + Layer 2
+bridge can serve. None of these are in zkCoins' control; this is a
+"keep an eye on" path, not a sprint candidate.
+
+### 6.5 Layer 2b — Confidential VTXOs (research, 1-2+ years)
+
+**Open research, not engineering.** [SPEC]-grade content.
+
+Arkade VTXOs are operator-visible by construction. The operator sees
+plaintext amounts and recipient pubkeys to construct the VTXT, cosign
+batches, and manage liquidity. End-to-end-encrypted communication
+channels protect against passive observers but not the operator.
+
+The question this section explores: could the operator be reduced to
+cosigning *commitments* to amounts and recipients, with a ZK proof of
+batch correctness?
+
+A confidential-VTXO scheme would need:
+
+1. **Pedersen commitments (or equivalent) on VTXO amounts.** Mature
+ crypto; standard.
+2. **Range proofs per VTXO.** Bulletproofs ~700 bytes/VTXO, or
+ SNARK-compressed via the same PCD/Plonky2 stack zkCoins already
+ uses (Shielded CSV §6.3).
+3. **A ZK proof of correctness of the operator's signed batch.**
+ "Sum of input commitments = sum of output commitments + fee" and
+ "each output commitment is well-formed". The operator signs a
+ circuit proof, not plaintext. Mathematically, this is exactly the
+ PCD compliance predicate Shielded CSV uses for coins, lifted to
+ batches.
+4. **A redesigned forfeit mechanism.** The operator must be able to
+ claim on double-spend without knowing the amount. This needs
+ either a deterministic binding (commit-to-spend) or a separate
+ amount-revelation in the forfeit-claim path. Genuinely new
+ cryptography; no existing template.
+
+**SP1 as the proving stack** would be the natural choice (zkCoins'
+predecessor used SP1, locked at v4.1.2 per institutional memory;
+current zkCoins uses Plonky2 per
+[`CONTRIBUTING.md`](./CONTRIBUTING.md) invariant 5). A zkCoins-style
+PCD layer over Arkade's batching is mathematically sensible — PCD is
+the right abstraction for "validity proof composes over a DAG-shaped
+state machine," which is exactly what Ark's VTXT is.
+
+**Realistic assessment:**
+
+- Without a Bitcoin soft fork (no Confidential Assets opcode, no
+ Mimblewimble in Bitcoin Script) the privacy is *off-chain in Ark*
+ but the on-chain `commitment_tx` still exposes the batch's input
+ totals.
+- The forfeit-mechanism redesign is paper-worthy new cryptography.
+- 1-2 year research project. The Shielded CSV authors sit in
+ exactly the right ecosystem to attack this; no public proposal as
+ of 2026-05.
+
+**This section is descriptive, not prescriptive.** zkCoins does not
+take responsibility for confidential VTXOs; if Arkade or an external
+research group ships them, the design space in §7 and §6.6 changes
+favourably. We track the direction; we do not invest in it.
+
+### 6.6 Layer 2c — Cross-asset DEX (12+ months, engineering not research)
+
+Arkade Labs has launched **Arkade Assets**
+([blog.arklabs.xyz — *Native Assets on Bitcoin: Introducing Arkade
+Assets*](https://blog.arklabs.xyz/native-assets-on-bitcoin-introducing-arkade-assets/),
+Oct 2025): TLV-encoded native assets in `OP_RETURN`, asset identifier
+`(genesis_txid, group_index)`, transferred through VTXOs with operator
+awareness. zkCoins is becoming permissionless multi-asset via
+[`MULTI_ASSET.md`](./MULTI_ASSET.md) — anyone mints a token, identifier
+is a Poseidon digest of genesis pre-image, transferred privately.
+
+A swap between Arkade Asset X and zkCoins Asset Y is structurally
+**A1 with two field substitutions**:
+
+- The Arkade side encumbers an Arkade Asset (not bare BTC) with an
+ HTLC. The Arkade compiler supports asset-flow validation
+ (transaction introspection), so the HTLC enforces "send `v` units
+ of `asset_id_A` to receiver on preimage reveal."
+- The zkCoins side uses a 2-of-2 shared account holding `asset_id_B`.
+ Multi-asset shared-account machinery works unchanged from the
+ single-asset case ([`MULTI_ASSET.md`](./MULTI_ASSET.md) §4.4 — every
+ state transition is single-asset, but shared accounts can hold any
+ asset).
+
+**Why this is novel** as a Bitcoin-native primitive:
+
+- First publicly-described BTC-L1-only cross-asset swap involving a
+ privacy-preserving asset (zkCoins-asset, hidden amount + sender +
+ recipient) and an operator-visible asset (Arkade Asset).
+- Composable: any Arkade Asset, any zkCoins asset. The matching
+ engine sits off-protocol.
+- A natural first cross-protocol DEX primitive for the
+ "Bitcoin-native trustless DeFi" thesis.
+
+**Honest framing.** This is **engineering, not research.** The crypto
+already exists, the templates exist; what is missing is wiring +
+a matching engine. Realistic in ~12 months of focused work after A1
+ships and [`MULTI_ASSET.md`](./MULTI_ASSET.md) reaches steady state.
+Tracked as decision A6.
+
+---
+
+## 7. Detailed Flow: HTLC Atomic Swap (Arkade BTC ↔ zkCoins)
+
+This section is the implementation-grade specification of decision A1.
+It mirrors the structure of
+[`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) §8: detailed
+flow, failure modes, trust argument.
+
+### 7.1 Parties and pre-conditions
+
+- **User (Alice):** Arkade wallet pointing at some Arkade instance,
+ zkCoins wallet pointing at a zkCoins server, an existing zkCoins
+ account.
+- **Counterparty (Bob, "swap provider"):** Arkade wallet with VTXO
+ inventory, zkCoins server with sufficient inventory in some operator
+ account. May be the same operator that runs the Arkade instance and
+ the zkCoins server, or a third party; the protocol does not require
+ it.
+- **Pre-agreed parameters:** swap amount `A`, provider fee `F`, the
+ on-Arkade HTLC timeout `T_htlc`, the zkCoins-side recovery timeout
+ `T_recovery` with `T_htlc < T_recovery`, both strictly less than the
+ Arkade batch expiry `T_e`.
+
+### 7.2 The asymmetry to resolve
+
+Section 5.4 framed it; this section operationalises it.
+
+An Arkade VTXO can encode an arbitrary Bitcoin Script — it is a
+Taproot output with at minimum a cooperative path
+(`checkSig(pkO ⊕ pkA)`), a unilateral exit path
+(`checkSig(pkA) ∧ relTimelock(t_v)`), and any number of additional
+script paths. The Arkade compiler ships an HTLC template natively
+(`arkade-os/compiler` README):
+
+```text
+contract HTLC(pubkey sender, pubkey receiver, bytes hash, int refundTime) {
+ function claim(signature receiverSig, bytes preimage) {
+ require(checkSig(receiverSig, receiver));
+ require(sha256(preimage) == hash);
+ }
+}
+```
+
+The HTLC compiles into a Taproot script-path. The VTXO retains its
+operator + user collaborative path (so the operator can sign Alice's
+spend cooperatively if she reveals the preimage in-protocol) and its
+unilateral exit path (so Alice can take it on-chain if the operator
+stalls).
+
+A zkCoins coin **cannot** encode any spending condition. There is no
+`script` field on `Coin`; the recipient check is hard-coded
+([`program/src/lib.rs::apply_coin`](./program-plonky2/src/circuit/main.rs)).
+[`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) §5.1–5.3
+derives why this is load-bearing for the protocol; the conclusion
+ports here unchanged.
+
+### 7.3 Where atomicity lives
+
+Per Shielded CSV §A.1.2 and [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md)
+§5.4, atomicity for a zkCoins side participant must come from either:
+
+1. **A 2-of-2 shared zkCoins account** with a pre-signed time-locked
+ recovery to the original owner. Shielded CSV §5.1 (Shared Accounts)
+ + §A.1.1 (Time-locked Transactions) provide the primitives.
+2. **The Bitcoin funding transaction of the zkCoins inscription**
+ carrying a script lock.
+
+For Arkade ↔ zkCoins, **option 1 is the canonical choice**: it
+mirrors the construction Shielded CSV §A.1.2 uses for Shielded-CSV ↔
+Bitcoin/L2 atomic swaps, and it does not couple atomicity to the
+publisher's inscription mechanics (which would force coordination
+between the swap counterparty and the publisher).
+
+Option 2 is preferred for Lightning swaps in
+[`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) §6 because the
+on-chain side there is bare Bitcoin without any other lever. For
+Arkade swaps the Arkade VTXO is itself the script-bearing side; the
+zkCoins side does not need to carry the HTLC.
+
+### 7.4 Protocol steps
+
+**Direction A — Alice has zkCoins, wants Arkade BTC. Bob has Arkade
+BTC, wants zkCoins.** Alice generates the preimage.
+
+```
+Step 1. Alice generates preimage x ←$ {0,1}^256, computes H = SHA256(x).
+ Alice sends to Bob:
+ - H
+ - alice_arkade_recipient_pubkey (for the VTXO claim)
+ - amount A
+ - alice_zkcoins_account_pubkey (for the 2-of-2 shared account)
+
+Step 2. Alice and Bob set up the 2-of-2 zkCoins shared account:
+ - Construct MuSig2 aggregate pubkey pkA⊕pkB
+ - Alice prepares recovery_tx (zkCoins nullifier publication
+ that returns the shared account's balance to Alice after
+ block height h_recovery = current + T_recovery)
+ - Alice signs her half of recovery_tx, sends to Bob
+ - Bob signs his half (MuSig2 partial), aggregates
+ - Alice now holds a valid recovery_tx she can publish after
+ T_recovery
+
+Step 3. Alice publishes the funding nullifier:
+ - zkCoins transaction Alice → 2-of-2(pkA⊕pkB), amount A
+ - Publisher batches the nullifier; coins land in the shared
+ account on next inscription
+
+Step 4. Bob constructs an Arkade VTXO with an HTLC encumbrance:
+ - contract HTLC(sender=Bob, receiver=Alice, hash=H,
+ refundTime=current + T_htlc)
+ - Cooperative-path: pkO⊕pkB (Bob can cooperate with operator
+ to refund after T_htlc, or to honour an
+ early settle)
+ - Unilateral-path: pkB ∧ relTimelock(t_v) (standard Arkade
+ exit)
+ - HTLC script-path (per Arkade Script template above) is the
+ new addition
+ - Bob boards the VTXO collaboratively with the Arkade operator
+
+Step 5. Alice verifies the VTXO:
+ - VTXO is in Arkade, value = A
+ - HTLC script-path matches: H, refundTime, alice's pubkey as receiver
+ - T_htlc < T_recovery (so Bob cannot refund the Arkade side
+ after Alice has lost the recovery option)
+ - T_htlc < T_e (so the cooperative-path stays live; if T_htlc
+ ≥ T_e the operator's sweep fires first and the
+ HTLC is moot)
+
+ If any check fails, Alice aborts. Alice's funds are in the
+ 2-of-2 shared account; recovery_tx returns them after
+ T_recovery. No loss to Alice.
+
+Step 6. Alice claims the Arkade VTXO by revealing x:
+ Option (a) — cooperative claim:
+ - Alice asks the operator to cosign an Arkade transaction
+ spending the VTXO via the HTLC script-path: input witness
+ includes
+ - Operator validates the script-path satisfaction (sha256(x)
+ == H), cosigns
+ - New VTXO with Alice's pubkey as cooperative-path key
+
+ Option (b) — unilateral claim (if operator stalls):
+ - Alice publishes the unilateral chain of Ark transactions
+ (O(log t) txs from the batch root to her VTXO leaf)
+ - Then publishes a Bitcoin tx spending her leaf VTXO via
+ the HTLC script-path
+
+ Either way, x is now public — on the Arkade transcript (option
+ a, visible to the operator and any party watching Arkade) or
+ on-chain (option b).
+
+Step 7. Bob learns x. Bob uses x to take control of the 2-of-2 zkCoins
+ shared account before T_recovery:
+ - Bob constructs a zkCoins transaction that nullifies the
+ shared account's balance to Bob's own zkCoins account
+ - Requires MuSig2 signature with both pkA and pkB; Bob
+ already has both pkA's contribution because the
+ shared-account setup pre-shared signing material with the
+ preimage-bound condition (this mirrors Shielded CSV §A.1.2's
+ "Bob learns x, uses it as one factor in the MuSig2
+ cooperative signature path")
+
+Step 8. Bob's transaction publishes the nullifier. Shared account
+ empty. Swap complete.
+```
+
+**Symmetric flow** for direction B (Bob has zkCoins, wants Arkade
+BTC) inverts roles — Bob generates the preimage. The construction is
+otherwise identical.
+
+### 7.5 Failure modes
+
+| Failure | Who has what | Recovery |
+| ------- | ------------ | -------- |
+| Alice aborts at Step 5 | Alice has shielded coins in 2-of-2 shared account; Bob has a VTXO encumbered by HTLC | Alice waits `T_recovery` and publishes `recovery_tx`. Bob's VTXO refunds via Arkade HTLC `refundTime`. Both made whole; small fees lost. |
+| Bob never boards the HTLC-encumbered VTXO (Step 4) | Alice has funds in shared account, Bob has nothing | Same as above: Alice's `recovery_tx` after `T_recovery`. Bob has nothing to refund. |
+| Operator refuses cooperative claim at Step 6(a) | Alice cannot get cooperative settlement | Alice falls back to unilateral claim (Step 6(b)), `O(log t)` virtual txs published on-chain. Preimage `x` becomes public. Bob still proceeds to Step 7. Higher cost to Alice. |
+| Alice never claims the VTXO (Step 6 not executed) | Bob has VTXO locked in HTLC; Alice has shielded coins | Bob waits `T_htlc`, refunds the VTXO via Arkade HTLC `refundTime` path (cooperative with operator). Alice waits `T_recovery > T_htlc`, recovers shielded coins via `recovery_tx`. Both whole. |
+| Bob never executes Step 7 (refuses to claim shared account after seeing `x`) | Alice has Arkade BTC, Bob has nothing on the zkCoins side; shared account still holds A | Alice's `recovery_tx` after `T_recovery` returns shielded coins to Alice. **Net: Alice has both A worth of Arkade BTC and A worth of shielded coins** — Bob's loss. Asymmetric incentive: Bob has no reason to do this. Documented as provider-side discipline. |
+| Bob claims shared account via Step 7 but Alice never sent the VTXO claim | Cannot happen — Step 7 requires `x`, which only becomes public after Step 6 | — |
+| Arkade operator goes offline between Step 4 and Step 6 | Same as "Operator refuses cooperative claim" — Alice unilateral-exits | Same recovery. |
+| `commitment_tx` carrying the HTLC-VTXO does not confirm before `T_e` | The Arkade batch expires; operator sweeps; HTLC is moot | This is the canonical `htlc_timeout < T_e` constraint from Ark §6. Step 5 verifies it. If misconfigured, Alice's preimage-reveal becomes useless because there's nothing left to claim; she falls back to her zkCoins recovery_tx. |
+| Both parties' refund txs race for the same block | Standard fee-management concern | Pre-sign with sufficient fee bumping; not a trust issue. |
+
+### 7.6 Trust assumptions
+
+At no point does either party transfer custody of an asset to the
+other party where the other party can withhold reciprocation:
+
+- Alice's funds in the 2-of-2 shared account are recoverable via
+ `recovery_tx` after `T_recovery` — Bob cannot block this.
+- Bob's VTXO encumbered by HTLC is recoverable via `refundTime`
+ after `T_htlc` (cooperative with operator, or unilateral exit) —
+ Alice cannot block this.
+- `T_htlc < T_recovery` ensures Bob's refund window closes before
+ Alice's recovery window opens, so the swap is timing-safe: if Alice
+ claims, Bob has time to learn `x` and execute Step 7 before
+ `T_recovery`; if Bob refunds, Alice has not yet given up her recovery.
+
+**The trust assumptions are independent in each leg.** Alice trusts
+the Arkade operator's rationality for the cooperative-claim path
+(falls back to unilateral exit if violated). Alice trusts the zkCoins
+publisher's liveness for the inscription publication (falls back to a
+different publisher; any party can publish). Alice trusts neither Bob
+nor the operator with custody — preimage-bound timeouts enforce
+correctness.
+
+### 7.7 Latency and costs
+
+**Latency (happy path, cooperative claim):**
+
+- Step 1–2 (shared-account setup): one round of MuSig2 messages
+ (sub-second over the wire).
+- Step 3 (funding nullifier): one Schnorr-signed inscription,
+ bounded by zkCoins publisher cadence + Bitcoin confirmation depth
+ needed for the swap timing model (typically 1–6 confirmations).
+- Step 4 (VTXO with HTLC): one Arkade boarding round, bounded by
+ Arkade operator's batch cadence.
+- Step 6(a) (cooperative claim): one Arkade transaction, sub-second
+ preconfirmation.
+- Step 7 (shared-account claim): one zkCoins inscription, bounded by
+ publisher cadence.
+
+**Total wall-clock for happy path:** dominated by zkCoins inscription
+confirmation. Per [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md)
+§14 the conservative envelope is on the order of an hour for
+end-to-end Bitcoin-confirmation safety; pre-D7 the same envelope
+applies here.
+
+**Costs (per swap):**
+
+- Arkade side: one VTXO worth of liquidity locked for `T_htlc`;
+ Arkade transaction fees (typically negligible inside Arkade).
+- zkCoins side: two inscriptions (funding + claim), each ~64 bytes
+ amortised plus the publisher's overhead.
+- Counterparty fee `F`: market-set, comparable to Boltz fees.
+
+**Pessimistic path** (unilateral exit, dispute) costs an extra
+`O(log t)` virtual transactions on the Arkade side. This is the
+standard Ark exit cost (Ark §2.3) and is borne by whoever invokes the
+unilateral path.
+
+---
+
+## 8. Trust-model stacking
+
+The combined stack inherits the union of both protocols' trust
+assumptions. Understanding what depends on what is the key to
+reasoning about real-world security.
+
+### 8.1 Independent assumptions
+
+| Component | Assumption | Effect of violation |
+| --------- | ---------- | ------------------- |
+| Arkade operator (rational) | Operator follows protocol | Operator loses their own funds, not users'; users still exit (Ark §5 Table 1) |
+| Arkade operator (malicious) | Operator deviates | NL, FL still hold; NS, AS, FS violations cost the operator, not users |
+| Arkade MuSig2 covenant emulation | 1-of-n VTXO holders + operator follow signing protocol | VTXT well-formed (Ark §3.2, §4 Remark 4.5) |
+| zkCoins server-side compute | Server runs the published Plonky2 circuit honestly | Per [`CONTRIBUTING.md`](./CONTRIBUTING.md) invariant 1 + invariant 2; closed test environment today, in-circuit verification long-term |
+| zkCoins Schnorr signatures | BIP-340 / secp256k1 secure | Standard Bitcoin cryptographic assumption |
+| zkCoins publisher liveness | Some publisher willing to inscribe | Permissionless — alternative publishers can take the nullifier |
+| zkCoins bridge Phase 1 (federation) | M-of-N federation honesty ([`BRIDGE_MVP.md`](./BRIDGE_MVP.md)) | M+ colluders can steal BTC reserves; zkCoins-side internal transfers unaffected |
+| zkCoins bridge Phase 2 (BitVM2) | 1-of-N setup honesty ([`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md)) | If all N are malicious at setup, vault parameters can be compromised; once setup completes, peg-out paths are public and trustless |
+| Bitcoin L1 | Bitcoin's PoW + censorship resistance | Catastrophic for both protocols; outside the design space |
+
+### 8.2 Composition for §7's HTLC swap
+
+The HTLC atomic swap of §7 requires:
+
+- Arkade rational operator (so cooperative claim works; unilateral
+ fallback if violated).
+- Bitcoin L1 (for confirmation of the inscriptions and any unilateral
+ Arkade exit).
+- zkCoins server-side compute (so the publisher accepts and processes
+ the nullifier).
+- BIP-340 Schnorr security (for both sides' signatures).
+
+It does **not** require:
+
+- A zkCoins bridge to be running. The swap is BTC-pegged on the
+ Arkade side and uses zkCoins-internal coins on the other side; the
+ bridge only matters if one party wants to convert between zkCoins
+ shielded coins and real BTC outside the swap.
+
+### 8.3 Composition for §6.3's pipeline
+
+The pipeline composes:
+
+- Arkade onboarding → Arkade rational operator + Bitcoin L1
+- §7 HTLC swap into zkCoins → as in §8.2
+- zkCoins-internal transfers → zkCoins server-side compute + Schnorr
+- §7 HTLC swap out of zkCoins → as in §8.2
+- Arkade exit → Arkade rational operator (cooperative) or pure Bitcoin
+ L1 (unilateral)
+
+Each step's failure mode is independent; nothing chains a failure
+into a worse failure downstream. The pipeline is no less secure than
+its weakest leg.
+
+### 8.4 Composition for §6.4's Ark-aware BitVM bridge
+
+If the same federation operates the BitVM2 bridge and an Arkade
+instance, both assumptions still apply independently:
+
+- Federation as Arkade operator: rational-operator assumption (Ark
+ §5).
+- Federation as BitVM2 bridge: 1-of-N setup honesty ([`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md)
+ §3.2).
+
+A federation that defects on its Arkade role (steals from itself, since
+Ark §5 says the operator can only harm itself under malice) does not
+compromise its BitVM2 role unless the same key material is involved.
+The design discipline is to keep the key material separate. With
+discipline, the trust assumptions do not collapse.
+
+---
+
+## 9. Personnel and ecosystem signal
+
+The author overlap between the two protocol families is real and
+load-bearing for the "designed to interlock" hypothesis. Worth
+naming explicitly so the implication is not over-claimed.
+
+**Shielded CSV (ePrint 2025/068):** Jonas Nick (Blockstream), Liam
+Eagen (Alpen Labs), Robin Linus (ZeroSync; BitVM creator).
+
+**BitVM / BitVM2:** Robin Linus (lead), Lukas Aumayr, Zeta Avarikioti,
+Matteo Maffei, Andrea Pelosi, Christos Stefo, Alexei Zamyatin (cited
+as ref [1] in Ark whitepaper itself).
+
+**Ark whitepaper:** Marco Argentieri, Zeta Avarikioti, Andrew
+Camilleri, Pim Keer, Matteo Maffei (Ark Labs + TU Wien). **Zeta
+Avarikioti and Matteo Maffei co-author both the BitVM eprint and the
+Ark litepaper.** TU Wien is the institutional connector.
+
+**Glock (Jan 2026):** Robin Linus + Liam Eagen + others (Alpen Labs).
+~430× cost reduction over BitVM2.
+
+**Argo (Jan 2026):** Robin Linus, Liam Eagen, Ying Tong Lai. ~2000×
+cost reduction over BitVM3.
+
+**Translation.** The same ~5 people — Linus, Eagen, Nick, Avarikioti,
+Maffei — are simultaneously authoring the BitVM bridge tech (which
+zkCoins Phase 2 depends on), the Shielded CSV protocol (which zkCoins
+implements), the Ark batching layer (which Arkade implements), and the
+next-generation bridge tech (Glock, Argo) that obsoletes BitVM2 in
+1-2 years. They are deliberately building an interlocking stack.
+
+**Public statements explicitly combining Arkade and zkCoins**: none
+found as of 2026-05.
+
+- Robin Linus' widely-cited quote — *"Shielded CSV is the most
+ interesting thing you can do with BitVM"* — signals the bridge-via-BitVM
+ intent that [`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md) is built on. It
+ does not mention Ark.
+- Ark whitepaper §6 lists "escrows, DLCs, payment channels" as Ark
+ applications. It does not mention Shielded CSV.
+- Shielded CSV paper does not mention Ark.
+- Both papers cite each other's adjacent ecosystem work (Lightning,
+ BitVM) but not each other.
+
+**The signal is institutional, not textual.** The same labs and people
+are shipping both stacks within ~1–2 years of each other; the
+integration is implicit in the personnel and the layered protocol
+design, not declared in the literature. Frame accordingly: a high
+prior that integration tooling will emerge from the same ecosystem,
+**not** a documented unified roadmap to cite.
+
+---
+
+## 10. Open Questions
+
+### 10.1 PTLC vs. HTLC for the swap (§7)
+
+§7 uses HTLC (SHA256 preimage). PTLC (point time-locked contract,
+Schnorr adaptor signature) would give better on-chain privacy by
+making the swap claim indistinguishable from a single-sig spend.
+
+- **Choice in doc:** HTLC. Production-ready toolchain, Arkade compiler
+ ships it, identical trustlessness, identical timing logic.
+- **Alternative:** PTLC. Better privacy on the Arkade side; requires
+ adaptor-signature support in the Arkade compiler (an SDK feature,
+ not a Bitcoin Script change).
+- **Trade-off:** PTLC reduces the on-chain analysability of swap
+ claims but does not change the security argument. Mirror of the
+ HTLC-vs-PTLC discussion in [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md)
+ §7.3. PTLC is a v2 upgrade once Arkade's compiler ships adaptor
+ signatures; not a v1 dependency.
+
+### 10.2 Timing parameter selection (`T_htlc`, `T_recovery`, `T_e`)
+
+§7.1 prescribes `T_htlc < T_recovery < T_e`. Concrete values are
+deployment-dependent.
+
+- **Choice in doc:** the inequalities are protocol-required; the
+ numeric values are operational.
+- **Trade-offs:** longer windows give users more time to act before
+ refund/recovery fires (good UX, more fee-bump headroom); shorter
+ windows reduce capital-lockup costs for swap counterparties (better
+ liquidity efficiency). Arkade's `T_e` is operator-set (Ark §4.4);
+ the swap design must adapt to whatever the chosen Arkade instance
+ uses. Recommended starting points: `T_e` = 1 week (typical Arkade
+ operator default), `T_recovery` = 24 hours, `T_htlc` = 12 hours.
+ Operators should publish their chosen values and update wallets
+ via capabilities flag.
+
+### 10.3 Counterparty discovery / matching engine
+
+§7 assumes Alice and Bob found each other. In practice, swap
+counterparties need a matching engine.
+
+- **Choice in doc:** out of scope for this design doc. Treat as a
+ separate piece of infrastructure (analogous to Boltz' role for
+ submarine swaps).
+- **Trade-off:** centralised matching engines (a website that lists
+ liquidity providers) are operationally trivial but introduce a
+ liveness dependency. Decentralised matching (DHT-based or LN-routing-style)
+ is research. For v1, centralised matching is the obvious choice.
+
+### 10.4 Cooperative vs. unilateral default at Step 6
+
+§7.4 Step 6 distinguishes (a) cooperative Arkade claim via the
+operator and (b) unilateral on-chain claim. Cooperative is sub-second
+and cheap; unilateral is slow and costs `O(log t)` virtual txs.
+
+- **Choice in doc:** wallet defaults to cooperative, falls back to
+ unilateral on operator timeout.
+- **Trade-off:** the cooperative path leaks the preimage to the
+ Arkade operator (operator sees the script-path satisfaction during
+ cosigning); the unilateral path leaks it on-chain to any observer.
+ Either way the preimage becomes public, which is what enables Step 7
+ — there is no privacy-preserving variant short of PTLC.
+
+### 10.5 Pipeline `recovery_tx` lifecycle
+
+In §6.3's pipeline, the user has a `recovery_tx` pre-signed for each
+HTLC swap into and out of zkCoins. These accumulate as the user moves
+between systems.
+
+- **Open:** wallet-side hygiene. Should the wallet auto-execute
+ `recovery_tx` when it observes the corresponding swap completed
+ successfully on the other side? Auto-nullify the recovery to free
+ the shared account?
+- **Recommendation:** track as `zk-coins/app` wallet UX issue once
+ A1 lands; not a server-side concern.
+
+### 10.6 Multi-asset semantics in A1 (vs. A6)
+
+A1 explicitly scopes to BTC-pegged swaps. A6 generalises to Arkade
+Asset ↔ zkCoins Asset.
+
+- **Open:** is there a clean upgrade path from A1 to A6, or does the
+ multi-asset variant want different swap mechanics?
+- **Speculation:** the §7 construction generalises straightforwardly
+ if both sides agree on the asset_id mapping out-of-band. The
+ matching engine (§10.3) becomes the natural place to declare
+ "Arkade Asset X ↔ zkCoins Asset Y" pairs. Confirm during A6 design.
+
+### 10.7 D7 reorg safety dependency
+
+[`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) §15 names
+D7 reorg safety as a zkCoins-side blocker that lengthens swap
+wall-clock time. The same dependency applies to the §7 HTLC swap.
+
+- **Choice in doc:** until D7 lands, the swap design adds Bitcoin
+ confirmation-depth requirements before either party considers an
+ inscription settled. Tracked as a cross-document dependency; not a
+ blocker for the integration design.
+
+---
+
+## 11. Implementation Order
+
+Phased rollout, mapped to discrete milestones. Effort estimates per
+the convention in [`BRIDGE_MVP.md`](./BRIDGE_MVP.md) §12.1 (S = small,
+M = medium, L = large, XL = extra large). All phases assume A1 has
+been locked in this document and a separate implementation spec has
+been opened.
+
+| Phase | Scope | Effort | Risk |
+| ----- | ----- | ------ | ---- |
+| **P0 — Approval of this design** | Maintainer locks A1–A6; this document moves from "draft" to "approved". | **S** | None |
+| **P1 — Implementation spec for §7 HTLC swap** | New sibling doc `ARKADE_HTLC_SWAP.md` (or extension to this document) specifying: zkCoins wire-protocol for shared-account funding, Arkade compiler HTLC parameterisation, swap-counterparty API, recovery-tx persistence model, wallet UX. Mirror of the relationship between [`BRIDGE_MVP.md`](./BRIDGE_MVP.md) and [`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md). | **M** | Low |
+| **P2 — zkCoins shared-account primitive** | Implement 2-of-2 MuSig2 shared accounts in `zk-coins/node` (a prerequisite that does not exist today; [`SPEC.md`](./SPEC.md) §3 single-account-per-pubkey model needs extension). Shielded CSV §5.1 has the protocol-level construction. Persistence, recovery-tx pre-signing, capabilities-flag gating. | **L** | Medium — touches account-state model |
+| **P3 — Arkade swap-counterparty service** | Off-protocol service (likely a separate small Rust crate) that runs as a liquidity provider: monitors Arkade for HTLC-encumbered VTXOs matching swap requests, drives the §7 protocol, signs MuSig2 partials, executes claims. Could be merged into `arkd` upstream or live as a separate binary. | **L** | Medium — coordination across two systems |
+| **P4 — Wallet integration** | `zk-coins/app` wallet learns the swap UX: pick direction, see liquidity, monitor swap status, auto-execute recovery if needed. Mirror of pattern for [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) wallet integration. | **L** | Medium — UX-heavy |
+| **P5 — End-to-end test suite** | Mutinynet + Arkade testnet integration tests, single-counterparty happy path + all failure modes from §7.5. Coverage gate per [`CONTRIBUTING.md`](./CONTRIBUTING.md) invariant 4. | **M** | Low |
+| **P6 — Pipeline orchestration (§6.3)** | Wallet-side multi-step flow combining Arkade boarding + swap-in + swap-out + Arkade exit. UX work, no new protocol. | **M** | Low |
+| **P7 — A6 multi-asset variant** | Generalise the §7 construction to Arkade Asset ↔ zkCoins Asset. Depends on [`MULTI_ASSET.md`](./MULTI_ASSET.md) reaching steady state and Arkade Assets being beyond beta. | **L** | Medium — combinatorial test surface |
+| **P8 — A5 BitVM bridge convergence (optional)** | Design + implementation of the Ark-aware BitVM bridge sketched in §6.4. Depends on Phase 2 BitVM bridge being live and Arkade multi-operator support. | **XL** | High — multi-protocol surgery |
+
+**Aggregate effort for P1–P6 (the A1 implementation path): S + M + L
++ L + L + M + M ≈ 4-6 person-months at focused effort.** P7 and P8
+are explicitly post-A1 and gated on external dependencies.
+
+Per [`CONTRIBUTING.md`](./CONTRIBUTING.md) invariant 4, every phase
+ships with 100% test coverage on the activated surface. Negative
+tests — every failure-mode row in §7.5 must be reproducible in
+integration tests — are mandatory.
+
+---
+
+## 12. Non-Goals (Restated)
+
+So nobody scope-creeps:
+
+- **Modifying the Arkade protocol** — not in scope. The integration
+ uses Arkade as it ships.
+- **Modifying the Shielded CSV protocol or zkCoins circuit** — not
+ in scope (decision A2). No 12th divergence in [`SPEC.md`](./SPEC.md)
+ §15.
+- **Confidential VTXOs** — not in scope (decision A4). Research
+ direction tracked; no zkCoins-side investment.
+- **Building a decentralised swap-counterparty matching engine** —
+ not in scope (§10.3). Centralised matching is fine for v1.
+- **PTLC-based swap variant** — not in v1 (§10.1). HTLC ships first;
+ PTLC is an upgrade.
+- **Federation operating both Arkade and BitVM2 bridge** — not in
+ scope as an A1 deliverable (decision A5 + §6.4). Tracked as a
+ potential 1-2 year roadmap item, depends on Arkade multi-operator
+ maturity.
+- **Generic cross-chain swaps** (Liquid, RSK, sidechains) — out of
+ scope. Different trust model, different document.
+
+---
+
+## 13. References
+
+**Papers:**
+
+- Argentieri, Avarikioti, Camilleri, Keer, Maffei. *Ark: A UTXO-based
+ Transaction Batching Protocol.* Ark Labs & TU Wien, 2024.
+ Local: `research/upstream/` or
+ [`assets.arklabs.xyz/ark-protocol.pdf`](https://assets.arklabs.xyz/ark-protocol.pdf).
+ Cited sections: §2 (overview), §3.2 (covenants), §4 (Ark
+ construction; Definition 4.1 VTXO, Definition 4.9 commitment
+ transaction), §4.3 (batch swaps, forfeit transactions), §4.4
+ (commitment transactions), §4.5 (boarding and leaving), §5
+ (security; Table 1), §6 (applications and HTLC/DLC/channel caveat),
+ §7 (discussion: centralisation, preconfirmation, liquidity).
+- Nick, Eagen, Linus. *Shielded CSV: Private and Efficient Client-Side
+ Validation.* ePrint 2025/068.
+ Local: `research/shieldedcsv-paper.pdf`.
+ Cited sections: §1.1 (privacy, blockchain efficiency, trustless
+ publishing), §4.2 (CoinEssence, accumulator value), §5.1 (shared
+ accounts), §6 (discussion), §A.1.1 (time-locked transactions),
+ §A.1.2 (atomic swap with Bitcoin/L2), §A.1.3 (multi-asset).
+
+**Sibling design docs (this branch):**
+
+- [`SPEC.md`](./SPEC.md) — single-asset zkCoins protocol specification
+- [`MULTI_ASSET.md`](./MULTI_ASSET.md) — permissionless multi-asset
+ extension (decision M5 defers cross-asset trading; this document is
+ one of the three out-of-protocol DEX layers)
+- [`BRIDGE_MVP.md`](./BRIDGE_MVP.md) — Phase 1 federation bridge
+- [`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md) — Phase 2 BitVM2 trustless
+ bridge
+- [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) — Lightning
+ atomic-swap layer (closest structural sibling to this document)
+- [`MIGRATION_RESEARCH.md`](./MIGRATION_RESEARCH.md) — Plonky2 migration
+ rationale; §5 (locked decisions) and §7 (lessons learned) supply the
+ decision-recipe pattern used in §3 here
+- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — project invariants,
+ pre-push checklist
+
+**External references:**
+
+- Arkade Labs blog — [*Press Start — Arkade Goes Live*](https://blog.arklabs.xyz/press-start-arkade-goes-live/)
+- Arkade Labs blog — [*Native Assets on Bitcoin: Introducing Arkade
+ Assets*](https://blog.arklabs.xyz/native-assets-on-bitcoin-introducing-arkade-assets/)
+- Arkade Labs blog — [*Closing the Lightning Loop*](https://blog.arklabs.xyz/closing-the-lightning-loop-bitcoins-missing-layer-secretly-goes-live/)
+- Arkade docs — `docs.arkadeos.com` (HTLC template, Escrow, Spilman
+ channel, Dryja-Poon channel, Lightning swaps, Arkade Script)
+- Arkade compiler — [arkade-os/compiler](https://github.com/arkade-os/compiler)
+- Arkade daemon — [arkade-os/arkd](https://github.com/arkade-os/arkd)
+- BitVM bridge whitepaper — [bitvm.org/bitvm_bridge.pdf](https://bitvm.org/bitvm_bridge.pdf)
+- Shielded CSV publishing site — [shieldedcsv.org](https://shieldedcsv.org)
+
+---
+
+## 14. Change Log
+
+| Date | Change |
+| ---- | ------ |
+| 2026-05-23 | Initial draft. Locked decisions A1–A6; HTLC atomic-swap protocol of §7; pipeline use of §6.3; trust-model stacking of §8. |
diff --git a/BITVM_BRIDGE.md b/BITVM_BRIDGE.md
new file mode 100644
index 00000000..88555feb
--- /dev/null
+++ b/BITVM_BRIDGE.md
@@ -0,0 +1,1125 @@
+# BitVM Bridge — Trustless Mint/Burn for zkCoins
+
+**Status:** Design draft. No code yet. Companion to `SPEC.md`
+(specifically D11), `MIGRATION_RESEARCH.md`, `ROADMAP.md`, and
+[`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md).
+
+**Authoritative source for:** how zkCoins removes the operator-controlled
+mint (D11) by binding mint operations to provable BTC custody on Bitcoin
+L1 via a BitVM2-style bridge.
+
+**Audience:** Engineers and stakeholders evaluating zkCoins's path from
+MVP-with-trusted-issuer to mainnet-with-cryptographic-issuance.
+
+> **Branch note.** This document presupposes the Plonky2 migration
+> currently on `feat/plonky2-migration` (PR #17). `SPEC.md`,
+> `MIGRATION_RESEARCH.md`, and `ROADMAP.md` live on that branch and
+> will resolve on `develop` only after PR #17 lands. Until then, view
+> cross-references against `feat/plonky2-migration`.
+
+---
+
+## 1. Scope
+
+This document specifies what it would take to make zkCoins coin issuance
+**trustless** by replacing the hard-coded `MINTING_ADDRESS` with a
+BitVM2-bridge-anchored mint mechanism. Concretely:
+
+- The exact trust model of BitVM2 bridges as deployed by Citrea
+ (Clementine) and others as of 2026-05
+- How a BitVM2 bridge would integrate with the zkCoins state-transition
+ circuit
+- What new circuit branch (`IssuanceProof` per Shielded CSV paper) needs
+ to exist
+- The federation setup, trusted setup ceremony, and operational burden
+- The peg-in (BTC → zkCoin) and peg-out (zkCoin → BTC) flows
+- Trust assumptions in plain terms (where 1-of-N suffices, where N-of-N
+ is required, where the user trusts no one)
+- Open issues, cost estimates, and what it does *not* solve
+
+It does **not** cover:
+
+- BitVM1 (superseded by BitVM2 for bridges)
+- BitVM3 (research-stage, not production-ready as of 2026-05)
+- Non-bridge BitVM use cases (general computation)
+- Lightning swap layer — that lives in `LIGHTNING_ATOMIC_SWAP.md`
+
+---
+
+## 2. The Problem Restated
+
+### 2.1 D11 today
+
+Per `program/src/lib.rs:70-73` and `program/src/main.rs:78-83`, the
+`InitialProof` branch of the state-transition circuit contains:
+
+```rust
+ProofType::InitialProof => {
+ if account_state.owner != MINTING_ADDRESS {
+ assert_eq!(account_state.balance, 0, "Starting balance has to be 0.")
+ }
+ DEFAULT_HASHES[0]
+}
+```
+
+Anyone holding the private key to the public key whose hash is
+`MINTING_ADDRESS` can produce an `InitialProof` with arbitrary starting
+balance — effectively unlimited mint authority. There is no on-chain
+binding, no cap, no audit constraint.
+
+In the closed-test environment (`feedback_zkcoins_closed_test_env`) and
+under the MVP-publisher self-issuance model (`MIGRATION_RESEARCH.md`
+§5.6) this is acceptable. It is **not** acceptable for any mainnet
+launch that claims trust-minimised properties over the issued asset.
+
+### 2.2 What "trustless mint" means here
+
+The user of a zkCoin must be able to verify, without trusting any
+single party, that **the total supply of zkCoins outstanding does not
+exceed the BTC locked in publicly verifiable on-chain custody**.
+
+Equivalently: every coin in circulation must trace its provenance back
+to a BTC peg-in on Bitcoin L1, and the protocol must prevent
+inflationary mints.
+
+### 2.3 What BitVM2 provides
+
+BitVM2 (specifically the Clementine bridge architecture as deployed by
+Citrea) provides exactly this binding: a Bitcoin-L1-anchored mechanism
+where:
+
+- BTC enters the bridge via deposit into an N-of-N MuSig Taproot vault
+- A side-system mint is authorised only when a Bitcoin Light Client
+ proof shows the deposit is final
+- Withdrawals back to Bitcoin require fronting by operators and are
+ optimistically verified, with on-chain disproof via Groth16 SNARK
+ verification baked into Bitcoin script
+
+Trust model: **1-of-N honesty per role**. As long as one signer deletes
+their key honestly at setup, one operator advances payouts honestly,
+and one challenger watches for fraud, the bridge holds.
+
+---
+
+## 3. BitVM2 / Clementine — Architecture in Detail
+
+This section is a precise read of the Citrea Clementine implementation
+as of 2026-05. References at the end.
+
+> **2026 context** (added 2026-05-17): BitVM2 is currently the only
+> trustless-bridge construction with a live mainnet deployment (Citrea
+> launched 2026-01-27). Three credible successors have emerged in
+> 2025–2026 — BitVM3-RSA (withdrawn after security flaw), Glock by
+> Alpen Labs (research/testnet-stage), and Mosaic by Eagen et al.
+> (research-stage, full Rust implementation). All three use garbled
+> circuits + cut-and-choose + adaptor signatures to push BitVM2's
+> on-chain Assert footprint down by 100–1000×. See §12 for a survey
+> of these alternatives and what it means for zkCoins's bridge choice.
+> The fundamentals of §3 (peg-in/peg-out flow, roles, 1-of-N honesty
+> assumption) remain identical across all BitVM-family bridges; the
+> innovations target the fraud-proof step specifically.
+
+### 3.1 Roles
+
+| Role | Function | Quorum |
+| ---- | -------- | ------ |
+| **User** | Initiates peg-in (locks BTC) or peg-out (burns side-chain asset) | — |
+| **Signers** | Pre-sign every spending path of every UTXO in the bridge graph at setup. Must delete keys after presigning. | N-of-N MuSig (all participate) |
+| **Operators** | Front BTC payouts to peg-out users from their own funds; later reimbursed from the vault | 1-of-N — any operator can serve any payout |
+| **Watchtowers** | Monitor Bitcoin chain and bridge state; publish header-chain proofs during disputes | 1-of-N |
+| **Challengers** | Permissionless — anyone can detect and challenge fraudulent operator claims | Permissionless |
+
+Hierarchy: every Signer is also an Operator and Watchtower; Challengers
+can be anyone (no membership required).
+
+### 3.2 Setup ceremony — N-of-N MuSig
+
+Once per bridge deployment, the N signers must:
+
+1. Generate fresh Schnorr keypairs
+2. Aggregate to a MuSig2 vault key
+3. Construct the **entire transaction graph** of allowed spending
+ paths: peg-in `MovetoVault`, peg-out `Payout`, `KickOff`,
+ `Challenge`, `Assert`, `Disprove`, `Take1`, `Take2`, `Burn`,
+ timeout refunds
+4. Pre-sign all of these with the N-of-N MuSig
+5. **Delete the per-signer private keys**
+
+The deletion step is the security crux. As long as **at least one
+signer actually deletes**, no future coalition can spend the vault
+outside the pre-signed paths. This is the **"1-of-N honesty"
+assumption**.
+
+### 3.3 Groth16 verifier on Bitcoin
+
+For fraud-proof verification, BitVM2 implements a **Groth16 verifier in
+Bitcoin script**, split into sub-programs each small enough to fit in
+a Bitcoin block. When an operator's claim is challenged, the operator
+must commit to intermediate computation states on-chain. A challenger
+who detects a wrong intermediate state executes the corresponding
+sub-program on-chain to disprove the operator's claim.
+
+This requires:
+
+- A **trusted setup ceremony** for the Groth16 SRS. Citrea ran theirs
+ with 63 contributors from RiscZero, StarkWare, Aztec, Celestia,
+ Babylon, Nansen, etc. — `MIGRATION_RESEARCH.md`-grade table.
+- The proven statement: the operator's payout transaction is included
+ in a finalized Bitcoin chain with accumulated work greater than the
+ watchtower's submitted header chain.
+
+### 3.4 Peg-in flow (BTC → bridged asset)
+
+```
+Step 1. User deposit: User sends BTC to a Taproot address with two
+ leaves:
+ - Bridge leaf: spendable by the N-of-N MuSig signature,
+ with witness binding to the user's side-chain receiving
+ address
+ - Refund leaf: spendable by user after 200 blocks (CSV)
+
+Step 2. Vault transfer: Signers cooperatively spend the deposit into
+ the operational vault UTXO using the pre-signed MovetoVault
+ transaction. The pre-signature binds the spend to exactly
+ the right operational vault path — no signer can divert.
+
+Step 3. Confirmation wait: Vault transfer needs 6+ Bitcoin confirms.
+
+Step 4. Mint authorization: The side-chain (zkCoins in our case)
+ reads the Bitcoin Light Client proof and confirms the
+ MovetoVault tx is in the finalized chain. The mint amount
+ equals the deposit amount minus fees, credited to the user's
+ side-chain address.
+```
+
+The refund path protects the user against bridge stalls — if the
+signers fail to advance the deposit within 200 blocks, the user
+recovers via the refund leaf.
+
+### 3.5 Peg-out flow (bridged asset → BTC)
+
+```
+Step 1. Burn: User invokes the side-chain's burn function. On Citrea
+ this is `safeWithdraw` on a contract; for zkCoins it would be
+ a coin-send to a designated BURN_ADDRESS or — paper-aligned —
+ a dedicated BurnProof predicate branch (§6.3).
+
+Step 2. Payout request: User submits a Payout transaction template
+ signed with SIGHASH_SINGLE|ANYONECANPAY, identifying their
+ BTC receiving address and amount.
+
+Step 3a. Optimistic path: If all N Signers are online and cooperative,
+ they collectively sign the Payout, releasing BTC from the
+ vault directly to the user. ~12 hours typical wait for signer
+ convening.
+
+Step 3b. Operator-fronted path: If signers don't convene in 12 hours,
+ an Operator funds the user out of their own wallet by adding
+ their own input to the Payout template and broadcasting.
+ User gets BTC immediately.
+
+Step 4. Operator reimbursement (only path 3b):
+ - Operator posts KickOff tx claiming reimbursement from
+ the vault, citing the peg-out they fronted
+ - Challenge window opens (1.5 days)
+ - Watchtowers + permissionless challengers can verify the
+ KickOff against the actual chain
+ - If no valid challenge: Operator posts NoChallenge tx
+ after timelock, retrieves reimbursement from vault
+ - If valid challenge: BitVM2 Groth16 verifier runs on-chain;
+ operator must produce ZK Light Client Proof showing their
+ committed chain has greater cumulative work AND includes
+ the legitimate payout
+ - Honest operator wins; malicious operator's entire bond is
+ slashed and they're ejected from the set
+
+Step 5. The same Payout template cannot be used twice — anti-replay.
+```
+
+### 3.6 Key timeouts and security parameters
+
+| Parameter | Value | Why |
+| --------- | ----- | --- |
+| Peg-in refund timeout | 200 Bitcoin blocks (~33 hours) | User recovers funds if bridge stalls |
+| Optimistic payout convening window | 12 hours | Signer assembly time before operator-fronted path activates |
+| Challenge window | 1.5 days (~216 blocks) | Permissionless dispute initiation |
+| Security analysis horizon | 2 weeks | Maximum reorg attempt window |
+| Hash rate adversary cap | < 45% | Below which the chain proof remains correct |
+
+### 3.7 Trust assumptions in plain terms
+
+A user holding bridged BTC trusts that:
+
+- **At least one of N signers deleted their keys** at setup (after
+ pre-signing). With Citrea's federation of ~20 members from
+ competing organisations, the probability of zero honest deletions
+ is extremely low but non-zero — this is the residual trust.
+- **At least one operator** is willing to advance peg-outs (else
+ liveness — funds are not stolen but become inaccessible until any
+ operator returns).
+- **At least one watchtower or challenger** is monitoring (else
+ fraudulent operator claims can succeed unchallenged).
+- **Bitcoin's < 45% adversary assumption** holds for the 2-week
+ challenge horizon (standard Bitcoin assumption).
+
+These are weaker assumptions than any federated bridge (Liquid, RSK)
+and stronger than any client-side-verifying chain (which has no bridge
+at all).
+
+---
+
+## 4. What Changes in zkCoins
+
+### 4.1 Circuit changes (`program/`, `program-plonky2/`)
+
+A new `ProofType` variant, paper-aligned with the Shielded CSV
+`issuance(IssuanceProof)` branch:
+
+```rust
+pub enum ProofType {
+ InitialProof,
+ AccountUpdateProof,
+ IssuanceProof, // NEW
+ BurnProof, // NEW — counterpart for peg-out
+}
+```
+
+The `IssuanceProof` branch replaces the current `MINTING_ADDRESS`
+bypass. Instead of trusting `owner == MINTING_ADDRESS`, the circuit
+verifies a **Bitcoin Light Client Proof (LCP)** witnessing that:
+
+- A specific peg-in UTXO (identified by txid and vout) has been
+ confirmed at depth ≥ 6 in the Bitcoin chain
+- The peg-in UTXO's amount equals the issuance amount
+- The peg-in UTXO has not been used as the basis of any prior
+ `IssuanceProof` (uniqueness — tracked in a new
+ `peg_in_consumed_smt`)
+- The peg-in UTXO's witness data binds to the recipient zkCoins
+ address (so only the intended recipient can mint against that
+ deposit)
+
+The `BurnProof` branch handles the peg-out side:
+
+- A coin is "consumed" by producing a `BurnProof` against it
+- The proof emits a public output containing
+ `(burn_amount, btc_recipient, withdrawal_nonce)` that the bridge
+ operator picks up to construct the Bitcoin Payout transaction
+- The burned coin's identifier is added to a `burned_coins_smt` so
+ it cannot be double-burned
+
+### 4.2 New state structures (`node/src/state.rs`)
+
+Three additions to the global state:
+
+```rust
+struct State {
+ // ... existing fields (smt, mmr, prev_mmr_root, root_indices)
+
+ // NEW: peg-ins that have been consumed by an IssuanceProof
+ peg_in_consumed_smt: SparseMerkleTree,
+
+ // NEW: coins that have been burned (peg-out initiated)
+ burned_coins_smt: SparseMerkleTree,
+
+ // NEW: pending peg-outs waiting for operator fronting
+ pending_payouts: Map,
+}
+```
+
+### 4.3 New off-circuit responsibilities
+
+The scanner gains:
+
+- Watching the bridge vault UTXO and any deposits to it
+- Maintaining a local Bitcoin Light Client (header chain + cumulative
+ work) — likely implemented via SP1's `bitcoin-spv` precompile or an
+ equivalent in Plonky2
+- Detecting peg-out completion (operator broadcasts Payout tx),
+ marking pending payouts as completed
+
+### 4.4 Federation participation
+
+This is the heaviest organisational change. zkCoins becomes a **member
+of a BitVM2 federation**, which requires:
+
+- Coordinating with N-1 other federation members at setup
+- Participating in the trusted setup ceremony for the Groth16 verifier
+- Continuously running a signer node, operator node, watchtower node
+- Maintaining operator collateral (BTC bond)
+
+Realistically, zkCoins cannot operate a single-member "federation" of
+size 1 and call itself trustless. The minimum credible size is ~5–7
+members from independent organisations. Citrea uses ~20.
+
+### 4.5 What does NOT change
+
+- The zkCoins coin model itself (`Coin { identifier, recipient,
+ amount }`) — D11 fix does not require D2 fix
+- The Schnorr/SHA256 boundary at the wallet (BIP-340 still off-circuit)
+- The SMT/MMR scanner architecture for normal sends
+- The Lightning atomic swap design — `LIGHTNING_ATOMIC_SWAP.md`
+ remains correct, and a swap liquidity provider becomes anyone
+ with bridge deposit/withdraw capability instead of relying on a
+ single sole minter
+
+---
+
+## 5. Detailed Flow A: Peg-In (BTC → zkCoin)
+
+### 5.1 Pre-conditions
+
+- User has BTC on Bitcoin L1
+- User has a zkCoins account (knows their `recipient = H(initial_pubkey)`)
+- Bridge federation is operational, vault UTXO exists, all
+ pre-signatures in place
+
+### 5.2 Protocol steps
+
+```
+Step 1. User constructs a deposit tx with a Taproot output containing
+ two leaves:
+ - Bridge leaf: vault_musig_pubkey, with witness commitment
+ to user's zkcoins recipient address
+ - Refund leaf: user_pubkey + 200-block CSV
+ User broadcasts.
+
+Step 2. Bridge federation observes the deposit. Signers cooperatively
+ spend it into the operational vault UTXO using the pre-signed
+ MovetoVault transaction (the pre-signature is parameterised
+ on the user's zkcoins address, embedded in the deposit's
+ witness commitment).
+
+Step 3. MovetoVault tx confirms (≥6 confirms). At this point the
+ peg-in is finalized on Bitcoin.
+
+Step 4. User (or their wallet, or any helper service) generates a
+ Bitcoin Light Client Proof showing MovetoVault is in the
+ canonical chain at depth ≥ 6.
+
+Step 5. User submits to a zkCoins server an IssuanceProof request:
+ - Their account state (initial, balance = 0)
+ - The Bitcoin LCP for MovetoVault
+ - The peg-in UTXO outpoint
+ - The non-inclusion proof against peg_in_consumed_smt
+
+Step 6. zkCoins server (or the user's own prover, in a more
+ decentralised future) generates the IssuanceProof:
+ - Verifies the Bitcoin LCP
+ - Verifies the deposit amount equals the requested mint
+ - Verifies the witness commitment binds the deposit to
+ this account
+ - Verifies non-inclusion in peg_in_consumed_smt and inserts
+ - Emits ProofData with the user's new account state
+ (balance = deposit_amount − bridge_fee) and the standard
+ commitment_history / coin_history fields
+
+Step 7. User signs the Schnorr commitment H(asth ‖ ocr) (same as any
+ send). User or their operator publishes the inscription.
+ Scanner picks up, state updates.
+
+Step 8. User now has zkCoins backed by the locked BTC. Total supply
+ increased by exactly the deposit amount.
+```
+
+### 5.3 Refund path
+
+If Step 2 doesn't happen within 200 blocks (e.g., federation offline
+or unwilling to process this deposit), the user spends the deposit
+back to themselves via the refund leaf. No interaction with zkCoins
+needed.
+
+### 5.4 Failure modes
+
+| Failure | Recovery |
+| ------- | -------- |
+| Federation refuses to MovetoVault | Refund leaf after 200 blocks |
+| Vault sweeps multiple deposits without proper mint authorisation | Pre-signing prevents this (vault can only spend via pre-signed paths) |
+| User's LCP is forged or stale | Circuit re-verifies LCP from headers; forgery requires breaking PoW |
+| Bitcoin reorg removes MovetoVault | LCP becomes invalid; user retries after deeper confirmation |
+| zkCoins server malicious — refuses to generate IssuanceProof | User goes to another zkCoins server (server-side compute is replicable; any party with the protocol can mint). This requires multiple zkCoins servers to exist; currently single-server. |
+
+### 5.5 The "user pays an operator to mint" alternative
+
+The above puts proof generation on the user side (or their chosen
+zkCoins server). A simpler MVP variant: the federation includes
+zkCoins-server operators who automatically generate the IssuanceProof
+when they see a confirmed MovetoVault. This is more centralised but
+operationally simpler. Trade-off documented as open question §10.
+
+---
+
+## 6. Detailed Flow B: Peg-Out (zkCoin → BTC)
+
+### 6.1 Pre-conditions
+
+- User has zkCoins they wish to redeem for BTC
+- Vault has sufficient BTC inventory to fund the payout
+- At least one operator is online and has sufficient liquid BTC to
+ front the payout
+
+### 6.2 Protocol steps
+
+```
+Step 1. User produces a BurnProof against their coin(s):
+ - Inputs: coin(s) to burn, valid inclusion proofs from
+ their source proofs
+ - Public outputs: ProofData { burn_amount, btc_recipient,
+ withdrawal_nonce, ... }
+ - The burn registers each coin in burned_coins_smt
+
+Step 2. User publishes the burn inscription (same `4242`-prefix
+ Taproot mechanism as a regular send). Scanner picks up, state
+ updates burned_coins_smt and registers the pending payout in
+ the bridge's pending_payouts queue.
+
+Step 3. User signs a Payout transaction template:
+ - Output: btc_recipient gets burn_amount − fees
+ - Input slot: SIGHASH_SINGLE|ANYONECANPAY, signed by user;
+ requires an operator to add their own funding input
+ User submits this template to the bridge.
+
+Step 4. Optimistic path (12-hour signer convening):
+ - Signers verify the BurnProof landed and pending_payouts
+ has the corresponding entry
+ - Signers collectively sign the Payout against the vault
+ - User receives BTC; vault is reduced
+
+Step 5. Operator-fronted path (if optimistic path stalls):
+ - An operator adds their UTXO as input, signs, broadcasts
+ - User receives BTC immediately
+ - Operator initiates reimbursement via KickOff
+ - Challenge window 1.5 days
+ - If no challenge: operator claims reimbursement from
+ vault
+ - If challenged: BitVM2 game decides; honest operator
+ wins, malicious one is slashed
+
+Step 6. Bridge marks the pending_payout as completed; the same
+ BurnProof cannot trigger another payout (replay protection
+ via withdrawal_nonce uniqueness in pending_payouts).
+```
+
+### 6.3 The BurnProof — circuit specifics
+
+The `BurnProof` branch in the circuit:
+
+- Asserts at least one input coin
+- Asserts no output coins (or only a "change" output coin for the
+ amount minus burn)
+- Asserts `burn_amount > 0` and `burn_amount ≤ sum_inputs`
+- Asserts each burned coin's identifier is inserted into
+ `burned_coins_smt`
+- Asserts `withdrawal_nonce` is a fresh value (e.g., random
+ field-element committed at burn time, never seen before in
+ `withdrawal_nonces_smt`)
+- Emits `btc_recipient` as 20- or 32-byte Bitcoin address as a public
+ output field
+
+### 6.4 Failure modes
+
+| Failure | Recovery |
+| ------- | -------- |
+| User burns but signers/operators refuse to pay | Fraud — the BurnProof is on-chain (in zkCoins state), the user has a permanent record. After protocol-defined dispute window, governance recourse via federation slashing. Recommended: hard timeout — if 30 days without payout, the burn entry expires and can be re-issued as a fresh mint to the user (requires extra circuit branch, not in v1) |
+| Operator double-claims reimbursement | KickOff replay protection — same Payout template can't be used twice; BitVM2 enforces |
+| Operator fronts and is slashed for fraud | User already received their BTC (the Payout completed before challenge window); operator loses bond. Bridge is intact. |
+| Vault doesn't have enough BTC | Pre-condition failure; bridge must reject burn requests above vault capacity, or queue them |
+
+---
+
+## 7. Sequencing — What Comes Before What
+
+A realistic implementation sequence:
+
+| Phase | Item | Effort | Dependencies |
+| ----- | ---- | ------ | ------------ |
+| 0 | Plonky2 cutover complete (`feat/plonky2-migration` merged) | Already in progress | — |
+| 0 | D2/D10 (hiding recipient) and D7 (reorg safety) closed | Pre-mainnet hardening, 2–3 weeks | — |
+| 1 | Decide bridge model: BitVM2 vs Liquid-style federation | Strategy decision | — |
+| 2a | Federation recruitment — ~5–7 independent organisations agree to participate | Org-level — months | Decision in Phase 1 |
+| 2b | Trusted setup ceremony for Groth16 | 2–4 weeks elapsed, ~63 contributor invitations | 2a |
+| 3 | Bitcoin Light Client gadget in circuit | 2–3 weeks | Phase 0 |
+| 4 | `IssuanceProof` circuit branch | 2 weeks | Phase 0, Phase 3 |
+| 5 | `BurnProof` circuit branch | 1–2 weeks | Phase 0 |
+| 6 | Bridge server-side state (peg_in_consumed_smt, burned_coins_smt, pending_payouts) | 1 week | Phase 4, Phase 5 |
+| 7 | Federation node software (signer + operator + watchtower roles) | 4–6 weeks | Phase 2a, Phase 6 |
+| 8 | Integration testing with all federation members on signet | 2–4 weeks | Phase 7 |
+| 9 | Mainnet launch | TBD | Phase 8 |
+
+**Aggregate effort:** 4–6 months engineering for the zkCoins-specific
+code (Phases 3–6), plus 2–6 months for federation coordination and
+trusted setup (Phases 2a–2b). Realistically 6–9 months elapsed time
+to a credible mainnet bridge.
+
+This is **substantial** — comparable to Citrea's bridge timeline. It
+also fundamentally changes zkCoins from a single-operator MVP into a
+multi-party federated infrastructure project.
+
+---
+
+## 8. Realistic Alternatives at Lower Cost
+
+Not every product needs full BitVM2. Three lower-cost alternatives,
+ordered from most to least trust-minimised:
+
+### 8.1 Liquid-style federation (Liquid Network, Blockstream)
+
+A k-of-n multisig federation holds the BTC. Mints are authorised by
+the federation's signing. No on-chain fraud proofs; trust is "honest
+majority of federation".
+
+- **Trust model:** k-of-n (typically 11-of-15 for Liquid)
+- **Effort:** weeks (just multisig + a side-chain mint authorisation
+ flow)
+- **Trade-off:** explicitly trusts the federation majority; if k
+ members collude, BTC can be stolen
+
+This is **what a single-organisation issuer could realistically run
+today** with existing infrastructure. It is **not** trustless in the
+BitVM2 sense, but it is trust-distributed and well-understood by the
+market.
+
+### 8.2 Optimistic bridge with permissionless challenge (no SNARK on Bitcoin)
+
+A 1-of-n optimistic bridge where withdrawals can be challenged for
+a window, but the challenge mechanism is off-chain (challenger
+publishes a fact and the federation slashes operators by
+governance), not via Bitcoin script SNARK verification.
+
+- **Trust model:** 1-of-n honesty assumption, but recourse is
+ governance not cryptography
+- **Effort:** 2–4 months
+- **Trade-off:** cheaper than BitVM2 but legally/socially harder to
+ enforce slashing
+
+### 8.3 Federated peg with hardware-secured signers
+
+The k-of-n federation runs HSMs that enforce policy in firmware (e.g.,
+"only sign payouts that match a corresponding burn observed in the
+side-chain state"). Adds hardware-level enforcement to 8.1.
+
+- **Trust model:** k-of-n federation + HSM vendor + firmware
+- **Effort:** 1–3 months
+- **Trade-off:** depends on HSM security, vendor trust
+
+### 8.4 Recommendation
+
+For a single-organisation-led zkCoins launch, **8.1 (Liquid-style)
+is the realistic short-term path**. BitVM2 is the long-term
+aspiration but requires federation recruitment and trusted setup
+ceremony coordination that do not fit a self-funded single-org
+timeline.
+
+The migration path is clean: a Liquid-style bridge in v2 can be
+upgraded to a BitVM2 bridge in v3 by replacing the trust model at
+the federation layer without changing the circuit's `IssuanceProof`
+contract.
+
+---
+
+## 9. Privacy Implications
+
+### 9.1 Peg-in observability
+
+The user's deposit on Bitcoin L1 is visible. Anyone watching the
+bridge vault UTXO sees:
+
+- The deposit amount
+- The user's Bitcoin address(es) used to fund
+- The MovetoVault tx and its timing
+- Eventually, the corresponding inscription on Bitcoin (via the
+ `4242` prefix) — even if the recipient address inside is hidden
+ (post-D2/D10), the temporal correlation of "deposit X confirmed
+ at time T, inscription Y appeared at time T+δ" is observable.
+
+This is **a privacy regression compared to a fully off-chain mint**
+where the user could mint without Bitcoin L1 exposure. It is **a
+privacy improvement compared to L1 BTC** (after the mint, all
+subsequent zkCoins transfers are private off-chain).
+
+### 9.2 Peg-out observability
+
+Symmetric. The user's BTC withdrawal address is on L1. The temporal
+correlation of "burn at time T, BTC arrives at user's address at time
+T+δ" links the on-chain zkCoins burn with the destination address.
+
+### 9.3 Mitigations
+
+- **Stealth peg-in:** the witness commitment to the recipient address
+ in the deposit's Taproot leaf can use a hiding commitment with
+ per-deposit randomness. Bridge federation sees the commitment but
+ not the actual recipient address. This is a privacy gain only if
+ the recipient address is also hidden in the issued coin (i.e., D2
+ is fixed).
+- **Per-deposit fresh addresses:** the user uses a fresh Bitcoin
+ address for each deposit. Standard hygiene.
+- **Coinjoin on peg-out:** the user mixes their burned BTC payout
+ with others via a separate coinjoin step after withdrawal. Adds
+ latency but breaks the on-chain link.
+
+### 9.4 Net assessment
+
+zkCoins-with-bridge has **less privacy than zkCoins-without-bridge**
+(the bridge adds L1 touch points), but **more privacy than any other
+BTC L2 with a bridge** because intra-zkCoins transfers remain fully
+private off-chain. The privacy story is "BTC enters the shielded
+zone, moves privately, BTC exits the shielded zone" — comparable to
+Zcash's t/z address model.
+
+---
+
+## 10. Open Questions
+
+1. **Who pays for proof generation in Phase 4–5?** Server-side
+ (zkCoins operator) is operationally simpler; user-side
+ (decentralised) is more trustless. Default: server-side for v1
+ with a clear migration path to user-side later.
+
+2. **Federation size and composition.** Minimum credible: 5
+ independent orgs. Target: 15+ for parity with Liquid. Who? Other
+ Swiss-regulated crypto entities, exchanges, custody providers,
+ academic institutions. This is mostly a business-development
+ question, not engineering.
+
+3. **Trusted setup ceremony logistics.** Coordinate with the BitVM
+ community for a shared SRS, or run a zkCoins-specific ceremony?
+ Citrea ran theirs because their predicate (RiscZero → Groth16) is
+ specific. zkCoins's predicate is also specific (Plonky2 verifier
+ wrapper → Groth16), so likely a dedicated ceremony — but the
+ ceremony tooling itself is reusable from Citrea's open-source
+ release.
+
+4. **Liquidity bootstrapping.** Operators need BTC inventory to front
+ peg-outs. Where does it come from? Self-funded by federation
+ members, with fee compensation. The initiating operator can
+ plausibly bootstrap with reasonable inventory before recruiting
+ further federation members.
+
+5. **Fee model.** Bridge fees per peg-in and peg-out. Should match
+ market rates (Liquid is 0% currently; Citrea has small fees).
+ Trade-off between user adoption and federation sustainability.
+
+6. **Audit-friendly accounting.** The bridge needs a public, real-time
+ view of "total BTC in vault" vs "total zkCoins outstanding" so any
+ user can verify the bridge is solvent. This is a side-chain
+ indexer feature, not a protocol feature, but it should ship at
+ launch to avoid trust-by-default concerns.
+
+7. **Plonky2 → Groth16 wrapping.** The BitVM2 verifier is Groth16.
+ The zkCoins predicate runs in Plonky2. There must be a wrapping
+ step: prove the Plonky2 verifier in Groth16, so Bitcoin can
+ verify the wrapped Groth16 proof via BitVM2. This wrapping step
+ is the same pattern Citrea uses (RiscZero → Groth16). Tooling
+ from `chainwayxyz/bitvm-zk-verifier` is the starting point.
+
+8. **What does "trustless" mean to our users?** The legal/compliance
+ framing matters. Even BitVM2 is "1-of-N honest" — not
+ "cryptographically impossible to cheat". Marketing-correctness
+ requires care.
+
+9. **Interaction with Lightning swap layer.** Once a bridge exists,
+ the swap design in `LIGHTNING_ATOMIC_SWAP.md` can be enhanced:
+ instead of an operator providing zkCoins liquidity from their own
+ inventory, the operator could trigger a fresh peg-in within the
+ swap flow. This reduces operator capital requirements but
+ increases per-swap latency (peg-in takes 33h refund window).
+ Likely worth modelling but not implementing.
+
+---
+
+## 11. Comparison Tables
+
+### 11.1 Trust models compared
+
+| Model | Trust assumption | Slashing | Compute-on-Bitcoin |
+| ----- | ---------------- | -------- | ------------------ |
+| Today (D11) | 100% trust in the single operator-minter | None | None |
+| Liquid-style federation | k-of-n federation honest majority | Off-chain governance | None |
+| Optimistic + governance dispute | 1-of-n + governance recourse | Off-chain | None |
+| BitVM2 / Clementine | 1-of-n setup honesty + 1-of-n watchtower | On-chain via Bitcoin Groth16 verifier (~2.6 MB Assert) | Yes (Groth16) |
+| BitVM3 (cut-and-choose) | Same as BitVM2 + cut-and-choose security | On-chain via Garbled-Circuit Disprove (~60 kB Assert, ~200 B Disprove) | Yes (DV-SNARK / GC) |
+| Glock (Alpen Labs) | Same as BitVM2 + cut-and-choose | On-chain DV-SNARK based Disprove (~5 kB Assert, 430–550× cheaper than BitVM2) | Yes (DV-SNARK / GC) |
+| Mosaic (Eagen et al.) | Same as BitVM2 + cut-and-choose | On-chain footprint **independent of N** (cut-and-choose copies) via polynomial label correlation + adaptor sigs | Yes (DV-SNARK / GC) |
+| Native Bitcoin (theoretical) | 0 trust | n/a | n/a |
+
+### 11.2 BitVM family + competing GC-based verifiers (state as of 2026-05)
+
+| Construction | Year | Status | Onchain dispute cost | Bridge deployed where |
+| ------------ | ---- | ------ | -------------------- | --------------------- |
+| BitVM1 | 2023-10 | Superseded | Very high (interactive multi-round) | Theoretical only |
+| BitVM2 | 2024-08 | **Mainnet production** | ~2.6 MB Assert tx | Citrea Clementine (mainnet since 2026-01-27); GOAT (testnet V3 since 2026-01-28); Alpen Strata (signet, 10 BTC fixed denomination) |
+| BitVM3-RSA | 2025-07 | **Withdrawn** — security flaw found by Eagen / Fairgate | ~60 kB Assert, ~200 B Disprove | None |
+| BitVM3-CC (cut-and-choose) | 2026 | Research / early demo | ~$10.91 dispute on mainnet (BOB) | BOB roadmap |
+| Glock (Alpen Labs) | 2025-08 | Research → testnet | 430–550× cheaper than BitVM2 (DV-SNARK based) | Strata bridge transition planned; Starknet partnership announced |
+| Mosaic (Eagen et al.) | 2026-04 | Research, full protocol spec + Rust impl | On-chain footprint **independent of N copies** (polynomial label correlation) | None yet |
+
+**Reading guide:**
+
+- **For a launch today** (zkCoins or any other side-system): BitVM2 is
+ the only choice with a live, production-tested implementation
+ (Clementine). Citrea has been in mainnet since 2026-01-27. Tooling,
+ trusted setup ceremony output, and operational documentation all
+ exist.
+- **For a launch in 6–12 months**: Glock and Mosaic both have credible
+ implementations and academic peer review going. Either could mature
+ to production status by then. Both are 100–1000× cheaper on-chain
+ than BitVM2 and use the same 1-of-N honesty trust model with
+ cut-and-choose security.
+- **Avoid**: BitVM3-RSA (broken). Plain garbled-circuit constructions
+ without cut-and-choose (not malicious-secure).
+
+### 11.3 Realistic timelines
+
+| Target | Effort | Realistic launch |
+| ------ | ------ | --------------- |
+| Liquid-style federated bridge | 2–3 months | Q3–Q4 2026 |
+| BitVM2 bridge (zkCoins-only federation) | 6–9 months | Q1 2027 |
+| BitVM2 bridge (multi-org federation) | 9–18 months | Late 2027 |
+| Glock-based bridge | depends on Glock production-readiness | Q2–Q4 2027 (if Glock stabilises) |
+| Mosaic-based bridge | depends on Mosaic production-readiness | Q3 2027+ (still in research, full Rust impl exists) |
+
+---
+
+## 12. Beyond BitVM2 — The 2026 Verification Landscape
+
+This section was added after the initial draft. It documents the
+post-BitVM2 alternatives that emerged in 2025–2026 and explains why
+the strategic recommendation in §13 (Bottom Line) still defaults to
+BitVM2 today despite the alternatives being more efficient.
+
+### 12.1 What changed since BitVM2
+
+BitVM2 (Linus et al., 2024-08) shipped as a Bitcoin-script Groth16
+verifier split into sub-programs small enough to fit individual
+Bitcoin transactions. The Assert transaction — the on-chain message
+where the operator commits to the intermediate computation states —
+is roughly 2.6 MB. At Bitcoin's economic block space cost, this is
+expensive but not prohibitive for high-value bridges where peg-out
+volume can absorb the fee.
+
+Three follow-up constructions in 2025–2026 attack the Assert size
+specifically by replacing the on-chain Groth16 verifier with a
+garbled-circuit-based fraud-proof mechanism. The garbled circuit
+itself is too large to put on Bitcoin directly, so the constructions
+post commitments and use cut-and-choose + adaptor signatures to
+ensure that revealing the on-chain signature also reveals enough
+information to disprove a fraudulent claim.
+
+### 12.2 BitVM3 — RSA construction (2025-07) — **withdrawn**
+
+The first attempt to use garbled circuits on Bitcoin for bridges. The
+original BitVM3 paper by Robin Linus proposed an RSA-based binding
+between garbled-circuit labels and Bitcoin signatures. Achieved ~60 kB
+Assert and ~200 B Disprove on paper.
+
+**Status:** withdrawn. Liam Eagen (later author of Glock) and Fairgate
+Labs identified core security flaws in the RSA construction. The
+paper was retracted. **Do not build on this.**
+
+Subsequent work continues under the BitVM3 banner using cut-and-choose
+rather than the broken RSA binding — see BitVM3-CC by BOB and others.
+
+### 12.3 BitVM3-CC (cut-and-choose) — BOB implementation
+
+BOB's engineering team implemented a cut-and-choose variant of BitVM3
+on Bitcoin mainnet, using Verifiable Secret Sharing Schemes (VSSS)
+and adaptor signatures. Demonstrates a ~$10.91 dispute cost on
+mainnet, with the Assert transaction ~87% cheaper than BitVM2.
+
+The construction is essentially a more careful cousin of the original
+BitVM3-RSA, replacing the broken RSA binding with multi-copy
+cut-and-choose where the prover commits to N garbled-circuit copies
+and the verifier opens t of them to check honest garbling.
+
+**Status:** demonstrated on mainnet by BOB, but the broader
+"BitVM3-CC" research line is still maturing. Reasonable to track as
+a credible 2027 alternative but too early to commit to.
+
+### 12.4 Glock — Garbled Locks for Bitcoin (Alpen Labs, 2025-08)
+
+**Paper:** [eprint 2025/1485](https://eprint.iacr.org/2025/1485),
+Liam Eagen, Alpen Labs.
+
+**Core idea.** Combine garbled circuits with a novel and very compact
+**designated-verifier SNARK (DV-SNARK)** to bind the garbled circuit's
+input/output labels directly to Bitcoin lock data. The DV-SNARK is
+constructed over binary elliptic curves, which Eagen argues have
+"nice synergies" with the garbled-circuit scheme.
+
+**Why it's smaller than BitVM2.** BitVM2 verifies Groth16, which is a
+universal SNARK and requires a verifier circuit of fixed substantial
+size when reduced to Bitcoin script. Glock's DV-SNARK is designed for
+a single specified verifier (the bridge contract), so it's
+specialised and much smaller. Combined with garbled-circuit fraud
+proofs, total on-chain footprint per dispute is in the few-kB range
+rather than MB.
+
+**Architecture sketch.**
+
+- **Setup:** federation runs cut-and-choose for honesty of N garbled
+ copies. t copies are opened publicly to prove honesty; the
+ remaining N-t are used for actual fraud-proof evaluation.
+- **Operator commits to a peg-out claim** by posting a signature
+ whose adaptor binds to a label of the garbled circuit.
+- **If the claim is wrong:** the corresponding garbled-circuit
+ evaluation reveals a secret that lets anyone reconstruct a "fraud
+ signature" — punishing the operator on-chain.
+- **If the claim is right:** no further on-chain interaction needed;
+ the operator's adaptor signature settles after timelock.
+
+**Efficiency.** Claimed 430–550× on-chain efficiency over BitVM2.
+Concretely: instead of a 2.6 MB Assert transaction, Glock's
+disprove-related on-chain footprint is in the few-kB range.
+
+**Status (2026-05).**
+
+- Paper published August 2025
+- Alpen Labs is building Glock into their Strata bridge as the
+ successor to the current BitVM2-based Strata bridge implementation
+- Starknet announced a strategic partnership with Alpen Labs in
+ October 2025 to use Glock as Starknet's BTC bridge primitive
+- **No mainnet deployment yet.** Strata's BitVM2 bridge runs on
+ Bitcoin signet only as of 2026-05; Glock transition is on the
+ roadmap, not live.
+- Research is active and the academic peer-review pipeline
+ is moving — multiple follow-up papers (Mosaic, Argo) build on or
+ refine Glock's primitives.
+
+**What this means for zkCoins.** Glock is the **most attractive 2026
+alternative** to BitVM2 if zkCoins is willing to wait. Its 1-of-N
+trust model is identical to BitVM2's; its on-chain cost is 100–1000×
+lower; and the construction is by the same team that wrote the
+Shielded CSV paper (Eagen, Linus). The fit is essentially perfect.
+
+The risk: it has not yet been deployed on mainnet by anyone. Glock
+**does require a circuit-specific trusted setup** — its DV-SNARK is
+instantiated with Pari (Eagen et al., eprint 2024/1245), and the
+Pari paper states explicitly: *"Pari requires a circuit-specific
+trusted setup, but the relevant prior work (namely, Groth16) also
+requires such a setup."* So the setup-coordination burden is
+comparable to BitVM2/Groth16, not eliminated. The advantage of
+Glock over BitVM2 is on-chain efficiency and proof size (Pari is
+the smallest known SNARK at 160 bytes), not setup transparency.
+
+### 12.5 Mosaic — Practical Malicious Security for Garbled Circuits on Bitcoin (Eagen et al., 2026-04)
+
+**Paper:** [eprint 2026/812](https://eprint.iacr.org/2026/812),
+Khambhati, Tiwari, Bajracharya, Bista, Eagen, Lewe, Feickert.
+
+**Core idea.** Where Glock uses DV-SNARKs to achieve compactness,
+Mosaic stays with traditional Groth16 verifier circuit but achieves
+malicious security via **cut-and-choose with polynomial label
+correlation**. The trick: labels across all N garbled copies are
+arranged as evaluations of a degree-t polynomial. The t shares
+revealed during cut-and-choose fall one short of the reconstruction
+threshold. Adaptor signatures ensure that the prover's on-chain
+witness commitment reveals the missing share as a byproduct. The
+evaluator can then reconstruct labels for all unchallenged copies by
+interpolation.
+
+**Killer feature.** The on-chain footprint is **independent of N**
+(the number of garbled copies used for cut-and-choose). Other
+cut-and-choose constructions need to post per-copy data on-chain
+that scales with N. Mosaic eliminates this scaling.
+
+**Practical.** Full protocol specification, Rust implementation,
+instantiated for trust-minimized Bitcoin bridging with a Groth16
+verifier circuit.
+
+**Status (2026-05).**
+
+- Paper published April 2026
+- Rust implementation exists (open-source per paper)
+- No production deployment yet
+- Same author family as Glock and Shielded CSV (Eagen)
+- Cleanly compatible with the existing Groth16-verifier ecosystem
+ (Plonky2 → Groth16 wrapping pipeline that Citrea uses works
+ unchanged)
+
+**What this means for zkCoins.** Mosaic is **the cleanest drop-in
+replacement** for BitVM2 because it keeps Groth16 as the verifier and
+therefore reuses the entire BitVM2 toolchain (trusted setup ceremony,
+Groth16 prover tools, `chainwayxyz/bitvm-zk-verifier`). It just cuts
+the Assert transaction footprint by a large factor.
+
+The risk: it's the youngest of the three (April 2026 paper). Has not
+seen the same testnet hours as Glock or production hours as BitVM2.
+
+### 12.6 Production state of major BitVM bridges (2026-05)
+
+| Bridge | Side-system | Construction | Status |
+| ------ | ----------- | ------------ | ------ |
+| Clementine | Citrea | BitVM2 | **Mainnet since 2026-01-27** |
+| GOAT Network bridge | GOAT Network | BitVM2 variant | **Testnet V3 since 2026-01-28** (permissionless-exit-first design) |
+| Strata bridge | Alpen | BitVM2 (Glock transition planned) | **Signet only**, 10 BTC fixed denomination, 64-block operator timeout, 36-block challenge |
+| BOB bridge | BOB | BitVM3-CC | Mainnet demo (cost-reduction proof of concept) |
+| Bitlayer bridge | Bitlayer | BitVM2 variant | Mainnet |
+
+**Reading guide.** As of May 2026, **only BitVM2 (and direct variants)
+have any mainnet exposure**. Everything garbled-circuit-based —
+BitVM3-CC, Glock, Mosaic — is at most demo or testnet. This will
+likely change over Q3–Q4 2026 as Strata and BOB push their Glock /
+BitVM3-CC bridges toward mainnet.
+
+### 12.7 Strategic implication for zkCoins
+
+If we were starting bridge implementation **today**:
+
+- BitVM2 / Clementine fork. Battle-tested, mainnet-proven, with
+ reusable trusted setup output. Trade-off: 2.6 MB Assert tx (~$60–200
+ at common fee rates).
+
+If we were starting bridge implementation **in Q3–Q4 2026**:
+
+- Wait for Strata's Glock transition or BOB's BitVM3-CC mainnet
+ hardening, then fork from there. Trade-off: more time before
+ zkCoins has a bridge, much cheaper on-chain dispute resolution.
+
+If we want to **hedge**:
+
+- Implement against an abstract "garbled-bridge-verifier" trait, with
+ BitVM2 as the v1 implementation and Glock/Mosaic as drop-in
+ replacements when one of them stabilises. The circuit-side
+ `IssuanceProof` and `BurnProof` contracts (§4) are identical in
+ any case — only the off-circuit Bitcoin scripting changes.
+
+The hedge is probably the right answer if implementation does not
+have to start this quarter. If implementation must start now and
+mainnet within a year, BitVM2 is forced.
+
+### 12.8 The "BTC denomination" question
+
+A practical note often overlooked: BitVM-family bridges typically
+require **fixed-denomination deposits** because the pre-signed
+transaction graph is parameterised on the deposit amount. Strata
+uses 10 BTC fixed denomination on testnet; Citrea uses similar
+quantisation on mainnet.
+
+For zkCoins, this means peg-ins would come in fixed chunks (e.g.,
+0.1 BTC, 1 BTC, 10 BTC) rather than arbitrary amounts. Users wanting
+smaller amounts would peg in 0.1 BTC and split internally; users
+wanting larger amounts would peg in multiple chunks.
+
+This is a UX consideration, not a protocol constraint. The Lightning
+swap design (`LIGHTNING_ATOMIC_SWAP.md`) is unaffected — it operates
+on arbitrary amounts because it consumes/produces zkCoins state
+which has no minimum increment.
+
+---
+
+## 13. Bottom Line
+
+- **D11 is the biggest unaddressed trust gap in zkCoins.** It is more
+ significant than D2 (recipient hiding), D7 (reorg safety), or D8
+ (per-coin nullifier) for an end-user-trust perspective. A user can
+ tolerate a small privacy gap or a small reorg-safety gap; they
+ cannot tolerate "the issuer can print unlimited supply".
+
+- **BitVM2 / Clementine is the only mainnet-deployed trustless bridge
+ as of 2026-05.** Citrea has been live since 2026-01-27. Tooling,
+ trusted setup ceremony output, and operational documentation all
+ exist. If a bridge must ship within 12 months, this is the only
+ feasible cryptographic option.
+
+- **Glock and Mosaic are the credible 2026 successors** (both authored
+ by the Eagen line of researchers, same family as Shielded CSV
+ itself). Glock is the 430–550× more efficient alternative using
+ DV-SNARKs (Alpen Labs, Strata bridge transition planned); Mosaic
+ keeps Groth16 but cuts on-chain footprint independently of N
+ cut-and-choose copies (April 2026 paper with Rust impl). Neither
+ has mainnet exposure yet. See §12 for the full landscape.
+
+- **BitVM3-RSA was withdrawn** after security flaws were identified
+ by Eagen / Fairgate. The "BitVM3" name continues under the BitVM3-CC
+ (cut-and-choose) variant, which is what BOB demonstrated on mainnet.
+
+- **The realistic short-term path is a Liquid-style federated
+ bridge.** It is implementable in months, provides meaningful
+ trust distribution, and can be upgraded to BitVM2 / Glock / Mosaic
+ later without protocol-layer changes — the `IssuanceProof` and
+ `BurnProof` circuit contracts (§4) are agnostic to the bridge
+ construction.
+
+- **The realistic 1-year cryptographic path is BitVM2.** Federation
+ recruitment and trusted setup ceremony coordination are the
+ bottleneck, not engineering.
+
+- **The realistic 2-year cryptographic path is Glock or Mosaic.** If
+ bridge implementation can wait into 2027, the on-chain efficiency
+ upgrade is worth the wait. The hedge: build the circuit side now,
+ pick the verifier construction when one of Glock/Mosaic has 6+
+ months of testnet history.
+
+- **`LIGHTNING_ATOMIC_SWAP.md` is unaffected.** The swap design's
+ mathematical atomicity holds regardless of how mints work. What
+ changes is the supply-side honesty of the underlying asset.
+
+- **D11 fix belongs in the pre-mainnet hardening block of `ROADMAP.md`.**
+ Currently it is not listed there. This is a documentation gap that
+ should be corrected.
+
+- **Federation target: N=100 independent members.** The MVP runs with
+ N=3 (same data centre, all operated by a single organisation —
+ engineering correctness only, not real trust distribution). The
+ production target is
+ N=100, the practical upper bound of the BitVM2 framework today per
+ Bitlayer's analysis (*"in practice the value of n can be 100"*).
+ Strict 1-of-N honesty: 1 honest key deletion among 100 independent
+ setup members suffices. Going beyond N=100 is open research
+ (Bitlayer: *"It is necessary to research a permissionless
+ multi-party OP challenge protocol that could expand BitVM's
+ existing 1-of-n trust model to 1-of-N, where N is much larger
+ than n"*) and not a current goal. Federation-member recruitment
+ to N=100 is business-development, not engineering. Intermediate
+ milestones expected: N=10 → N=30 → N=100. See `BRIDGE_MVP.md` §2.2.
+
+---
+
+## 14. References
+
+### BitVM2 and Clementine (production-grade)
+- [BitVM2 paper (Linus, Aumayr, Avarikioti, Maffei, Moreno-Sanchez, eprint 2025/1158)](https://eprint.iacr.org/2025/1158.pdf)
+- [BitVM2 site](https://bitvm.org/bitvm2.html)
+- [Citrea Clementine bridge docs](https://docs.citrea.xyz/essentials/clementine-trust-minimized-bitcoin-bridge)
+- [Citrea Risc0-to-BitVM Trusted Setup Ceremony announcement](https://www.blog.citrea.xyz/citrea-completes-the-first-ever-trusted-setup-ceremony-for-zk-proofs-used-in-bitvm/)
+- [BitVM Groth16 Verifier Toolkit (chainwayxyz)](https://github.com/chainwayxyz/bitvm-zk-verifier)
+- [BitVM GitHub org](https://github.com/BitVM/BitVM)
+- [Fairgate review of BitVM2 Linus24 bridge](https://www.fairgate.io/post/3-a-review-of-the-the-bitvm2-based-linus24-bridge)
+- [Bitlayer BitVM bridge analysis](https://blog.bitlayer.org/BitVM_Bridge_Becomes_Practical/)
+
+### BitVM3 and cut-and-choose successors
+- [BitVM3 paper (eprint 2026/933)](https://eprint.iacr.org/2026/933.pdf) — includes both withdrawn RSA construction and cut-and-choose variants
+- [BOB BitVM3 cut-and-choose announcement](https://www.gobob.xyz/blog/bob-lowers-onchain-costs-for-bitvm3)
+- [Fairgate Computing on Bitcoin newsletter](https://www.fairgate.io/newsletter/) — ongoing coverage
+
+### Glock (Alpen Labs)
+- [Glock: Garbled Locks for Bitcoin (Eagen, eprint 2025/1485)](https://eprint.iacr.org/2025/1485)
+- [Glock paper PDF mirror (Alpen)](https://cdn.prod.website-files.com/67cfca80708eb505376820af/68a3e174eaff71d197ac4080_glock.pdf)
+- [Glock: A new standard for verification on Bitcoin (Alpen blog)](https://www.alpenlabs.io/blog/glock-verification-on-bitcoin)
+- [Efficient verifiable cut-and-choose for Glock (Alpen HackMD)](https://hackmd.io/@alpen/B1QfSSO5gg)
+- [Starknet × Alpen partnership announcement (Glock as Starknet BTC bridge)](https://www.starknet.io/blog/starknet-alpen-bitcoin-glock/)
+- [Strata bridge docs (currently BitVM2)](https://docs.alpenlabs.io/how-alpen-works/bitcoin-bridge)
+
+### Mosaic
+- [Mosaic: Practical Malicious Security for Garbled Circuits on Bitcoin (eprint 2026/812)](https://eprint.iacr.org/2026/812)
+
+### Survey / market context
+- [Bitcoin L2s in 2026: A Reality Check (hozk.io)](https://www.hozk.io/articles/bitcoin-l2s-in-2026-a-reality-check)
+- [State of Bitcoin: BitVM3, Glock & Bitcoin Dollar (Bitfinity)](https://www.blog.bitfinity.network/state-of-bitcoin-bitvm3-glock-bitcoin-dollar/)
+
+### Shielded CSV / zkCoins context
+- [Shielded CSV paper §"Issuance" predicate branch](https://eprint.iacr.org/2025/068)
+- `SPEC.md` §15 D11 — this repo
+- `MIGRATION_RESEARCH.md` §5.6 — self-funded MVP publisher
+
+---
+
+## 15. Change Log
+
+| Date | Change |
+| ---- | ------ |
+| 2026-05-17 | Initial draft. |
+| 2026-05-17 | Add §12 "Beyond BitVM2 — 2026 Verification Landscape" covering BitVM3-RSA withdrawal, BitVM3-CC (BOB), Glock (Alpen Labs), Mosaic (Eagen et al.). Update §3 with 2026-landscape note. Update §11.1 / §11.2 / §11.3 comparison tables. Update §13 Bottom Line with hedging strategy. Refactor references into themed groups. |
+| 2026-05-17 | §13 Bottom Line: add explicit production federation target of N=100 (practical upper bound of BitVM2 framework per Bitlayer). Beyond N=100 noted as open research, not current goal. |
+| 2026-05-17 | Consistency audit pass: §12.4 — correct the Glock trusted-setup claim (Glock's DV-SNARK is instantiated with Pari which requires a circuit-specific trusted setup, comparable to Groth16; the previous "the DV-SNARK might not require a setup" wording was wrong). Add a branch note at the top explaining that `SPEC.md` / `MIGRATION_RESEARCH.md` / `ROADMAP.md` currently live on `feat/plonky2-migration` only. |
+| 2026-05-17 | Audit round 2: §6.2 Step 1 — fix proof-name inconsistency ("WithdrawalProof" was a one-off term; renamed to `BurnProof` consistent with §6.3 and §4.1) and correct the §5.2 cross-reference to §6.3. |
+| 2026-05-17 | Audit round 3: harmonise header structure (Status / Authoritative source / Audience / Branch note). Remove organisation-specific "DFX" references in §4.5, §8.1, §8.4, §10.4, §11.1, and §13 — replaced with generic operator/issuer wording for consistency with the rest of the repo. |
diff --git a/BRIDGE_MVP.md b/BRIDGE_MVP.md
new file mode 100644
index 00000000..155bfcd5
--- /dev/null
+++ b/BRIDGE_MVP.md
@@ -0,0 +1,1011 @@
+# Bridge MVP — Engineering Spec
+
+**Status:** Engineering specification. No code yet. Companion to
+[`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md) (strategy / landscape) and
+[`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) (LN swap layer).
+
+**Authoritative source for:** the MVP scope, the locked technical
+decisions, the implementation order, the test plan, and the
+non-goals.
+
+**Audience:** The engineers implementing the MVP. This is the
+file-by-file, phase-by-phase plan; it presupposes the strategic
+decisions made in `BITVM_BRIDGE.md` §12–§13.
+
+> **Branch note.** This document presupposes the Plonky2 migration
+> currently on `feat/plonky2-migration` (PR #17). `SPEC.md`,
+> `MIGRATION_RESEARCH.md`, and `ROADMAP.md` live on that branch and
+> will resolve on `develop` only after PR #17 lands. Until then, view
+> cross-references against `feat/plonky2-migration`.
+
+---
+
+## 1. Scope
+
+This document specifies the **MVP engineering plan** for a trustless
+BTC ↔ zkCoins bridge. It covers:
+
+- The MVP definition (what's in, what's deferred)
+- Three locked technical decisions
+- An eight-phase implementation plan, file-by-file
+- The test plan per phase
+- A risk register
+- Open implementation questions
+
+**MVP goal:** the *technology* is built. The federation is initially
+**3 nodes in the same data centre, all operated by a single
+organisation**. This proves the cryptographic and protocol-level
+correctness of the bridge mechanism. The same code, with a 5–15
+node federation of independent organisations, becomes a real
+trustless bridge — that deployment is a separate operational
+concern, not an engineering one.
+
+It does **not** cover:
+
+- Federation member recruitment (business-development; out of scope)
+- Production hardening beyond the 100%-coverage MVP gate
+- Operational runbooks for federation operators
+- BitVM3 / Glock / Mosaic implementations (deferred per
+ `BITVM_BRIDGE.md` §13 hedging strategy)
+
+---
+
+## 2. MVP Definition
+
+Per `feedback_zkcoins_mvp_definition`, MVP means **minimal feature
+surface** AND **100% test coverage on the activated surface**, both
+non-negotiable.
+
+### 2.1 In scope
+
+- **Peg-in flow:** user deposits BTC, receives a freshly minted
+ zkCoin to a specified `recipient` address
+- **Peg-out flow:** user burns a zkCoin, receives BTC to a specified
+ L1 address, fronted by an operator with later reimbursement
+- **N-of-N MuSig2 federation** with N=3 nodes (configurable; tested
+ with N=3 in MVP)
+- **Cooperative key-path spending** for the funded vault UTXO when
+ all signers cooperate (most peg-ins)
+- **Operator-fronted payouts** with KickOff / Challenge /
+ Assert / Disprove state machine
+- **Bitcoin Light Client gadget** for verifying that a deposit is in
+ the canonical chain at depth ≥ 6
+- **Fraud-proof game** (BitVM2-style) — full implementation, even if
+ in MVP the only adversary is a test fixture
+- **End-to-end integration test** on Bitcoin signet (preferable to
+ regtest because of more realistic block timing; regtest is
+ fallback)
+
+### 2.2 Deferred
+
+- Glock / Mosaic backends (after Plonky2 → Groth16 wrapping is solid
+ for BitVM2, swap is mechanical)
+- BTC denomination flexibility (MVP: fixed denominations e.g.
+ 0.01 BTC, 0.1 BTC, 1 BTC)
+- Watchtower payment incentives (MVP: watchtowers are part of the
+ 3-node federation, paid out-of-band)
+- Multi-coin peg-outs in a single burn (MVP: one burn per peg-out)
+- Production trusted setup ceremony (MVP: single-contributor SRS
+ marked "DO NOT USE IN PRODUCTION")
+- **Federation scaling beyond N=3.** Target federation size for the
+ production bridge is **N=100 independent members** with a 1-of-N
+ setup-honesty assumption (1 honest key deletion suffices). N=100
+ is the practical upper bound of BitVM2's framework today per
+ Bitlayer's analysis (*"in practice the value of n can be 100"*).
+ Beyond N=100 is open research and not a current goal. Intermediate
+ milestones expected: N=10 → N=30 → N=100. Each step is a separate
+ setup ceremony with all new members. Federation-member recruitment
+ is a business-development concern, not engineering, and out of MVP
+ scope.
+
+### 2.3 Out of scope (post-MVP, may need separate spec)
+
+- Liquid-style federated bridge as interim before BitVM2
+- Bridge upgrade to Glock or Mosaic
+- Cross-bridge interoperability (peg-out from this bridge to peg-in
+ to another)
+- Privacy upgrades for peg-in / peg-out (the user's L1 BTC address
+ is visible by construction; mitigations in `BITVM_BRIDGE.md` §9.3
+ are out of MVP scope)
+
+---
+
+## 3. Locked Technical Decisions
+
+These are fixed for v1. Reversing any of them means a non-trivial
+re-design.
+
+### 3.1 Bridge construction: BitVM2 (Citrea-Clementine style)
+
+- Mainnet-deployed (Citrea since 2026-01-27)
+- Reusable tooling (`chainwayxyz/bitvm-zk-verifier`)
+- 1-of-N honesty trust model
+- Trade-off: ~2.6 MB Assert transaction, vs. 5 kB with Glock
+
+Glock and Mosaic are **explicitly deferred** to a future bridge-version-2.
+The MVP abstracts the verifier behind a trait so that switching is a
+later config change.
+
+### 3.2 Bitcoin Light Client: recursive Plonky2 sub-proof
+
+A separate Plonky2 circuit verifies a chain of Bitcoin headers
+(SHA256d + target-bits) and outputs `(tip_hash, cumulative_work)`. The
+`IssuanceProof` branch then **recursively verifies** that
+light-client proof and asserts that a specific UTXO (txid, vout, amount)
+is in a block whose header is part of the verified chain at depth
+≥ 6.
+
+This is preferred over inlining SHA256d directly into the `IssuanceProof`
+circuit because:
+
+- SHA256d in Plonky2 ≈ 262k gates per hash; 6 confirms ≈ 3M gates
+ extra per IssuanceProof — sub-second budget broken
+- Recursive verification cost is approximately constant once
+ warmed up
+- The light-client sub-proof is reusable for other future use cases
+ (e.g., zkCoins-side observation of arbitrary Bitcoin events)
+
+### 3.3 Trusted setup for Groth16 wrapping: single-contributor SRS for MVP
+
+- The Plonky2 → Groth16 wrapper requires a Groth16 trusted setup
+- For MVP with N=3 single-operator nodes, a single-contributor SRS
+ is acceptable: every node already trusts the others (same operator)
+- The SRS file is committed to the repo with a clear marker:
+ ```
+ ⚠️ DO NOT USE IN PRODUCTION
+ This SRS was generated by a single contributor for MVP testing.
+ Replace before any multi-organisation federation deployment.
+ ```
+- Replacement: ~30–60 contributor ceremony before the first real
+ federation deployment. Tooling reused from Citrea's open-source
+ ceremony software.
+
+---
+
+## 4. Phase 1 — Circuit Extension (`IssuanceProof` + `BurnProof`)
+
+### 4.1 Goal
+
+Add two new `ProofType` variants to the state-transition circuit,
+implementing the Shielded-CSV-paper-aligned `issuance(IssuanceProof)`
+and the new `BurnProof` branches.
+
+### 4.2 Files touched
+
+| File | Change |
+| ---- | ------ |
+| `program-plonky2/src/types.rs` | Extend `ProofType` enum with `Issuance` and `Burn` variants; extend `ProofData` with optional fields for issuance/burn metadata |
+| `program-plonky2/src/inputs.rs` | Extend `ProgramInputs` with `peg_in_witness: Option` and `burn_witness: Option` fields |
+| `program-plonky2/src/circuit/issuance.rs` | **new** — `IssuanceProof` circuit branch |
+| `program-plonky2/src/circuit/burn.rs` | **new** — `BurnProof` circuit branch |
+| `program-plonky2/src/circuit/main.rs` | Extend `conditionally_verify_cyclic_proof_or_dummy` dispatch to handle Initial / AccountUpdate / Issuance / Burn |
+| `program-plonky2/src/circuit/mod.rs` | Wire in new modules |
+
+### 4.3 IssuanceProof predicate
+
+The circuit asserts:
+
+```
+Given:
+ account_state: AccountState (new account, owner = recipient address)
+ peg_in_witness: PegInWitness { lcp_proof, utxo_outpoint, amount, recipient_commitment }
+ prev_peg_in_consumed_root: HashDigest
+ new_peg_in_consumed_root: HashDigest
+ non_inclusion_proof: NonInclusionProof of peg-in into peg_in_consumed_smt
+
+Asserts:
+ 1. lcp_proof.verify(verifier_data_bitcoin_lcp) — recursive Plonky2 verify
+ of the Bitcoin Light Client sub-proof
+ 2. utxo_outpoint is included in lcp_proof.confirmed_utxos at depth ≥ 6
+ 3. peg_in_witness.amount equals the UTXO's amount
+ 4. peg_in_witness.recipient_commitment matches the user's intended
+ zkCoins address (binding: witness commitment in the Taproot leaf
+ of the deposit script hashes to recipient_commitment)
+ 5. account_state.balance == amount − bridge_fee_constant
+ 6. account_state.owner == recipient_commitment.address
+ 7. non_inclusion_proof.verify(utxo_outpoint, prev_peg_in_consumed_root)
+ 8. non_inclusion_proof.insert(utxo_outpoint) == new_peg_in_consumed_root
+ 9. Emit ProofData with new state and the new peg_in_consumed_root
+
+Result: a new account with the deposit amount minus fees, provably
+backed by a confirmed on-chain UTXO that cannot be reused.
+```
+
+### 4.4 BurnProof predicate
+
+```
+Given:
+ account_state: AccountState (existing account, has coins)
+ in_coins: Vec (coins being burned; sum_amount = burn_amount)
+ in_coins_inclusion_proofs: inclusion proofs for each in_coin
+ in_coins_history_proofs: same as for normal AccountUpdate
+ burn_witness: BurnWitness { btc_recipient_address, withdrawal_nonce }
+ prev_burned_coins_root: HashDigest
+ new_burned_coins_root: HashDigest
+ burn_insert_proofs: NonInclusionProof per in_coin into burned_coins_smt
+
+Asserts:
+ 1. Each in_coin is verified the same way as in AccountUpdate
+ (source-proof inclusion, history-root containment, coin-history
+ non-inclusion + insert)
+ 2. account_state.balance is decremented by sum(in_coin.amount) using
+ checked_sub
+ 3. burn_witness.withdrawal_nonce is fresh (not in withdrawal_nonces_smt;
+ inserted as part of this proof — or alternatively: nonce is the
+ hash of the burn proof's public values, deterministic uniqueness)
+ 4. Each in_coin.identifier is inserted into burned_coins_smt via
+ burn_insert_proofs, producing new_burned_coins_root
+ 5. No new out_coins are created
+ 6. account_state.public_key is rotated to next_public_key (same as
+ normal send)
+ 7. Emit ProofData including burn_amount, btc_recipient, and
+ withdrawal_nonce as part of public values
+
+Result: the coins are consumed; the bridge can use the public output
+to construct a Bitcoin Payout transaction to the burner.
+```
+
+### 4.5 New types
+
+```rust
+// program-plonky2/src/types.rs additions
+
+pub enum ProofType {
+ InitialProof,
+ AccountUpdateProof,
+ IssuanceProof, // NEW
+ BurnProof, // NEW
+}
+
+pub struct PegInWitness {
+ pub lcp_proof: Plonky2ProofTarget, // recursive LCP proof
+ pub utxo_txid: HashDigest,
+ pub utxo_vout: u32,
+ pub utxo_amount: u64,
+ pub recipient_commitment: RecipientCommitment,
+}
+
+pub struct RecipientCommitment {
+ pub address: Address, // = H(initial_pubkey)
+ pub randomness: HashDigest, // hiding commitment randomness; even
+ // for plaintext-recipient MVP we
+ // carry this for forward-compat
+ // with D2/D10
+}
+
+pub struct BurnWitness {
+ pub btc_recipient_address: [u8; 32], // Bitcoin address (Taproot)
+ pub withdrawal_nonce: HashDigest,
+}
+```
+
+### 4.6 Test plan (Phase 1)
+
+Per `feedback_zkcoins_mvp_definition`, 100% coverage gate applies.
+
+Positive:
+- **IssuanceProof base case:** valid LCP, valid UTXO, fresh
+ non-inclusion → proof accepts; ProofData contains new state with
+ amount − fee.
+- **IssuanceProof for second user:** second peg-in to a different
+ account with a different UTXO → still accepts, peg_in_consumed_smt
+ grows correctly.
+- **BurnProof single coin:** burn one input coin → accepts; output
+ has zero out_coins; account.balance decremented; coin in
+ burned_coins_smt.
+- **BurnProof multiple coins:** burn two input coins summing to
+ burn_amount → accepts; both in burned_coins_smt.
+- **IssuanceProof then BurnProof for same account:** full mint → burn
+ cycle.
+
+Negative (each is a separate test, must assert `data.prove(pw).is_err()`):
+- **IssuanceProof with invalid LCP:** rejected.
+- **IssuanceProof with UTXO at depth < 6:** rejected.
+- **IssuanceProof with amount mismatch:** account claims amount ≠ UTXO
+ amount → rejected.
+- **IssuanceProof with recipient mismatch:** account.owner ≠
+ recipient_commitment.address → rejected.
+- **IssuanceProof reusing a peg-in:** second IssuanceProof with same
+ utxo_outpoint → non-inclusion check fails → rejected.
+- **BurnProof with wrong coin source:** in_coin not in source's
+ out_coins_root → rejected.
+- **BurnProof with double-burn:** burn the same coin twice → second
+ attempt's insert into burned_coins_smt fails → rejected.
+- **BurnProof with wrong balance update:** account.balance not
+ decremented correctly → rejected.
+
+Estimated effort: **3–4 weeks**, risk medium (first time defining
+new ProofType variants; recursive LCP verification needs Phase 2
+to be at least partially done).
+
+---
+
+## 5. Phase 2 — Bitcoin Light Client Gadget
+
+### 5.1 Goal
+
+A Plonky2 circuit that, given a chain of Bitcoin block headers,
+verifies that:
+
+- Each header's hash satisfies its target (proof-of-work valid)
+- Each header chains correctly to the previous one (prev_block_hash
+ match)
+- The cumulative work is computed correctly
+- A claimed UTXO is included in a transaction in one of the headers
+ via Merkle proof against the header's `merkle_root`
+
+### 5.2 Files touched
+
+| File | Change |
+| ---- | ------ |
+| `program-plonky2/src/circuit/lcp/mod.rs` | **new** — light client proof module |
+| `program-plonky2/src/circuit/lcp/header.rs` | **new** — single-header verify (SHA256d + target) |
+| `program-plonky2/src/circuit/lcp/chain.rs` | **new** — multi-header chain verify with cumulative work |
+| `program-plonky2/src/circuit/lcp/spv.rs` | **new** — SPV/Merkle inclusion of a tx in a block |
+| `program-plonky2/src/circuit/lcp/main.rs` | **new** — top-level LCP circuit; outputs (tip_hash, cumulative_work, confirmed_utxos_root) |
+| `program-plonky2/src/circuit/sha256.rs` | **new** — Plonky2 SHA256 gadget (or import from polymerdao/plonky2-sha256) |
+
+### 5.3 SHA256 gadget — buy or build
+
+**Option A: import [polymerdao/plonky2-sha256](https://github.com/polymerdao/plonky2-sha256).**
+
+- Pros: existing implementation, known working
+- Cons: dependency on a third-party crate; older Plonky2 version
+ (0.2.0, our codebase is on 1.1.0); ~262k gates per hash
+- Action: fork into our tree, upgrade to 1.1.0, vendor as a sub-module
+
+**Option B: write our own.**
+
+- Pros: full control, matches our coverage standards
+- Cons: 1–2 weeks of high-precision arithmetic-circuit work; SHA256
+ bit-twiddling is error-prone
+- Action: only if Option A's upgrade to 1.1.0 turns out to be > 1 week
+
+→ **Default: Option A.** Fork to `program-plonky2/src/circuit/sha256/`
+ and upgrade in-place.
+
+### 5.4 LCP public output
+
+```rust
+pub struct LCPPublicValues {
+ pub tip_block_hash: HashDigest,
+ pub cumulative_work: [u32; 8], // 256-bit big-int
+ pub starting_block_hash: HashDigest, // genesis or last-checkpoint
+ pub confirmed_utxos_root: HashDigest, // Merkle root of all UTXOs
+ // proven via SPV in this proof
+}
+```
+
+The `confirmed_utxos_root` is the SMT root of all UTXOs the LCP claims
+are confirmed. When the `IssuanceProof` recursively verifies the LCP,
+it checks one specific UTXO's inclusion in this root.
+
+### 5.5 Block-batch sizing
+
+Naïve LCP verifies the full Bitcoin chain from genesis on every
+issuance — infeasible (~750k blocks as of 2026). Real solutions:
+
+- **Checkpointed LCP:** the circuit starts from a hard-coded
+ checkpoint block hash, verifies only blocks since the checkpoint.
+ Checkpoint updated by federation governance periodically.
+- **Recursive accumulating LCP:** each LCP proof verifies the previous
+ LCP proof and extends it. The "tip" of the chain advances as new
+ blocks come in. New peg-ins use the current LCP proof.
+
+→ **MVP: checkpointed LCP.** The checkpoint is updated weekly by
+the bridge operator; this is acceptable because the bridge trusts
+its own operator to advance the checkpoint, not for security but
+for liveness. Security comes from the SHA256d/target verification
+covering all post-checkpoint blocks.
+
+### 5.6 Test plan (Phase 2)
+
+Positive:
+- **Single block:** verify one valid header → accepts; cumulative
+ work matches expected.
+- **Chain of 6 blocks:** verify a sequence; tip_hash and
+ cumulative_work computed correctly.
+- **SPV inclusion:** verify a tx is in a block's Merkle tree.
+- **Recursive LCP:** prove LCP_1, then prove LCP_2 = LCP_1 +
+ extension; the recursive proof accepts.
+
+Negative:
+- **Invalid PoW:** header hash > target → rejected.
+- **Broken chain:** header[N].prev_block_hash ≠ hash(header[N−1]) →
+ rejected.
+- **Wrong cumulative work:** off-by-one error in difficulty
+ accumulation → rejected.
+- **Wrong SPV:** Merkle proof with wrong sibling → rejected.
+
+Estimated effort: **3–5 weeks**, risk **high** for two reasons:
+
+- SHA256d performance in Plonky2 — if proving time blows up despite
+ recursive sub-proofs, we may need to look at Plonky3 (Poseidon2 is
+ also faster but doesn't help with SHA256d; the only mitigation is
+ a smaller block batch per recursive step)
+- First time integrating an external proof system component (SHA256
+ gadget) — version compatibility risk
+
+---
+
+## 6. Phase 3 — State Extension
+
+### 6.1 Goal
+
+Extend `node::state::State` to track peg-in consumption,
+burn records, and pending payouts.
+
+### 6.2 Files touched
+
+| File | Change |
+| ---- | ------ |
+| `node/src/state.rs` | Add 3 new fields, persist/load, expose query methods |
+| `node/src/state_tests.rs` | Tests for new state operations |
+
+### 6.3 New fields
+
+```rust
+struct State {
+ // existing fields unchanged: smt, mmr, prev_mmr_root, root_indices
+
+ pub peg_in_consumed_smt: SparseMerkleTree, // key = utxo_outpoint hash
+ // value = peg-in metadata hash
+ pub burned_coins_smt: SparseMerkleTree, // key = coin.identifier
+ // value = burn metadata hash
+ pub pending_payouts: BTreeMap,
+ // key = withdrawal_nonce
+}
+
+struct PendingPayout {
+ pub burn_proof_id: ProofId,
+ pub btc_recipient: [u8; 32],
+ pub amount: u64,
+ pub status: PayoutStatus,
+ pub assigned_operator: Option,
+ pub created_block: u64, // signet block height at burn-inscription
+}
+
+enum PayoutStatus {
+ PendingAssignment,
+ Assigned,
+ Fronted { payout_txid: HashDigest, kickoff_txid: Option },
+ Completed,
+ TimedOut, // operator did not front within 64 blocks; ready for re-assignment
+ Disputed { challenge_txid: HashDigest },
+ Slashed,
+}
+```
+
+### 6.4 Persistence
+
+Follow the existing pattern in `node/src/state.rs`: bincode-serialised
+binary files alongside `smt.bin` / `mmr.bin`. Names:
+
+- `peg_in_consumed_smt.bin`
+- `burned_coins_smt.bin`
+- `pending_payouts.bin`
+
+Per `feedback_zkcoins_closed_test_env`, no migration code is needed —
+on first server start with this code, all three files are created
+fresh.
+
+### 6.5 Test plan (Phase 3)
+
+Coverage on `State` extensions:
+
+- Insert into `peg_in_consumed_smt` → root advances; subsequent
+ non-inclusion proof for same utxo fails.
+- Insert into `burned_coins_smt` → same.
+- Add `pending_payouts` entry → retrievable by nonce.
+- State transitions: PendingAssignment → Assigned → Fronted →
+ Completed.
+- Persistence round-trip: write to disk, read back, equal state.
+
+Estimated effort: **1 week**, risk **low** (mechanical extension).
+
+---
+
+## 7. Phase 4 — N-of-N MuSig2 Signer Node
+
+### 7.1 Goal
+
+A daemon that:
+
+- Participates in the federation's MuSig2 key aggregation at setup
+- Pre-signs all spending paths of the bridge transaction graph
+- Cooperatively signs vault outputs for peg-ins
+- Provides signing services for cooperative peg-outs
+
+### 7.2 Where the code lives
+
+This is **not** in `zk-coins/node` directly — it's a separate
+crate that the server binary depends on. Proposed:
+
+```
+zk-coins/node/
+ crates/
+ bridge-signer/ ← new crate
+ src/
+ lib.rs
+ musig2.rs
+ pre_signing.rs
+ tx_graph.rs
+ signer_protocol.rs
+ Cargo.toml
+```
+
+(Alternative: separate repo `zk-coins/bridge-signer`. MVP: keep in
+the server tree to avoid premature repo proliferation. Memory note:
+zkCoins works in `zk-coins/*` org with direct-to-develop pushes
+per `feedback_zkcoins_direct_develop`.)
+
+### 7.3 Library choices
+
+- **MuSig2:** [`secp256k1-musig2`](https://docs.rs/secp256k1/) once
+ it lands (Rust-Bitcoin community); or fork `rust-secp256k1`'s
+ experimental musig branch
+- **Bitcoin tx construction:** `rust-bitcoin` (canonical)
+- **PSBT manipulation:** `rust-bitcoin`'s PSBT support
+- **Network:** simple TCP+protobuf or HTTP JSON, MVP doesn't need a
+ protocol-level standardisation
+
+### 7.4 The tx graph
+
+At federation setup, the signers pre-sign the following templates
+for each peg-in denomination:
+
+1. **MovetoVault:** spends the user's deposit → operational vault
+ UTXO. Parameterised on (deposit_utxo, user_zkcoins_address).
+2. **Payout:** spends vault → user_btc_recipient. Parameterised on
+ (burn_nonce, btc_recipient, amount). Uses
+ `SIGHASH_SINGLE|ANYONECANPAY` so any operator can add a fee input.
+3. **KickOff:** operator's reimbursement claim. Spends operator's
+ bond UTXO + claims vault output.
+4. **Challenge, Assert, Disprove:** BitVM2 fraud-proof state machine.
+5. **Take1, Take2:** operator's eventual reimbursement paths after
+ challenge window or successful defence.
+6. **Burn:** punitive tx that destroys operator's bond on a
+ successful disprove.
+
+For MVP with N=3 and a small set of denominations (say 0.01, 0.1, 1
+BTC), the total pre-signed transaction count is ~6 templates × 3
+denominations = ~18 base templates. Manageable.
+
+### 7.5 The setup ceremony (MVP version)
+
+1. All three signers generate fresh keypairs
+2. MuSig2 key aggregation → `vault_aggregated_pubkey`
+3. Each signer generates and exchanges nonce commitments for every
+ pre-signed transaction
+4. Each signer signs every template; partial signatures aggregated
+5. Each signer **deletes the per-signer private key** (MVP demo:
+ logs a "deleted" message; production: actually zeroes memory and
+ removes any persisted private-key file)
+6. Aggregated signatures stored persistently
+
+### 7.6 Operations
+
+After setup, the signers participate in:
+
+- **MovetoVault signing:** when a user's deposit lands on Bitcoin,
+ signers cooperate to broadcast the pre-signed MovetoVault tx that
+ binds the deposit to the user's zkCoins address
+- **Cooperative payout:** if all signers are online during a peg-out,
+ they cooperatively sign a direct vault→user Payout, bypassing the
+ operator-fronting path
+
+### 7.7 Test plan (Phase 4)
+
+Positive:
+- 3-node MuSig2 setup: aggregated pubkey computed identically by all
+ 3
+- Pre-signing one template: all 3 produce valid partial sigs;
+ aggregation yields a valid BIP-340 sig
+- Pre-signing all 18 templates: completes within reasonable time
+ (target: < 30s)
+- MovetoVault cooperation: 3-node test signs and broadcasts on
+ regtest; transaction confirms
+
+Negative:
+- One node refuses to sign: aggregation fails gracefully (returns
+ Error, not panic)
+- One node provides a corrupt partial sig: detection via verification
+ before aggregation
+- Replay of a pre-signed nonce: detected, rejected
+
+Estimated effort: **3–4 weeks**, risk **medium** (MuSig2 + Bitcoin
+tx construction is well-understood territory but precise pre-signing
+of a complex tx graph has been tricky historically; reference Citrea
+Clementine's `signer` crate as starting point).
+
+---
+
+## 8. Phase 5 — Operator + Watchtower Daemons
+
+### 8.1 Goal
+
+The **operator** daemon advances peg-outs from its own BTC balance
+and claims reimbursement via KickOff. The **watchtower** daemon
+monitors Bitcoin for fraudulent operator claims and posts challenges.
+
+In MVP, the same 3 nodes run both daemons.
+
+### 8.2 Files touched
+
+```
+zk-coins/node/
+ crates/
+ bridge-operator/ ← new crate
+ src/
+ lib.rs
+ payout.rs
+ kickoff.rs
+ bond.rs
+ bridge-watchtower/ ← new crate
+ src/
+ lib.rs
+ monitor.rs
+ challenge.rs
+ disprove.rs
+```
+
+### 8.3 Operator flow
+
+```
+1. Subscribe to `pending_payouts` events from server (see Phase 6)
+2. On PendingAssignment with status changing to Assigned:
+ a. Verify the burn-proof landed (zkCoins state confirms)
+ b. Verify own BTC balance ≥ amount + fees
+ c. Construct the Payout tx (add own input as fee, sign)
+ d. Broadcast Payout tx to Bitcoin
+ e. Wait for confirmation
+ f. Update server: payout fulfilled (txid)
+3. Submit KickOff tx claiming vault reimbursement
+4. Wait for 36-block challenge window
+ a. If no challenge: post NoChallenge tx after timelock, retrieve
+ reimbursement
+ b. If challenged: enter BitVM2 dispute (Assert + Disprove)
+```
+
+### 8.4 Watchtower flow
+
+```
+1. Subscribe to Bitcoin chain (rust-bitcoin chain notifier)
+2. On any KickOff tx detected:
+ a. Verify: does the corresponding pending_payout exist on zkCoins?
+ b. Verify: does the Payout tx claimed by KickOff actually exist
+ on Bitcoin?
+ c. If either check fails: this is a fraudulent KickOff. Post
+ Challenge tx within the challenge window.
+3. On Assert tx (operator's response to Challenge):
+ a. Run our local Groth16 verifier on the asserted computation
+ b. If wrong: post Disprove tx, slashing operator's bond
+```
+
+### 8.5 Bonds
+
+For MVP with 3 trusted nodes, bonds can be dust (~10000 sat) — the
+slashing is symbolic. Production-grade bonds match peg-out
+denominations.
+
+### 8.6 Test plan (Phase 5)
+
+Positive:
+- Happy path peg-out: user burns, operator pays, no challenge, kickoff
+ succeeds.
+- Two parallel peg-outs: both operators advance; both reimbursements
+ complete.
+
+Negative (essential to validate the fraud-proof game works):
+- **Malicious operator simulation:** operator posts KickOff for a
+ payout they did not fund → watchtower detects, posts Challenge →
+ operator cannot produce valid Assert → Disprove fires → bond
+ slashed.
+- **Operator times out on fronting:** assigned operator does not
+ broadcast Payout within 64 blocks → server reassigns.
+- **Network partition:** simulate Bitcoin node disconnect for an
+ operator during KickOff → operator retries on reconnect.
+
+Estimated effort: **3 weeks**, risk **medium** (state-machine
+correctness, especially fraud-proof game; reference Citrea's
+operator + watchtower implementations).
+
+---
+
+## 9. Phase 6 — Bridge-Aware Server
+
+### 9.1 Goal
+
+Extend `zk-coins/node` HTTP API with peg-in and peg-out endpoints.
+
+### 9.2 Files touched
+
+| File | Change |
+| ---- | ------ |
+| `node/src/bridge.rs` | **new** — bridge module |
+| `node/src/server.rs` | Add bridge endpoints to router |
+| `node/src/runtime.rs` | Wire bridge state into runtime |
+
+### 9.3 Endpoints
+
+```
+GET /api/bridge/quote
+ Returns current peg-in and peg-out fees, denominations
+ supported, estimated wait times.
+
+POST /api/bridge/peg-in/initiate
+ Body: { recipient_zkcoins_address, denomination, refund_btc_pubkey }
+ Returns: { deposit_taproot_address, refund_timeout_block }
+ Server records the pending peg-in; user makes the Bitcoin deposit.
+
+POST /api/bridge/peg-in/finalize
+ Body: { deposit_txid, deposit_vout, lcp_proof_bytes }
+ Server verifies the LCP, runs the prover to generate
+ IssuanceProof, returns ProofId to user; user signs the
+ commitment and POSTs it back via the standard /api/commit.
+
+POST /api/bridge/peg-out/burn
+ Body: { source_coins[], btc_recipient_address }
+ Server runs the prover to generate BurnProof, returns ProofId
+ and withdrawal_nonce.
+
+GET /api/bridge/peg-out/status?nonce={nonce}
+ Returns current PayoutStatus.
+
+POST /api/bridge/peg-out/payout-template
+ (Operator-only.) Returns the unsigned Payout template ready
+ for fee-input addition.
+
+POST /api/bridge/peg-out/fronted
+ (Operator-only.) Notify that an operator broadcast a Payout
+ tx; server marks PendingPayout as Fronted.
+```
+
+### 9.4 Test plan (Phase 6)
+
+Per `feedback_zkcoins_mvp_definition`: 100% coverage on the activated
+endpoints.
+
+- Each endpoint with happy-path input → correct response
+- Each endpoint with malformed input → 400-class error, no state
+ change
+- Each endpoint with operator/user role mismatch → 403
+- Race conditions: concurrent peg-out initiations on the same coin
+ set → second rejected with conflict
+
+Estimated effort: **2 weeks**, risk **low** (standard HTTP API
+extension).
+
+---
+
+## 10. Phase 7 — Plonky2 → Groth16 Wrapping
+
+### 10.1 Goal
+
+For BitVM2 to verify our state-transition proof on Bitcoin, the proof
+needs to be in Groth16. Our circuit is Plonky2. The standard pattern
+(Citrea, GOAT) is: prove the Plonky2 verifier circuit in Groth16,
+then BitVM2 verifies the resulting Groth16 proof.
+
+### 10.2 Files touched
+
+| File | Change |
+| ---- | ------ |
+| `crates/bridge-groth16/` | **new crate** — Plonky2 → Groth16 wrapper |
+| `crates/bridge-groth16/src/wrap.rs` | Implement Plonky2 verifier as a Groth16 circuit |
+| `crates/bridge-groth16/src/srs.rs` | Trusted setup SRS loading / validation |
+| `crates/bridge-groth16/srs/mvp_srs.bin` | The MVP single-contributor SRS — **DO NOT USE IN PRODUCTION** |
+
+### 10.3 Approach
+
+Two viable paths:
+
+**Path A: arkworks-based Plonky2 verifier in Groth16.** Implement the
+Plonky2 verifier (Poseidon hashing, FRI proximity checks, etc.) as
+an arkworks Groth16 circuit. Reuse and modify the gnark-style
+verifier patterns Citrea uses for RiscZero → Groth16.
+
+**Path B: Aggregate via a STARK-friendly intermediate.** Plonky2 →
+RiscZero → Groth16. Adds latency but reuses Citrea's exact toolchain.
+
+→ **MVP: Path A.** Direct wrap. Effort estimate is roughly comparable
+ to Path B and avoids an extra dependency.
+
+### 10.4 Trusted setup ceremony
+
+For MVP: single contributor (the lead dev). The SRS file is committed
+to the repo with the warning marker (§3.3).
+
+Production replacement: run a ceremony with 30–60 contributors using
+`chainwayxyz`'s ceremony software (open-sourced as part of Citrea's
+Risc0-to-BitVM ceremony). Each contributor adds randomness; only one
+honest contributor is needed for the resulting SRS to be secure.
+
+### 10.5 Test plan (Phase 7)
+
+- Wrap a small Plonky2 proof in Groth16 → wrapping completes; the
+ Groth16 proof verifies against the SRS.
+- Wrap a state-transition proof from `IssuanceProof` → Groth16 proof
+ has the expected public values (asth, ocr, peg-in-consumed-root,
+ etc.).
+- Negative: wrap a malformed Plonky2 proof → wrapping fails with
+ clear error.
+
+Estimated effort: **3–4 weeks**, risk **medium-high** (most novel
+cryptographic engineering of the MVP; the Plonky2 verifier circuit
+is non-trivial in Groth16; mitigation: study Citrea's open-sourced
+Risc0-to-BitVM verifier).
+
+---
+
+## 11. Phase 8 — Integration Test on Signet
+
+### 11.1 Goal
+
+3-node end-to-end run on Bitcoin signet (or regtest): peg-in, send
+within zkCoins, peg-out. Demonstrate the full happy path and at least
+one fraud-proof challenge.
+
+### 11.2 Setup
+
+- 3 Linux VMs, each running:
+ - Bitcoin signet node (synced)
+ - `zk-coins/node` instance configured for bridge mode
+ - `bridge-signer`, `bridge-operator`, `bridge-watchtower` daemons
+- Shared regtest or signet Bitcoin network
+- A test client that drives peg-ins and peg-outs
+
+### 11.3 Test scenarios
+
+1. **Happy peg-in:** test client deposits 0.1 BTC on signet → 3 nodes
+ cooperatively MovetoVault → LCP advances → IssuanceProof generated
+ → zkCoins minted.
+2. **Happy peg-out:** test client burns 0.1 BTC worth of zkCoins →
+ operator fronts → KickOff → no challenge → operator reimbursed.
+3. **Internal zkCoins send between two test users.**
+4. **Adversarial peg-out:** simulate a malicious operator that posts
+ KickOff for a non-existent payout → watchtower posts Challenge →
+ Disprove succeeds → bond slashed → recoverable state.
+5. **Cooperative peg-out (all 3 signers online):** bypass operator
+ fronting; direct vault → user payout.
+6. **Refund path:** simulate federation outage; test client deposits,
+ federation fails to MovetoVault for 200 blocks → test client uses
+ refund leaf to recover deposit.
+
+### 11.4 Success criteria
+
+- All 6 scenarios complete on signet within reasonable timing
+- No double-spends, no stuck funds, no unauthorised mints
+- Each scenario covered by automated integration test in the CI
+ pipeline
+- Coverage gate maintained on all touched server/bridge code
+
+Estimated effort: **3–4 weeks** integration + debugging, risk
+**medium-high** (first full-stack run; expect timing and
+state-machine bugs).
+
+---
+
+## 12. Aggregate Effort and Risk Register
+
+### 12.1 Total effort
+
+| Phase | Effort | Risk |
+| ----- | ------ | ---- |
+| 1 — Circuit extension | 3–4 weeks | Medium |
+| 2 — Bitcoin Light Client | 3–5 weeks | High |
+| 3 — State extension | 1 week | Low |
+| 4 — MuSig2 signer | 3–4 weeks | Medium |
+| 5 — Operator + watchtower | 3 weeks | Medium |
+| 6 — Bridge-aware server | 2 weeks | Low |
+| 7 — Plonky2 → Groth16 | 3–4 weeks | Medium-high |
+| 8 — Integration on signet | 3–4 weeks | Medium-high |
+| **Total** | **21–28 weeks ≈ 5–7 months** | — |
+
+Assumes Plonky2 migration (PR #17) is complete before Phase 1
+starts. If parallelised carefully, Phases 1–3 can begin while PR #17
+finishes (since they don't depend on the server-side replace step).
+
+### 12.2 Risk register
+
+- **B1 — SHA256d in Plonky2 too slow.** Phase 2.
+ *Mitigation:* recursive sub-proofs with small batch sizes;
+ worst-case fall back to a STARK-friendly LCP (Risc0 / sp1)
+ externally verified.
+- **B2 — Plonky2 → Groth16 wrapping cost.** Phase 7.
+ *Mitigation:* study Citrea's verifier; if it's too custom, fall
+ back to Path B (intermediate Risc0).
+- **B3 — MuSig2 production-readiness.** Phase 4.
+ *Mitigation:* if `rust-secp256k1` MuSig2 is not stable, vendor
+ a known-good fork; reference Citrea's signer.
+- **B4 — Fraud-proof game state-machine bugs.** Phases 5 + 8.
+ *Mitigation:* extensive negative testing (scenario 4 in Phase 8);
+ cross-reference Citrea's operator implementation.
+- **B5 — Bitcoin tx fee market spikes.** Phase 8.
+ *Mitigation:* MVP uses signet (fees ≈ 0); production design
+ includes fee bump mechanisms (RBF, CPFP). Out of MVP scope.
+- **B6 — Light Client checkpoint becomes stale.** Phase 2.
+ *Mitigation:* document checkpoint update procedure; out of MVP
+ automation scope.
+
+---
+
+## 13. Open Implementation Questions
+
+1. **MVP denominations.** Three? Five? `BITVM_BRIDGE.md` §12.8 covers
+ the trade-off. Suggest: `{0.01, 0.1, 1.0} BTC` for MVP.
+
+2. **Refund timeout for peg-in.** Strata uses 200 blocks (~33h).
+ Match.
+
+3. **Challenge window for peg-out.** Strata uses 36 blocks (~6h).
+ Citrea Clementine uses 1.5 days. For MVP: 36 blocks to keep
+ testing fast.
+
+4. **Where does `bridge-signer` live?** In-tree under
+ `server/crates/` or separate repo? MVP: in-tree.
+
+5. **How is the LCP checkpoint advanced?** Manual operator commit
+ for MVP. Automation = post-MVP.
+
+6. **What happens on an LCP that hasn't been refreshed?** Reject the
+ IssuanceProof; user retries after operator refreshes the LCP.
+ Worst case: 1 day operator response time.
+
+7. **Auditability surface for "total BTC in vault vs zkCoins
+ outstanding".** Bridge dashboard endpoint. Useful but
+ out-of-MVP-scope for circuit correctness; add post-Phase 8.
+
+8. **What happens if Plonky2 step 5 (cyclic recursion plumbing, the
+ blocker on `feat/plonky2-migration`) hits issues?** This MVP
+ plan assumes step 5 lands cleanly. If it doesn't, the recursive
+ LCP architecture in Phase 2 cannot work either and we'd need to
+ rethink. Trigger: re-evaluate Phase 2 if step 5a's panic on
+ `circuit_digest` mismatch (`MIGRATION_RESEARCH.md` §7.12)
+ recurs at scale.
+
+---
+
+## 14. Non-Goals (Restated)
+
+So nobody scope-creeps:
+
+- Federation diversity / multi-org recruitment — **not in MVP**
+- BitVM3 / Glock / Mosaic — **not in MVP**
+- Production trusted setup ceremony — **not in MVP**
+- Real economic operator bonds — **not in MVP**
+- Auditability dashboard — **post-MVP**
+- Bridge → Bridge interoperability — **post-MVP**
+- Privacy hardening of peg-in / peg-out — **post-MVP**, depends on
+ D2/D10 closure first
+
+---
+
+## 15. References
+
+- [`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md) — strategic context, landscape,
+ why BitVM2 for v1
+- [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) — LN swap layer
+ that this bridge enables
+- `SPEC.md` — protocol specification (D11 will close with this MVP).
+ Currently on `feat/plonky2-migration`.
+- `MIGRATION_RESEARCH.md` — Plonky2 lessons (§7.12 cyclic-recursion
+ gotcha specifically relevant to Phase 2). Currently on
+ `feat/plonky2-migration`.
+- `ROADMAP.md` — `feat/plonky2-migration` progress; this MVP starts
+ after step 9. Currently on `feat/plonky2-migration`.
+- [Citrea Clementine bridge docs](https://docs.citrea.xyz/essentials/clementine-trust-minimized-bitcoin-bridge)
+- [BitVM Groth16 Verifier Toolkit (chainwayxyz)](https://github.com/chainwayxyz/bitvm-zk-verifier)
+- [polymerdao/plonky2-sha256](https://github.com/polymerdao/plonky2-sha256)
+- [Strata bridge docs (BitVM2 reference impl)](https://docs.alpenlabs.io/how-alpen-works/bitcoin-bridge)
+
+---
+
+## 16. Change Log
+
+| Date | Change |
+| ---- | ------ |
+| 2026-05-17 | Initial draft. |
+| 2026-05-17 | §2.2: add "Federation scaling beyond N=3" as deferred item with production target N=100 (1-of-N strict, practical upper bound of BitVM2 framework). Beyond N=100 noted as open research, not current goal. |
+| 2026-05-17 | Consistency audit pass: add branch note at the top explaining that `SPEC.md` / `MIGRATION_RESEARCH.md` / `ROADMAP.md` currently live on `feat/plonky2-migration` only; downgrade hyperlinks to those files to plain references (with branch annotation) in §15 References. |
+| 2026-05-17 | Audit round 3: harmonise header structure (Status / Authoritative source / Audience / Branch note). Remove "DFX-operated" wording in §2.1 and §3.3 — replaced with generic "single-organisation" wording for consistency with the rest of the repo. |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 50b5dfa0..42af8c9c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,21 +1,316 @@
-# Contributing to zkCoins Server
+# Contributing to zkCoins Node
This guide covers everything you need to develop, test, and deploy the zkCoins backend.
+The first section, "Working on the Plonky2 Migration", documents the project invariants, the decision recipe for "should this go in the MVP?", the pre-push checklist, and the known foot-guns. It applies to all work on `develop` after the 2026-05-18 SP1 → Plonky2 cutover. The rest of this file is the dev guide for day-to-day server work.
+
+---
+
+## Working on the Plonky2 Migration
+
+Canonical entry point for any session (agent or human) picking up the
+codebase without prior context. The Plonky2 migration (PR [#17](https://github.com/zk-coins/node/pull/17))
+merged on 2026-05-18; this section captures the project invariants that
+survive the migration. Read this section, then dive into the linked
+documents in the order given below.
+
+### Reading order
+
+1. **This section** — invariants, decision recipe, gates.
+2. **[`ROADMAP.md`](./ROADMAP.md)** — live status table, per-step plans,
+ effort, risk register, post-MVP Plonky3 path.
+3. **[`SPEC.md`](./SPEC.md)** — what the protocol *does*. Glossary,
+ divergences from the paper (§15), full circuit spec.
+4. **[`MIGRATION_RESEARCH.md`](./MIGRATION_RESEARCH.md)** — why we
+ chose what we chose. §3 (11 divergences), §5 (6 locked-in design
+ decisions), **§7 Lessons Learned** (11 gotchas — required reading
+ before touching the affected code areas).
+5. **[`program-plonky2/CONTRIBUTING.md`](./program-plonky2/CONTRIBUTING.md)**
+ — operational handoff for the migration crate: toolchain,
+ build/test/lint, coverage gate, gadget-authoring pattern.
+
+### No polling — events only
+
+Bitcoin / Esplora signals on the server's hot path are subscribed to,
+never polled. The scanner consumes block events from the
+mempool.space-compatible WebSocket stream (`scanner_ws.rs`,
+`ESPLORA_WS_URL`, default `wss://mutinynet.com/api/v1/ws`); the
+publisher waits for `track-tx` events between commit and reveal
+broadcasts instead of sleeping a fixed propagation interval. The
+previous 30-s tip-poll gated `/api/mint` and `/api/send` visibility
+by up to a full block-time + poll-interval (issue #84); event-driven
+ingestion brings that down to the WS round-trip.
+
+Where it applies:
+
+- `node/src/scanner.rs` — pure inscription parsing, no polling.
+- `node/src/scanner_runtime.rs` — block-walk loop, drains the WS-fed channel.
+- `node/src/scanner_ws.rs` — WS subscriber + reconnect-with-backoff.
+- `node/src/scanner_ws_parse.rs` — pure WS frame parsers.
+- `node/src/publisher.rs` — `track-tx` wait between commit and reveal.
+
+Where it does NOT apply: integration tests
+(`node/tests/api_remote.rs`), health-readiness probes, and any
+self-host operator code outside the four files above.
+
+CI enforces this with a `grep` step inside the `Lint & Build` job in
+`.github/workflows/ci.yaml`:
+
+```bash
+grep -rEn 'tokio::time::(sleep|sleep_until|interval)|std::thread::sleep' \
+ node/src/scanner.rs \
+ node/src/scanner_runtime.rs \
+ node/src/scanner_ws.rs \
+ node/src/scanner_ws_parse.rs \
+ node/src/publisher.rs \
+ | grep -v 'scanner-polling-ok:'
+```
+
+Any match without the `scanner-polling-ok:` token on the same line
+fails the build with a pointer to issue #84. The token is a plain
+comment marker — not an `#[allow(...)]` attribute, which would have
+been mistakable for a real lint suppression — and is the documented
+per-line opt-out for genuinely justified exceptions (today: the
+WS-reconnect backoff in `scanner_ws`, the inner `track-tx`
+reconnect-with-backoff in `scanner_ws`, and the bounded HTTP-retry
+sleep in `scanner_runtime`). The same line must carry a comment
+explaining WHY this particular sleep is not a chain-tip poll. New
+uses require either changing the design or extending this section
+with the rationale.
+
+### Project invariants (non-negotiable)
+
+The five constraints below are decided and apply across every PR on
+`develop`.
+
+1. **Server-side compute architecture.** The server generates every ZK
+ proof, holds every Merkle tree, broadcasts every Taproot inscription.
+ The wallet holds only the user's private key and signs BIP-340 Schnorr
+ over `SHA256(serialize(asth) ‖ serialize(ocr))`. No in-browser
+ Poseidon, no wasm-Plonky2 verifier, no in-app ZK gadget.
+2. **Closed test environment** — DEV *and* PRD. No external users, no
+ real money, no migration of existing state. Step 7 of the ROADMAP
+ deleted the SP1 path outright; no Cargo feature flag, no dual
+ backend. At cutover (PR [#17](https://github.com/zk-coins/node/pull/17), 2026-05-18) the server state files
+ were wiped and the new Plonky2 server started fresh.
+3. **Hardware target: Mac Studio M3 Ultra, 96 GB unified RAM, single
+ host.** All on-box compute resources are available (Performance +
+ Efficiency cores, the integrated Apple GPU reachable via Metal,
+ Neural Engine, AMX). **No external hardware** (no NVIDIA, no CUDA,
+ no GPU farms). **No external cloud proving services** (no Succinct
+ Prover Network, no AWS GPU, no Lambda Labs). Note: Plonky2 today
+ has no Metal backend, so the integrated GPU is effectively idle for
+ proving — that's a library property, not a constraint we imposed.
+ Performance budget: warm proof ≤ 5 s (target ≤ 1 s), cold-start
+ ≤ 30 s, memory peak < 64 GB.
+4. **MVP = minimal feature surface + 100% test coverage.** Simultaneous,
+ not alternative. "Minimal" reduces the surface; "100%" keeps what
+ remains clean. Gate: `cargo llvm-cov --fail-under-lines 100 -- --test-threads=1`
+ from inside the affected crate. Current state on `program-plonky2`:
+ 100% lines / functions / regions, 115 default-run tests (+ 2
+ `#[ignore]`d `recursion_shape_probe` diagnostics). The authoritative
+ coverage gate for `server` runs in CI on the self-hosted M3 Ultra
+ runner (`.github/workflows/ci.yaml`, `Coverage Gate` job, gated
+ behind the `ci:full` label on PRs). See `ROADMAP.md` § "Done" for
+ the live test count and breakdown.
+5. **Plonky2 is bridge tech; Plonky3 is the long-term destination.**
+ But we do not preemptively adopt BabyBear / Poseidon2 inside this
+ migration — see `MIGRATION_RESEARCH.md` §5 (decisions) and ROADMAP
+ "Considered alternative".
+6. **`num_pubkeys` only advances after on-chain broadcast — never
+ before.** The mint and commit flows must follow prepare → broadcast
+ → commit ordering: build the prover witness on a clone, attempt
+ the inscription broadcast first, and only on broadcast success
+ commit the bumped `minting_meta.num_pubkeys` (with an optimistic
+ `... WHERE num_pubkeys = $expected_prev` clause) together with the
+ mutated account snapshots in a single sqlx transaction. The
+ broadcast-then-commit ordering is load-bearing; any future
+ refactor that moves a `minting_meta` UPDATE, an `accounts` UPSERT,
+ or an in-memory `receive_coin` above the broadcast call re-
+ introduces the state-desync class fixed in
+ [zk-coins/node#89](https://github.com/zk-coins/node/issues/89).
+ Startup invariant check in `runtime::check_minting_state_invariant`
+ enforces the corollary at boot: every `pubkey_idx ∈
+ 0..num_pubkeys` MUST have a commitment in the SMT, no flag
+ override — operator recovery is via the `reset_state` workflow.
+
+### Decision recipe — should this go in the MVP?
+
+Run this checklist in order on every proposed change. Stop at the
+first "no".
+
+1. **Is X on the critical path for the one-shot user loop?** (create
+ account → mint → send → receive → balance) If no, defer to post-MVP.
+2. **Does X compromise invariant 1 (server-side compute)?** If yes,
+ redesign so all heavy compute is server-side.
+3. **Does X require external hardware or cloud services (invariant 3)?**
+ If yes, redesign.
+4. **Does X assume migration logic (invariant 2)?** If yes, redesign
+ to "replace not migrate" or defer until mainnet launch.
+5. **Can X be tested to 100% coverage including negative paths
+ (invariant 4)?** If not, refactor or gate behind a Cargo feature.
+6. **Does X drift from the divergence list (`SPEC.md` §15)?** If yes,
+ updating the divergence list is part of the PR.
+
+If all six pass, X enters the MVP. Update `ROADMAP.md` Status-at-a-Glance
+and the relevant `### Step N` section *in the same PR*.
+
+### Pre-push checklist
+
+The repo-level pre-push hook (`.githooks/pre-push`) runs `cargo fmt
+--check`, `cargo clippy` (all three feature scopes), and `cargo
+check --workspace --all-features` automatically. The full test +
+coverage gate for `node` and `shared` runs in CI on the self-hosted
+M3 Ultra runner pool — push and keep working, do not block the
+terminal on the suite.
+
+When touching `program-plonky2/` specifically, also run the local
+sweep + coverage gate **before** opening / updating the PR — the
+cyclic-recursion sweep is not in CI yet (decision tracked in [issue #50](https://github.com/zk-coins/node/issues/50)):
+
+```bash
+cd program-plonky2
+cargo test --release --lib -- --test-threads=1
+cargo llvm-cov --release --fail-under-lines 100 -- --test-threads=1
+```
+
+After push, poll CI until it goes green; if red, investigate and
+fix — never abandon a red CI run.
+
+### Branch hygiene
+
+- No force-pushes, even to side branches.
+- No `--no-verify` on commits.
+- No squashing by the agent — the maintainer squashes at merge time if needed.
+- Maintainers merge PRs; agents open them as drafts.
+- Doc-only commits to `ROADMAP.md` / `SPEC.md` / `MIGRATION_RESEARCH.md`
+ / `CONTRIBUTING.md` / `program-plonky2/CONTRIBUTING.md` that just
+ correct or extend these files are not individually listed in
+ `ROADMAP.md` "Done" — they're in `git log`.
+
+### Where to put new knowledge
+
+When you discover a new gotcha or take a new decision, the right home is:
+
+| Type of knowledge | Where |
+| --- | --- |
+| Protocol-level fact (circuit invariant, public-input change) | `SPEC.md` |
+| Why we chose / didn't choose something | `MIGRATION_RESEARCH.md` §5 or §7 |
+| New status / step / risk | `ROADMAP.md` |
+| Toolchain or workflow detail for the migration crate | `program-plonky2/CONTRIBUTING.md` |
+| Cross-cutting invariant for the whole project | This section |
+
+Don't duplicate prose across files — the second copy will drift.
+Link from one to the other.
+
+### Common foot-guns (already encountered)
+
+Condensed pointers into [`MIGRATION_RESEARCH.md`](./MIGRATION_RESEARCH.md) §7:
+
+1. Don't seed `DEFAULT_HASHES[TREE_DEPTH]` with `ZERO_HASH` in
+ Poseidon SMTs — structural collision (§7.1).
+2. `pw.set_target(t, v)` returns `Result` in plonky2 1.x — must
+ handle (§7.3).
+3. Pack 7 bytes per Goldilocks element, never 8 — modulus safety (§7.4).
+4. Defensive bounds checks: use `Option::get().copied().unwrap_or(...)`,
+ not explicit `if/else` — keeps coverage at 100% (§7.9).
+5. Every `#[cfg(test)] mod tests` needs `#[cfg_attr(coverage_nightly, coverage(off))]` (§7.10).
+6. No external GPU / cloud assumption in performance plans — single
+ Mac Studio M3 Ultra (§7.11).
+7. Kill orphan `cargo test` binaries after long circuit-test runs —
+ they leak 30+ GB of swap (§7.6).
+8. `gh` in background tasks needs `--repo /` (§7.7).
+
+---
+
## Quick Start
```bash
-git clone https://github.com/zk-coins/server.git
-cd server
-SP1_PROVER=mock cargo run -p server
+git clone https://github.com/zk-coins/node.git
+cd node
+USERNAME_DOMAIN=test.zkcoins.local cargo run -p node
# Server starts on http://0.0.0.0:4242
```
+## Local Development with Postgres
+
+The Postgres state-layer added in PR-A1 expects a running PostgreSQL
+instance to be reachable at `DATABASE_URL`. The module is not wired
+into the bootstrap yet (PR-A2 + PR-A3 land that), so you can develop
+without it — but to run the `db_tests` locally you do need either
+Docker available (the tests spin up a Postgres 17 container via
+`testcontainers-modules`) or a manually-started Postgres.
+
+Manual Postgres for ad-hoc query work:
+
+```bash
+docker run --name zkcoins-pg \
+ -e POSTGRES_PASSWORD=dev \
+ -p 5432:5432 \
+ -d postgres:17
+export DATABASE_URL=postgres://postgres:dev@localhost:5432/postgres
+
+# Apply the migrations against the running instance:
+cargo install sqlx-cli --no-default-features --features rustls,postgres
+cd node
+sqlx migrate run
+```
+
+Run the `db_tests` (Docker required, runs `postgres:17` per test):
+
+```bash
+cargo test -p node db -- --test-threads=1
+```
+
+The schema lives in `node/migrations/0001_initial.sql`. After
+changing it, drop the local database (`docker rm -f zkcoins-pg`) and
+re-run `sqlx migrate run` against a fresh instance — there is no
+`down` migration in the MVP, the migration set is forward-only.
+
+## Setup
+
+After cloning, enable the repo's pre-push hook. The hook runs `cargo
+fmt --check`, `cargo clippy` (all three feature scopes), and `cargo
+check --workspace --all-features` — fast enough that it stays out of
+the way (< 30 s warm, < 2 min cold) while still flagging lint and
+type regressions before they reach a CI runner.
+
+```bash
+git config core.hooksPath .githooks
+```
+
+The authoritative test + coverage gate runs in CI on a self-hosted
+M3 Ultra runner pool (issue #40, `.github/workflows/ci.yaml`), not
+in this hook. CI takes 60-90 min for a Rust change but does not
+block your terminal — you push, you keep working, the pool reports
+back via PR check status.
+
+Wall budgets on warm cache:
+
+| Stage | Wall | Where |
+|--------------------------------|-----------|-----------|
+| Pre-push hook (lint + check) | < 30 s | local |
+| Node + shared tests | 60-90 min | CI runner |
+| Coverage gate (100% scope) | + 60 min | CI runner |
+
+When preparing a release PR to `main`, run the circuit sweep manually
+— only the `node` + `shared` test sweep is gated in CI (decision
+on the cyclic sweep is tracked in [issue #50](https://github.com/zk-coins/node/issues/50)):
+
+```bash
+cargo test -p zkcoins-program-plonky2 --release --lib -- --test-threads=1
+```
+
+You can bypass the hook with `git push --no-verify` in genuine
+emergencies. CI is the real gate, so a bypassed lint failure surfaces
+at the PR check level instead — and `develop` must be 100% green
+before any main-merge.
+
## Prerequisites
| Tool | Version | Purpose |
|---|---|---|
-| Rust | 1.81+ | Build toolchain (pinned via `rust-toolchain`) |
+| Rust | nightly (pinned via `rust-toolchain`) | Required for Plonky2 (`feature(specialization)`) |
| Bitcoin node | — | Required for blockchain scanning (or use Esplora API) |
## Project Structure
@@ -26,24 +321,28 @@ server/
│ └── src/
│ ├── main.rs # Entry point, chain scanner, bind address
│ ├── server.rs # REST endpoints (mint, send, balance, proof)
-│ ├── account_server.rs # Account management, coin proofs, prover calls
+│ ├── account_node.rs # Account management, coin proofs, prover calls
│ ├── state.rs # Sparse Merkle Tree + Merkle Mountain Range
│ ├── scanner.rs # Bitcoin block scanner (Taproot Inscriptions)
+│ ├── scanner_ws.rs # Esplora WebSocket subscriber (event-driven, issue #84)
│ └── publisher.rs # Inscription broadcaster (commit/reveal, prefix 4242)
├── shared/ # Shared types (Commitment, Invoice, ClientAccount)
│ └── src/
│ ├── lib.rs # Types, key derivation, crypto helpers
│ └── commitment.rs # Schnorr commitment (sign + verify)
-├── program/ # SP1 zkVM circuit (Zero-Knowledge proof logic)
+├── program-plonky2/ # Plonky2 + Poseidon cyclic-recursion state-transition circuit
│ └── src/
-│ ├── lib.rs # Types: AccountState, Coin, ProofData, ProgramInputs
-│ ├── main.rs # zkVM entrypoint (gated behind "zkvm" feature)
-│ └── merkle/ # SMT + MMR implementations
-├── script/ # Prover wrapper (stub for Docker, real SP1 for local)
-│ └── src/lib.rs # Prover struct: create_account(), update_account()
-├── Cargo.toml # Workspace root
-├── Dockerfile # Multi-stage Rust build
-└── rust-toolchain # Pinned Rust version (1.81.0)
+│ ├── lib.rs # Prelude: F, C, D type aliases
+│ ├── hash.rs # Poseidon HashDigest + byte conversions
+│ ├── types.rs # AccountState, Coin, ProofData, MINTING_ADDRESS placeholder
+│ ├── inputs.rs # ProgramInputs, CommitmentMerkleProofs
+│ ├── merkle/ # Poseidon-based SMT + MMR
+│ └── circuit/ # build_circuit + per-stage gadgets + aggregator
+├── script-plonky2/ # Host-side Plonky2 prover wrapper (zkcoins-prover-plonky2)
+│ └── src/lib.rs # Prover struct: prove_initial / prove_account_update
+├── Cargo.toml # Workspace root (nightly toolchain, no SP1 patches)
+├── Dockerfile # Multi-stage Rust build (linux/arm64, FEATURES build-arg)
+└── rust-toolchain # Pinned nightly date (matches program-plonky2)
```
## Git Workflow
@@ -89,11 +388,11 @@ update
| Item | Convention | Example |
|---|---|---|
-| Crate | kebab-case | `zkcoins-program` |
-| Module | snake_case | `account_server` |
+| Crate | kebab-case | `zkcoins-program-plonky2` |
+| Module | snake_case | `account_node` |
| Struct | PascalCase | `AccountState`, `CoinProof` |
| Function | snake_case | `process_block`, `send_coins` |
-| Constant | SCREAMING_SNAKE | `ACCOUNT_SERVER_ADDR` |
+| Constant | SCREAMING_SNAKE | `ACCOUNT_NODE_ADDR` |
### Error Handling
@@ -109,15 +408,15 @@ let block = fetch_block(hash).unwrap();
- Workspace dependencies in root `Cargo.toml` — individual crates reference `{ workspace = true }`
- Pin exact versions for security-critical crates (`bitcoin`, `sha2`)
-- SP1 patches in `[patch.crates-io]` — only in the full workspace, removed in the Docker stub
+- `plonky2 = "1.1.0"` from crates.io; no `[patch.crates-io]` entries
## Architecture
### Request Flow
```
-Client Request → Axum Router → server.rs (endpoint) → account_server.rs (logic)
- ├── Prover (stub/SP1)
+Client Request → Axum Router → server.rs (endpoint) → account_node.rs (logic)
+ ├── Prover (Plonky2)
├── State (SMT + MMR)
└── Publisher (Bitcoin)
```
@@ -136,88 +435,126 @@ struct Account {
}
```
-**Prover abstraction:** The `Prover` trait has two implementations:
-- **Stub** (`script/src/lib.rs`) — returns mock proofs, compiles without SP1 toolchain
-- **Real SP1** — requires the `succinct` Rust toolchain and SP1 SDK (not used in Docker)
+**Prover:** `zkcoins_prover_plonky2::Prover` (in `script-plonky2/src/lib.rs`)
+wraps the cyclic state-transition circuit. `Prover::new()` builds the
+circuit once; `prove_initial` / `prove_account_update` (with their
+`_with_in_coins` / `_with_in_and_out_coins_and_sources` variants) drive
+individual transitions. No mock/stub backend — the only build is the
+Plonky2 prover.
### Bitcoin Integration
The server continuously scans the Bitcoin blockchain:
-1. `scanner.rs` polls Esplora every 30 seconds
-2. Filters transactions by prefix `4242` in Taproot witness
+1. `scanner_ws.rs` subscribes to the mempool.space-compatible WebSocket
+ (`ESPLORA_WS_URL`) and pushes block events into a channel; no
+ chain-tip polling (issue #84, see "No polling — events only" above)
+2. `scanner_runtime.rs` drains the channel and hands each block to
+ `scanner.rs`, which filters transactions by prefix `4242` in the
+ Taproot witness
3. Deserializes `Commitment` structs (Schnorr-signed)
4. `state.rs` inserts valid commitments into SMT, appends to MMR
The publisher (`publisher.rs`) creates Taproot Inscriptions:
- Commit/reveal pattern (two transactions)
- Data split into 520-byte chunks (max push size)
-- Broadcasts via Esplora API
+- Broadcasts via Esplora API, then waits for the WS `track-tx` event
+ between commit and reveal instead of sleeping a fixed interval
-### SP1 zkVM Circuit
+### Plonky2 State-Transition Circuit
-The `program/` crate defines the Zero-Knowledge proof logic. It compiles to two targets:
-
-| Target | Feature | Use |
-|---|---|---|
-| Native (x86/ARM) | default (no `zkvm`) | Library — types and Merkle trees used by server |
-| RISC-V (SP1) | `zkvm` | zkVM binary — actual proof execution |
-
-The `zkvm` feature gates the SP1 entrypoint and all `sp1_zkvm::` calls.
+The `program-plonky2/` crate defines the Zero-Knowledge proof logic.
+The full SPEC §8 predicate (cyclic recursion, MMR + SMT inclusion,
+in-coin source-side aggregator pattern from Stage 5d-next-5, out-coin
+identifier derivation, pubkey rotation) lives in `circuit/main.rs`.
+`MAX_IN_COINS = MAX_OUT_COINS = 8`. See
+[`MIGRATION_RESEARCH.md` §7.22](./MIGRATION_RESEARCH.md#722-stage-5d-next-5-source-side-verification-via-aggregator-pattern--codified-resolves-721)
+for the architecture writeup and `program-plonky2/SESSION_STATE.md`
+for the historical pickup record.
## Environment Variables
+The node reads its configuration exclusively from environment variables;
+no `.env` file is loaded by the process. The table below covers every
+variable the server actually reads (`node/src/lib.rs`, `runtime.rs`,
+`scanner_ws.rs`, `publisher.rs`). Required variables panic the bootstrap
+on startup if unset — there is no silent fallback.
+
| Variable | Default | Description |
|---|---|---|
-| `SP1_PROVER` | `mock` | `mock` (no proof), `cpu`, `cuda`, or `network` |
-| `ESPLORA_URL` | `https://mutinynet.com/api` | Esplora API endpoint (electrs or public) |
-| `IS_MAINNET` | `false` | `true` for Bitcoin Mainnet, `false` for Mutinynet/Signet |
-| `NETWORK_NAME` | `Mutinynet` | Human-readable network name (returned by `/api/info`) |
-| `PUBLISHER_KEY` | test key | 32-byte hex private key for inscription publishing. **Required on mainnet** |
-| `RUST_LOG` | `info` | Log level (`debug`, `info`, `warn`, `error`) |
+| `DATABASE_URL` | _(required, no default)_ | Postgres connection string for the state-layer (e.g. `postgresql://zkcoins:@postgres:5432/zkcoins`). Server panics on startup if unset. |
+| `PUBLISHER_KEY` | _(required, no default)_ | 32-byte hex private key for Taproot inscription publishing. **Required on every network — DEV, signet, and mainnet.** No fallback default exists: the previous `1234…` placeholder was a publicly-known test key that drainer bots swept within minutes of any on-chain top-up (4 historical drains confirmed). Server panics on startup if unset. Generate locally via `openssl rand -hex 32`. In any deployed environment, source it from your secret manager — **never commit a real key**. |
+| `USERNAME_DOMAIN` | _(required, no default)_ | External hostname returned by `/api/info`; server panics on startup if unset (see PR [#36](https://github.com/zk-coins/node/pull/36) for the regression that introduced the global panic hook). |
+| `POSTGRES_PASSWORD` | _(required, no default for the DB container)_ | Read by the Postgres container, not by the node process itself; the node's `DATABASE_URL` already embeds the password. Listed here because it is part of the local-dev bootstrap (see `Local Development with Postgres` below). |
+| `ESPLORA_URL` | `https://mutinynet.com/api` | Esplora REST API endpoint (electrs or public). |
+| `ESPLORA_WS_URL` | `wss://mutinynet.com/api/v1/ws` | Esplora WebSocket endpoint consumed by `scanner_ws` (issue #84). DEV/PRD override only when the upstream WS path changes. |
+| `IS_MAINNET` | `false` | `true` for Bitcoin Mainnet, `false` for Mutinynet/Signet. |
+| `NETWORK_NAME` | `Mutinynet` / `Mainnet` | Human-readable name returned by `/api/info`. |
+| `PROOFS_DIR` | `./proofs` | Directory for per-proof bincode files (see `Persistent State` below). |
+| `SCANNER_INITIAL_SETTLE_TIMEOUT_MS` | (runtime-defined) | Override for the scanner's initial-settle deadline; see `runtime.rs`. |
+| `RUST_LOG` | `info` | Log level (`debug`, `info`, `warn`, `error`). |
+
+### Minimal local-dev env
+
+```bash
+export DATABASE_URL="postgresql://postgres:dev@localhost:5432/postgres"
+export PUBLISHER_KEY="$(openssl rand -hex 32)"
+export USERNAME_DOMAIN="test.zkcoins.local"
+# Optional — defaults are fine for Mutinynet:
+# export ESPLORA_URL="https://mutinynet.com/api"
+# export IS_MAINNET="false"
+cargo run -p node
+```
+
+For any deployed environment, the real values live in your secret manager
+of choice and are passed into the node container as env vars at startup.
## Docker
```bash
-docker build -t zkcoin/server .
+docker build -t zkcoins/node .
docker run -p 4242:4242 \
--network bitcoin \
- -e SP1_PROVER=mock \
-e ESPLORA_URL=http://electrs-mainnet:3000 \
- zkcoin/server
+ -e USERNAME_DOMAIN=zkcoins.app \
+ zkcoins/node
```
-The pre-built ELF (`elf/zkcoins-program`) is committed to the repo, so Docker builds do not require the Succinct toolchain — only standard Rust.
+Docker builds use nightly Rust auto-installed via the workspace `rust-toolchain` — no Succinct toolchain, no zkVM target.
## Persistent State
-The server writes the following files under its data volume (`/data` in the container, `zkcoins_server-data` Docker volume on dfxdev/dfxprd). Together they define the recoverable state:
+After the PR-A1/PR-A2/PR-A3 Postgres migration series, all persistent server state lives in a Postgres 17 database (`DATABASE_URL` env var). The only on-disk state remaining is the per-proof file store. The state-layer schema (`node/migrations/*.sql`) is applied idempotently on every boot by `db::connect_and_migrate`.
-| File | Format | Purpose |
-| -------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
-| `smt.bin` | bincode `SparseMerkleTree` | Sparse Merkle Tree of every commitment ever processed (key = sha256(public_key), leaf = account_state_hash). |
-| `mmr.bin` | bincode `MerkleMountainRange` | Append-only Merkle Mountain Range of `hash(smt_root ‖ prev_mmr_root)` leaves; one entry per processed commitment. |
-| `mmr.bin.prev_root` | 32 bytes | The previous MMR root, kept separately so the SMT/MMR pair stays atomically consistent across restarts. |
-| `latest_block.bin` | 32 bytes (block hash) | Last Bitcoin block whose inscriptions were fully processed and persisted. Scanner resumes from `latest_block + 1` after a restart. |
-| `accounts.bin` | bincode `HashMap` | Server-side account ledger — per-address balance, coin_queue, coin_history (SMT), and latest proof. Includes the minting account. |
-| `usernames.bin` | bincode `UsernameStore` | Gated by `usernames` Cargo feature. Bidirectional map of claimed usernames ↔ addresses. |
-| `minting_num_pubkeys.bin` | 4 bytes LE u32 | Gated by `faucet`. Counter of how many mint commitments have been issued; **must** survive restart, otherwise the next mint sends a stale `prev_commitment_pubkey` and `send_coins` returns `prev_commitment_pubkey required for account update`. |
-| `proofs/.bin` | bincode `CoinProof` | Individual per-send proof + commitment, indexed by `proof_id`. Append-only. |
+| Location | Format | Purpose |
+| --------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `smt_state` row (singleton, `id = 1`) | bincode `SparseMerkleTree` in a `BYTEA` column | Sparse Merkle Tree of every commitment ever processed (key = sha256(public_key), leaf = account_state_hash). |
+| `mmr_state` row (singleton, `id = 1`) | bincode `MerkleMountainRange` in a `BYTEA` column | Append-only Merkle Mountain Range of `hash(smt_root ‖ prev_mmr_root)` leaves; one entry per processed commitment. |
+| `latest_block` row (singleton, `id = 1`) | 32-byte block hash in a `BYTEA` column | Last Bitcoin block whose inscriptions were fully processed and persisted. Scanner resumes from `latest_block + 1` after a restart. Written in the same `BEGIN; UPSERT; UPSERT; UPSERT; COMMIT` transaction as the SMT and MMR (issue #11 fix). |
+| `accounts` table (one row per address) | 32-byte `address` PRIMARY KEY + bincode `Account` `BYTEA` | Server-side account ledger — per-address balance, coin_queue, coin_history (SMT), and latest proof. Includes the minting account. Upserted per mutation by the send / receive / mint handlers. |
+| `usernames` table (one row per name) | `TEXT` name PRIMARY KEY + 32-byte `address` `BYTEA` | Bidirectional map of claimed usernames ↔ addresses. Race-free claims via `INSERT … ON CONFLICT (name) DO NOTHING`. Always present — usernames are permanent MVP. |
+| `minting_meta` row (singleton, `id = 1`) | `BIGINT` num_pubkeys | Counter of how many mint commitments have been issued; **must** survive restart, otherwise the next mint sends a stale `prev_commitment_pubkey` and `send_coins` returns `prev_commitment_pubkey required for account update`. Always present — mint is permanent MVP. |
+| `proofs/.bin` (on-disk file) | bincode `CoinProof` | Individual per-send proof + commitment, indexed by `proof_id`. Append-only. **Not** in Postgres because the per-proof blobs are large Plonky2 proof bytes and the directory layout makes recovery trivial. Path configurable via `PROOFS_DIR` (default `./proofs`). |
-`atomic_write` is used for every write (tempfile + rename). A crash between writes can still leave `latest_block.bin` lagging the SMT/MMR pair; the scanner is now tolerant of this — `state.update` errors are logged (see `main.rs::scan_for_inscriptions` callback) rather than propagated as panics.
+Writes are atomic at the row / transaction level (`ON CONFLICT DO UPDATE` for singleton rows, the BEGIN/COMMIT block in `db::persist_state_tx` for the SMT/MMR/latest-block trio). Per-proof file writes still use a write-to-temp + rename pattern inside `ProofStore::persist_proof_bytes`. The pre-migration `smt.bin` / `mmr.bin` / `latest_block.bin` / `accounts.bin` / `usernames.bin` / `minting_num_pubkeys.bin` sibling files no longer exist, and the previous `main.rs::atomic_write` helper has been removed.
### DEV state recovery
-If the DEV server gets into a bad state (panic loop, mint failures with `prev_commitment_pubkey required`, balance never rising after a successful mint, etc.), the recovery procedure is to wipe the data volume:
+If the DEV server gets into a bad state (panic loop, mint failures with `prev_commitment_pubkey required`, balance never rising after a successful mint, etc.), the recovery procedure is to truncate the Postgres state-layer tables (and drop the on-disk proofs directory):
```bash
-# On the host running the server (e.g. dfxdev):
-docker stop zkcoins-server
-docker run --rm -v zkcoins_server-data:/data alpine sh -c 'rm -f /data/*.bin /data/*.bin.prev_root'
-docker start zkcoins-server
+# On the host running the server (DEV or PRD):
+docker stop zkcoins-node
+# Truncate every state-layer table. _sqlx_migrations is intentionally
+# left in place so connect_and_migrate skips re-applying the schema.
+docker exec -i zkcoins-postgres psql -U zkcoins -d zkcoins -c \
+ 'TRUNCATE accounts, usernames, smt_state, mmr_state, latest_block, minting_meta;'
+# Drop the per-proof files (proof_id state resets at next boot).
+docker run --rm -v zkcoins_server-data:/data alpine sh -c 'rm -rf /data/proofs'
+docker start zkcoins-node
```
-The server starts from genesis on next boot: `Creating new State / No accounts file found / No saved block hash found / fetching latest from Esplora`. Past test wallets are abandoned on-chain (they're random) but the SMT is re-built from the chain tip onwards. This is **destructive** — never run it on PRD without a known-needed reason.
+The server starts from genesis on next boot: `Loaded State from Postgres` (empty), `Loaded AccountNode from Postgres` (empty), `No saved block hash found, fetching latest from Esplora`. Past test wallets are abandoned on-chain (they're random) but the SMT is re-built from the chain tip onwards. This is **destructive** — never run it on PRD without a known-needed reason.
The E2E regen workflow on the app repo wipes this state before every run as part of the per-PR cadence in `app/e2e/README.md § 11.3`.
@@ -234,10 +571,33 @@ See [docs.zkcoins.app/infrastructure/backend](https://docs.zkcoins.app/infrastru
| Workflow | Trigger | Action |
|---|---|---|
-| `deploy-dev.yaml` | Push to develop | Docker build (ARM64) → push `zkcoin/server:beta` → deploy to DEV |
-| `deploy-prd.yaml` | Push to main | Docker build (ARM64) → push `zkcoin/server:latest` → deploy to PRD |
+| `ci.yaml` (Lint & Build) | Ready PR → develop, push to develop | `cargo fmt --check`, clippy (MVP + all-features + program lib), build (MVP + all-features) on `ubuntu-latest`. |
+| `ci.yaml` (Node + Shared Tests) | Ready PR → develop with `ci:full` label, push to develop | `cargo nextest run -p node -p shared --release --all-features --test-threads 1 -E 'not binary(api_remote)'` on the self-hosted M3 Ultra runner pool (issue #40). |
+| `ci.yaml` (Coverage Gate) | Ready PR → develop with `ci:full` label, push to develop | `cargo llvm-cov nextest` with the 100% line + function gate, MVP scope, on the same runner pool. |
+| `deploy-dev.yaml` | Push to develop | Docker build (ARM64) → push `zkcoins/node:beta` → deploy to DEV |
+| `deploy-prd.yaml` | Push to main | Docker build (ARM64) → push `zkcoins/node:latest` → deploy to PRD |
| `auto-release-pr.yaml` | Push to develop | Creates Release PR (develop → main) |
+**Draft PRs** skip every `ci.yaml` job — the workflow fires once the
+PR is marked ready-for-review.
+
+**Heavy jobs** (`Node + Shared Tests`, `Coverage Gate`) additionally
+require the `ci:full` label on a ready PR. Apply the label when the
+PR is in shape to run against the authoritative ~60-90 min M3 Ultra
+gate; remove it before the next push to keep an agent free for other
+work. `Lint & Build` (fast, GitHub-hosted, free) keeps running on
+every ready-PR push.
+
+`push to develop` always runs the full gate — the post-merge run on
+`develop` is the source of truth, and `deploy-dev.yaml` consumes its
+result via the auto-release PR's check rollup.
+
+To stop a Heavy run that is already executing, removing the `ci:full`
+label is *not* enough — the workflow isolates label events into their
+own concurrency group so an unrelated label toggle doesn't cancel an
+in-flight 60-min run. If you need to free an agent immediately, use
+`gh run cancel ` (the run id is on the PR's checks tab).
+
Build time is ~5 minutes (Rust compilation on ARM64).
## Related Repos
diff --git a/Cargo.lock b/Cargo.lock
index a767b637..f170096f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,50 +1,25 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "addchain"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2e69442aa5628ea6951fa33e24efe8313f4321a91bd729fc2f75bdfc858570"
-dependencies = [
- "num-bigint 0.3.3",
- "num-integer",
- "num-traits",
-]
-
-[[package]]
-name = "addr2line"
-version = "0.24.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
-dependencies = [
- "gimli",
-]
-
-[[package]]
-name = "adler2"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+version = 4
[[package]]
name = "ahash"
-version = "0.8.11"
+version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
+ "const-random",
"once_cell",
"version_check",
- "zerocopy 0.7.35",
+ "zerocopy",
]
[[package]]
name = "aho-corasick"
-version = "1.1.3"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
@@ -56,3540 +31,2124 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
-name = "alloy-consensus"
-version = "0.11.1"
+name = "android_system_properties"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69e32ef5c74bbeb1733c37f4ac7f866f8c8af208b7b4265e21af609dcac5bd5e"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
- "alloy-eips",
- "alloy-primitives",
- "alloy-rlp",
- "alloy-serde",
- "alloy-trie",
- "auto_impl",
- "c-kzg",
- "derive_more 1.0.0",
- "serde",
+ "libc",
]
[[package]]
-name = "alloy-consensus-any"
-version = "0.11.1"
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "assert-json-diff"
+version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fa13b7b1e1e3fedc42f0728103bfa3b4d566d3d42b606db449504d88dbdbdcf"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
- "alloy-consensus",
- "alloy-eips",
- "alloy-primitives",
- "alloy-rlp",
- "alloy-serde",
"serde",
+ "serde_json",
]
[[package]]
-name = "alloy-eip2124"
-version = "0.1.0"
+name = "astral-tokio-tar"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "675264c957689f0fd75f5993a73123c2cc3b5c235a38f5b9037fe6c826bfb2c0"
+checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277"
dependencies = [
- "alloy-primitives",
- "alloy-rlp",
- "crc",
- "thiserror 2.0.12",
+ "filetime",
+ "futures-core",
+ "libc",
+ "portable-atomic",
+ "rustc-hash",
+ "tokio",
+ "tokio-stream",
+ "xattr",
]
[[package]]
-name = "alloy-eip2930"
-version = "0.1.0"
+name = "async-stream"
+version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0069cf0642457f87a01a014f6dc29d5d893cd4fd8fddf0c3cdfad1bb3ebafc41"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
- "alloy-primitives",
- "alloy-rlp",
- "serde",
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
]
[[package]]
-name = "alloy-eip7702"
-version = "0.5.1"
+name = "async-stream-impl"
+version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b15b13d38b366d01e818fe8e710d4d702ef7499eacd44926a06171dd9585d0c"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
- "alloy-primitives",
- "alloy-rlp",
- "serde",
- "thiserror 2.0.12",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "alloy-eips"
-version = "0.11.1"
+name = "async-trait"
+version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5591581ca2ab0b3e7226a4047f9a1bfcf431da1d0cce3752fda609fea3c27e37"
-dependencies = [
- "alloy-eip2124",
- "alloy-eip2930",
- "alloy-eip7702",
- "alloy-primitives",
- "alloy-rlp",
- "alloy-serde",
- "auto_impl",
- "c-kzg",
- "derive_more 1.0.0",
- "once_cell",
- "serde",
- "sha2 0.10.8",
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "alloy-json-rpc"
-version = "0.11.1"
+name = "atoi"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "762414662d793d7aaa36ee3af6928b6be23227df1681ce9c039f6f11daadef64"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
- "alloy-primitives",
- "alloy-sol-types",
- "serde",
- "serde_json",
- "thiserror 2.0.12",
- "tracing",
+ "num-traits",
]
[[package]]
-name = "alloy-network"
-version = "0.11.1"
+name = "atomic-waker"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8be03f2ebc00cf88bd06d3c6caf387dceaa9c7e6b268216779fa68a9bf8ab4e6"
-dependencies = [
- "alloy-consensus",
- "alloy-consensus-any",
- "alloy-eips",
- "alloy-json-rpc",
- "alloy-network-primitives",
- "alloy-primitives",
- "alloy-rpc-types-any",
- "alloy-rpc-types-eth",
- "alloy-serde",
- "alloy-signer",
- "alloy-sol-types",
- "async-trait",
- "auto_impl",
- "futures-utils-wasm",
- "serde",
- "serde_json",
- "thiserror 2.0.12",
-]
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
-name = "alloy-network-primitives"
-version = "0.11.1"
+name = "autocfg"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a00ce618ae2f78369918be0c20f620336381502c83b6ed62c2f7b2db27698b0"
-dependencies = [
- "alloy-consensus",
- "alloy-eips",
- "alloy-primitives",
- "alloy-serde",
- "serde",
-]
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
-name = "alloy-primitives"
-version = "0.8.22"
+name = "axum"
+version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c66bb6715b7499ea755bde4c96223ae8eb74e05c014ab38b9db602879ffb825"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
- "alloy-rlp",
+ "async-trait",
+ "axum-core 0.4.5",
"bytes",
- "cfg-if",
- "const-hex",
- "derive_more 2.0.1",
- "foldhash",
- "hashbrown 0.15.2",
- "indexmap 2.7.1",
+ "futures-util",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.9.0",
+ "hyper-util",
"itoa",
- "k256",
- "keccak-asm",
- "paste",
- "proptest",
- "rand 0.8.6",
- "ruint",
- "rustc-hash 2.1.1",
+ "matchit 0.7.3",
+ "memchr",
+ "mime",
+ "multer",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
"serde",
- "sha3",
- "tiny-keccak",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper 1.0.2",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
]
[[package]]
-name = "alloy-rlp"
-version = "0.3.11"
+name = "axum"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6c1d995bff8d011f7cd6c81820d51825e6e06d6db73914c1630ecf544d83d6"
+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
- "alloy-rlp-derive",
- "arrayvec",
+ "axum-core 0.5.6",
"bytes",
+ "futures-util",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "itoa",
+ "matchit 0.8.4",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "sync_wrapper 1.0.2",
+ "tower",
+ "tower-layer",
+ "tower-service",
]
[[package]]
-name = "alloy-rlp-derive"
-version = "0.3.11"
+name = "axum-core"
+version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a40e1ef334153322fd878d07e86af7a529bcb86b2439525920a88eba87bcf943"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper 1.0.2",
+ "tower-layer",
+ "tower-service",
+ "tracing",
]
[[package]]
-name = "alloy-rpc-types-any"
-version = "0.11.1"
+name = "axum-core"
+version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "318ae46dd12456df42527c3b94c1ae9001e1ceb707f7afe2c7807ac4e49ebad9"
+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
- "alloy-consensus-any",
- "alloy-rpc-types-eth",
- "alloy-serde",
+ "bytes",
+ "futures-core",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper 1.0.2",
+ "tower-layer",
+ "tower-service",
]
[[package]]
-name = "alloy-rpc-types-eth"
-version = "0.11.1"
+name = "base58ck"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b4dbee4d82f8a22dde18c28257bed759afeae7ba73da4a1479a039fd1445d04"
+checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
dependencies = [
- "alloy-consensus",
- "alloy-consensus-any",
- "alloy-eips",
- "alloy-network-primitives",
- "alloy-primitives",
- "alloy-rlp",
- "alloy-serde",
- "alloy-sol-types",
- "itertools 0.14.0",
- "serde",
- "serde_json",
- "thiserror 2.0.12",
+ "bitcoin-internals 0.3.0",
+ "bitcoin_hashes 0.14.1",
]
[[package]]
-name = "alloy-serde"
-version = "0.11.1"
+name = "base64"
+version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8732058f5ca28c1d53d241e8504620b997ef670315d7c8afab856b3e3b80d945"
-dependencies = [
- "alloy-primitives",
- "serde",
- "serde_json",
-]
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
-name = "alloy-signer"
-version = "0.11.1"
+name = "base64"
+version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f96b3526fdd779a4bd0f37319cfb4172db52a7ac24cdbb8804b72091c18e1701"
-dependencies = [
- "alloy-primitives",
- "async-trait",
- "auto_impl",
- "either",
- "elliptic-curve",
- "k256",
- "thiserror 2.0.12",
-]
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
-name = "alloy-signer-local"
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "bech32"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe8f78cd6b7501c7e813a1eb4a087b72d23af51f5bb66d4e948dc840bdd207d8"
-dependencies = [
- "alloy-consensus",
- "alloy-network",
- "alloy-primitives",
- "alloy-signer",
- "async-trait",
- "k256",
- "rand 0.8.6",
- "thiserror 2.0.12",
-]
+checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
[[package]]
-name = "alloy-sol-macro"
-version = "0.8.22"
+name = "bincode"
+version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7f9c3c7bc1f4e334e5c5fc59ec8dac894973a71b11da09065affc6094025049"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
- "alloy-sol-macro-expander",
- "alloy-sol-macro-input",
- "proc-macro-error2",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "serde",
]
[[package]]
-name = "alloy-sol-macro-expander"
-version = "0.8.22"
+name = "bitcoin"
+version = "0.32.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46ff7aa715eb2404cb87fa94390d2c5d5addd70d9617e20b2398ee6f48cb21f0"
+checksum = "9cf93e61f2dbc3e3c41234ca26a65e2c0b0975c52e0f069ab9893ebbede584d3"
dependencies = [
- "alloy-sol-macro-input",
- "const-hex",
- "heck 0.5.0",
- "indexmap 2.7.1",
- "proc-macro-error2",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
- "syn-solidity",
- "tiny-keccak",
+ "base58ck",
+ "bech32",
+ "bitcoin-internals 0.3.0",
+ "bitcoin-io 0.1.4",
+ "bitcoin-units",
+ "bitcoin_hashes 0.14.1",
+ "hex-conservative 0.2.2",
+ "hex_lit",
+ "secp256k1",
+ "serde",
]
[[package]]
-name = "alloy-sol-macro-input"
-version = "0.8.22"
+name = "bitcoin-internals"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f105fa700140c0cc6e2c3377adef650c389ac57b8ead8318a2e6bd52f1ae841"
+checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
dependencies = [
- "const-hex",
- "dunce",
- "heck 0.5.0",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
- "syn-solidity",
+ "serde",
]
[[package]]
-name = "alloy-sol-types"
-version = "0.8.22"
+name = "bitcoin-internals"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f819635439ebb06aa13c96beac9b2e7360c259e90f5160a6848ae0d94d10452"
-dependencies = [
- "alloy-primitives",
- "alloy-sol-macro",
- "const-hex",
-]
+checksum = "a90bbbfa552b49101a230fb2668f3f9ef968c81e6f83cf577e1d4b80f689e1aa"
[[package]]
-name = "alloy-trie"
-version = "0.7.9"
+name = "bitcoin-io"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
+
+[[package]]
+name = "bitcoin-io"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d95a94854e420f07e962f7807485856cde359ab99ab6413883e15235ad996e8b"
+checksum = "26792cd2bf245069a1c5acb06aa7ad7abe1de69b507c90b490bca81e0665d0ee"
dependencies = [
- "alloy-primitives",
- "alloy-rlp",
- "arrayvec",
- "derive_more 1.0.0",
- "nybbles",
- "serde",
- "smallvec",
- "tracing",
+ "bitcoin-internals 0.4.2",
]
[[package]]
-name = "android-tzdata"
-version = "0.1.1"
+name = "bitcoin-units"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+checksum = "346568ebaab2918487cea76dd55dae13c27bb618cdb737c952e69eb2017c4118"
+dependencies = [
+ "bitcoin-internals 0.3.0",
+ "serde",
+]
[[package]]
-name = "android_system_properties"
-version = "0.1.5"
+name = "bitcoin_hashes"
+version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
dependencies = [
- "libc",
+ "bitcoin-io 0.1.4",
+ "hex-conservative 0.2.2",
+ "serde",
]
[[package]]
-name = "ansi_term"
-version = "0.12.1"
+name = "bitcoin_hashes"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+checksum = "7e5d09f16329cd545d7e6008b2c6b2af3a90bc678cf41ac3d2f6755943301b16"
dependencies = [
- "winapi",
+ "bitcoin-io 0.2.0",
+ "hex-conservative 0.3.2",
]
[[package]]
-name = "anstream"
-version = "0.6.18"
+name = "bitcoincore-zmq"
+version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+checksum = "81e38c7506e3278f65cf7c36eee4df9525d2ab9dddf24ed77999b085c5ab3a39"
dependencies = [
- "anstyle",
- "anstyle-parse",
- "anstyle-query",
- "anstyle-wincon",
- "colorchoice",
- "is_terminal_polyfill",
- "utf8parse",
+ "bitcoin",
+ "zmq",
+ "zmq-sys",
]
[[package]]
-name = "anstyle"
-version = "1.0.10"
+name = "bitflags"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
-name = "anstyle-parse"
-version = "0.2.6"
+name = "bitflags"
+version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
dependencies = [
- "utf8parse",
+ "serde_core",
]
[[package]]
-name = "anstyle-query"
-version = "1.1.2"
+name = "block-buffer"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
- "windows-sys 0.59.0",
+ "generic-array",
]
[[package]]
-name = "anstyle-wincon"
-version = "3.0.7"
+name = "bollard"
+version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4"
dependencies = [
- "anstyle",
- "once_cell",
- "windows-sys 0.59.0",
+ "async-stream",
+ "base64 0.22.1",
+ "bitflags 2.11.1",
+ "bollard-buildkit-proto",
+ "bollard-stubs",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "home",
+ "http 1.4.0",
+ "http-body-util",
+ "hyper 1.9.0",
+ "hyper-named-pipe",
+ "hyper-rustls",
+ "hyper-util",
+ "hyperlocal",
+ "log",
+ "num",
+ "pin-project-lite",
+ "rand 0.9.4",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pki-types",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_urlencoded",
+ "thiserror 2.0.18",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "tonic",
+ "tower-service",
+ "url",
+ "winapi",
]
[[package]]
-name = "anyhow"
-version = "1.0.97"
+name = "bollard-buildkit-proto"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
+checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad"
+dependencies = [
+ "prost",
+ "prost-types",
+ "tonic",
+ "tonic-prost",
+ "ureq",
+]
[[package]]
-name = "ark-ff"
-version = "0.3.0"
+name = "bollard-stubs"
+version = "1.52.1-rc.29.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6"
+checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066"
dependencies = [
- "ark-ff-asm 0.3.0",
- "ark-ff-macros 0.3.0",
- "ark-serialize 0.3.0",
- "ark-std 0.3.0",
- "derivative",
- "num-bigint 0.4.6",
- "num-traits",
- "paste",
- "rustc_version 0.3.3",
- "zeroize",
+ "base64 0.22.1",
+ "bollard-buildkit-proto",
+ "bytes",
+ "prost",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "time",
]
[[package]]
-name = "ark-ff"
-version = "0.4.2"
+name = "bs58"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
dependencies = [
- "ark-ff-asm 0.4.2",
- "ark-ff-macros 0.4.2",
- "ark-serialize 0.4.2",
- "ark-std 0.4.0",
- "derivative",
- "digest 0.10.7",
- "itertools 0.10.5",
- "num-bigint 0.4.6",
- "num-traits",
- "paste",
- "rustc_version 0.4.1",
- "zeroize",
+ "tinyvec",
]
[[package]]
-name = "ark-ff-asm"
-version = "0.3.0"
+name = "bumpalo"
+version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44"
-dependencies = [
- "quote",
- "syn 1.0.109",
-]
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
-name = "ark-ff-asm"
-version = "0.4.2"
+name = "byteorder"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348"
-dependencies = [
- "quote",
- "syn 1.0.109",
-]
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
-name = "ark-ff-macros"
-version = "0.3.0"
+name = "bytes"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20"
-dependencies = [
- "num-bigint 0.4.6",
- "num-traits",
- "quote",
- "syn 1.0.109",
-]
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
-name = "ark-ff-macros"
-version = "0.4.2"
+name = "cc"
+version = "1.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565"
+checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
dependencies = [
- "num-bigint 0.4.6",
- "num-traits",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
]
[[package]]
-name = "ark-serialize"
-version = "0.3.0"
+name = "cfg-expr"
+version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
dependencies = [
- "ark-std 0.3.0",
- "digest 0.9.0",
+ "smallvec",
+ "target-lexicon",
]
[[package]]
-name = "ark-serialize"
-version = "0.4.2"
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chacha20"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5"
+checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
- "ark-std 0.4.0",
- "digest 0.10.7",
- "num-bigint 0.4.6",
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
]
[[package]]
-name = "ark-std"
-version = "0.3.0"
+name = "chrono"
+version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
+ "iana-time-zone",
"num-traits",
- "rand 0.8.6",
+ "serde",
+ "windows-link",
]
[[package]]
-name = "ark-std"
-version = "0.4.0"
+name = "concurrent-queue"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
- "num-traits",
- "rand 0.8.6",
+ "crossbeam-utils",
]
[[package]]
-name = "arrayref"
-version = "0.3.9"
+name = "const-oid"
+version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
-name = "arrayvec"
-version = "0.7.6"
+name = "const-random"
+version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
- "serde",
+ "const-random-macro",
]
[[package]]
-name = "async-stream"
-version = "0.3.6"
+name = "const-random-macro"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
- "async-stream-impl",
- "futures-core",
- "pin-project-lite",
+ "getrandom 0.2.17",
+ "once_cell",
+ "tiny-keccak",
]
[[package]]
-name = "async-stream-impl"
-version = "0.3.6"
+name = "core-foundation"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "async-trait"
-version = "0.1.87"
+name = "core-foundation"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "atomic-waker"
-version = "1.1.2"
+name = "core-foundation-sys"
+version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
-name = "auto_impl"
-version = "1.2.1"
+name = "cpufeatures"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "libc",
]
[[package]]
-name = "autocfg"
-version = "1.4.0"
+name = "cpufeatures"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
[[package]]
-name = "axum"
-version = "0.7.9"
+name = "crc"
+version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
dependencies = [
- "async-trait",
- "axum-core",
- "bytes",
- "futures-util",
- "http 1.2.0",
- "http-body 1.0.1",
- "http-body-util",
- "hyper 1.6.0",
- "hyper-util",
- "itoa",
- "matchit",
- "memchr",
- "mime",
- "multer",
- "percent-encoding",
- "pin-project-lite",
- "rustversion",
- "serde",
- "serde_json",
- "serde_path_to_error",
- "serde_urlencoded",
- "sync_wrapper 1.0.2",
- "tokio",
- "tower 0.5.2",
- "tower-layer",
- "tower-service",
- "tracing",
+ "crc-catalog",
]
[[package]]
-name = "axum-core"
-version = "0.4.5"
+name = "crc-catalog"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
-dependencies = [
- "async-trait",
- "bytes",
- "futures-util",
- "http 1.2.0",
- "http-body 1.0.1",
- "http-body-util",
- "mime",
- "pin-project-lite",
- "rustversion",
- "sync_wrapper 1.0.2",
- "tower-layer",
- "tower-service",
- "tracing",
-]
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
[[package]]
-name = "backoff"
-version = "0.4.0"
+name = "crossbeam"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
+checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
- "futures-core",
- "getrandom 0.2.15",
- "instant",
- "pin-project-lite",
- "rand 0.8.6",
- "tokio",
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
]
[[package]]
-name = "backtrace"
-version = "0.3.74"
+name = "crossbeam-channel"
+version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
- "addr2line",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
- "serde",
- "windows-targets 0.52.6",
+ "crossbeam-utils",
]
[[package]]
-name = "base16ct"
-version = "0.2.0"
+name = "crossbeam-deque"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
[[package]]
-name = "base58ck"
-version = "0.1.0"
+name = "crossbeam-epoch"
+version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
- "bitcoin-internals 0.3.0",
- "bitcoin_hashes 0.14.0",
+ "crossbeam-utils",
]
[[package]]
-name = "base64"
-version = "0.12.3"
+name = "crossbeam-queue"
+version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
[[package]]
-name = "base64"
-version = "0.21.7"
+name = "crossbeam-utils"
+version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
-name = "base64"
-version = "0.22.1"
+name = "crunchy"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
-name = "base64ct"
-version = "1.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
-
-[[package]]
-name = "bech32"
-version = "0.11.0"
+name = "crypto-common"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
[[package]]
-name = "bincode"
-version = "1.3.3"
+name = "darling"
+version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
- "serde",
+ "darling_core",
+ "darling_macro",
]
[[package]]
-name = "bindgen"
-version = "0.70.1"
+name = "darling_core"
+version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [
- "bitflags 2.9.0",
- "cexpr",
- "clang-sys",
- "itertools 0.12.1",
- "log",
- "prettyplease",
+ "ident_case",
"proc-macro2",
"quote",
- "regex",
- "rustc-hash 1.1.0",
- "shlex",
- "syn 2.0.100",
+ "strsim",
+ "syn 2.0.117",
]
[[package]]
-name = "bit-set"
-version = "0.8.0"
+name = "darling_macro"
+version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
- "bit-vec",
+ "darling_core",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "bit-vec"
-version = "0.8.0"
+name = "data-encoding"
+version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]]
-name = "bitcoin"
-version = "0.32.5"
+name = "deadpool"
+version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026"
+checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
dependencies = [
- "base58ck",
- "bech32",
- "bitcoin-internals 0.3.0",
- "bitcoin-io 0.1.3",
- "bitcoin-units",
- "bitcoin_hashes 0.14.0",
- "hex-conservative 0.2.1",
- "hex_lit",
- "secp256k1",
- "serde",
+ "deadpool-runtime",
+ "lazy_static",
+ "num_cpus",
+ "tokio",
]
[[package]]
-name = "bitcoin-internals"
-version = "0.3.0"
+name = "deadpool-runtime"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
-dependencies = [
- "serde",
-]
+checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
[[package]]
-name = "bitcoin-internals"
-version = "0.4.0"
+name = "der"
+version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b854212e29b96c8f0fe04cab11d57586c8f3257de0d146c76cb3b42b3eb9118"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
[[package]]
-name = "bitcoin-io"
-version = "0.1.3"
+name = "deranged"
+version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+ "serde_core",
+]
[[package]]
-name = "bitcoin-io"
-version = "0.2.0"
+name = "digest"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26792cd2bf245069a1c5acb06aa7ad7abe1de69b507c90b490bca81e0665d0ee"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
- "bitcoin-internals 0.4.0",
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
]
[[package]]
-name = "bitcoin-units"
-version = "0.1.2"
+name = "dircpy"
+version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2"
+checksum = "ebcbec2b9a580ddee352ac38523d2ecd4dcaad53532957034394556909e27f4b"
dependencies = [
- "bitcoin-internals 0.3.0",
- "serde",
+ "jwalk",
+ "log",
+ "walkdir",
]
[[package]]
-name = "bitcoin_hashes"
-version = "0.14.0"
+name = "displaydoc"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
- "bitcoin-io 0.1.3",
- "hex-conservative 0.2.1",
- "serde",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "bitcoin_hashes"
-version = "0.16.0"
+name = "docker_credential"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e5d09f16329cd545d7e6008b2c6b2af3a90bc678cf41ac3d2f6755943301b16"
+checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d"
dependencies = [
- "bitcoin-io 0.2.0",
- "hex-conservative 0.3.0",
+ "base64 0.22.1",
+ "serde",
+ "serde_json",
]
[[package]]
-name = "bitflags"
-version = "1.3.2"
+name = "dotenvy"
+version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
-name = "bitflags"
-version = "2.9.0"
+name = "dyn-clone"
+version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
-name = "bitvec"
-version = "1.0.1"
+name = "either"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
dependencies = [
- "funty",
- "radium",
- "tap",
- "wyz",
+ "serde",
]
[[package]]
-name = "blake2"
-version = "0.10.6"
+name = "encoding_rs"
+version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
- "digest 0.10.7",
+ "cfg-if",
]
[[package]]
-name = "blake2b_simd"
-version = "1.0.3"
+name = "equivalent"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99"
-dependencies = [
- "arrayref",
- "arrayvec",
- "constant_time_eq",
-]
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
-name = "block-buffer"
-version = "0.10.4"
+name = "errno"
+version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
- "generic-array 0.14.7",
+ "libc",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "block-buffer"
-version = "0.11.0-pre.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ded684142010808eb980d9974ef794da2bcf97d13396143b1515e9f0fb4a10e"
+name = "esplora-client"
+version = "0.11.0"
+source = "git+https://github.com/BitVM/rust-esplora-client?branch=master#a29ee89e6fa003655e179615405761b27e67b973"
dependencies = [
- "crypto-common 0.2.0-pre.5",
+ "bitcoin",
+ "hex-conservative 0.2.2",
+ "log",
+ "minreq",
+ "reqwest 0.11.27",
+ "serde",
+ "serde_json",
+ "tokio",
]
[[package]]
-name = "bls12_381"
-version = "0.7.1"
+name = "etcetera"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3c196a77437e7cc2fb515ce413a6401291578b5afc8ecb29a3c7ab957f05941"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
- "ff 0.12.1",
- "group 0.12.1",
- "pairing",
- "rand_core 0.6.4",
- "subtle",
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
]
[[package]]
-name = "blst"
-version = "0.3.14"
+name = "etcetera"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d"
+checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96"
dependencies = [
- "cc",
- "glob",
- "threadpool",
- "zeroize",
+ "cfg-if",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "bumpalo"
-version = "3.17.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
-
-[[package]]
-name = "byte-slice-cast"
-version = "1.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d"
-
-[[package]]
-name = "bytemuck"
-version = "1.22.0"
+name = "event-listener"
+version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
[[package]]
-name = "byteorder"
-version = "1.5.0"
+name = "fastrand"
+version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
-name = "bytes"
-version = "1.11.1"
+name = "ferroid"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4"
dependencies = [
- "serde",
+ "portable-atomic",
+ "rand 0.10.1",
+ "web-time",
]
[[package]]
-name = "c-kzg"
-version = "1.0.3"
+name = "filetime"
+version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0307f72feab3300336fb803a57134159f6e20139af1357f36c54cb90d8e8928"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
- "blst",
- "cc",
- "glob",
- "hex",
+ "cfg-if",
"libc",
- "once_cell",
- "serde",
]
[[package]]
-name = "camino"
-version = "1.1.9"
+name = "find-msvc-tools"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
-dependencies = [
- "serde",
-]
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
-name = "cargo-platform"
-version = "0.1.9"
+name = "fixed-hash"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c"
dependencies = [
- "serde",
+ "static_assertions",
]
[[package]]
-name = "cargo_metadata"
-version = "0.18.1"
+name = "flume"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
- "camino",
- "cargo-platform",
- "semver 1.0.26",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
+ "futures-core",
+ "futures-sink",
+ "spin",
]
[[package]]
-name = "cbindgen"
-version = "0.27.0"
+name = "fnv"
+version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb"
-dependencies = [
- "clap",
- "heck 0.4.1",
- "indexmap 2.7.1",
- "log",
- "proc-macro2",
- "quote",
- "serde",
- "serde_json",
- "syn 2.0.100",
- "tempfile",
- "toml",
-]
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
-name = "cc"
-version = "1.2.16"
+name = "foldhash"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
-dependencies = [
- "shlex",
-]
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
-name = "cexpr"
-version = "0.6.0"
+name = "foreign-types"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
- "nom",
+ "foreign-types-shared",
]
[[package]]
-name = "cfg-if"
-version = "1.0.0"
+name = "foreign-types-shared"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
-name = "cfg_aliases"
-version = "0.2.1"
+name = "form_urlencoded"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
[[package]]
-name = "chrono"
-version = "0.4.40"
+name = "futures"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
- "android-tzdata",
- "iana-time-zone",
- "num-traits",
- "windows-link",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
]
[[package]]
-name = "clang-sys"
-version = "1.8.1"
+name = "futures-channel"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
- "glob",
- "libc",
- "libloading",
+ "futures-core",
+ "futures-sink",
]
[[package]]
-name = "clap"
-version = "4.5.31"
+name = "futures-core"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
-dependencies = [
- "clap_builder",
- "clap_derive",
-]
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
-name = "clap_builder"
-version = "4.5.31"
+name = "futures-executor"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
- "anstream",
- "anstyle",
- "clap_lex",
- "strsim",
+ "futures-core",
+ "futures-task",
+ "futures-util",
]
[[package]]
-name = "clap_derive"
-version = "4.5.28"
+name = "futures-intrusive"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
- "heck 0.5.0",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "futures-core",
+ "lock_api",
+ "parking_lot",
]
[[package]]
-name = "clap_lex"
-version = "0.7.4"
+name = "futures-io"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
-name = "colorchoice"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
-
-[[package]]
-name = "console"
-version = "0.15.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
-dependencies = [
- "encode_unicode",
- "libc",
- "once_cell",
- "unicode-width",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "const-hex"
-version = "1.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b0485bab839b018a8f1723fc5391819fea5f8f0f32288ef8a735fd096b6160c"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "hex",
- "proptest",
- "serde",
-]
-
-[[package]]
-name = "const-oid"
-version = "0.9.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
-
-[[package]]
-name = "const-oid"
-version = "0.10.0-pre.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7e3352a27098ba6b09546e5f13b15165e6a88b5c2723afecb3ea9576b27e3ea"
-
-[[package]]
-name = "const_format"
-version = "0.2.34"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd"
-dependencies = [
- "const_format_proc_macros",
-]
-
-[[package]]
-name = "const_format_proc_macros"
-version = "0.2.34"
+name = "futures-macro"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
- "unicode-xid",
-]
-
-[[package]]
-name = "constant_time_eq"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
-
-[[package]]
-name = "core-foundation"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "core-foundation"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
-
-[[package]]
-name = "cpufeatures"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crc"
-version = "3.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
-dependencies = [
- "crc-catalog",
-]
-
-[[package]]
-name = "crc-catalog"
-version = "2.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
-
-[[package]]
-name = "crossbeam-channel"
-version = "0.5.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-deque"
-version = "0.8.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
-dependencies = [
- "crossbeam-epoch",
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-epoch"
-version = "0.9.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
-dependencies = [
- "crossbeam-utils",
+ "syn 2.0.117",
]
[[package]]
-name = "crossbeam-utils"
-version = "0.8.21"
+name = "futures-sink"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
-name = "crunchy"
-version = "0.2.3"
+name = "futures-task"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
-name = "crypto-bigint"
-version = "0.5.5"
+name = "futures-util"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
- "generic-array 0.14.7",
- "rand_core 0.6.4",
- "subtle",
- "zeroize",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
]
[[package]]
-name = "crypto-common"
-version = "0.1.6"
+name = "generic-array"
+version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
- "generic-array 0.14.7",
"typenum",
+ "version_check",
]
[[package]]
-name = "crypto-common"
-version = "0.2.0-pre.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7aa2ec04f5120b830272a481e8d9d8ba4dda140d2cda59b0f1110d5eb93c38e"
-dependencies = [
- "getrandom 0.2.15",
- "hybrid-array",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "ctrlc"
-version = "3.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
-dependencies = [
- "nix",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "darling"
-version = "0.20.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
-dependencies = [
- "darling_core",
- "darling_macro",
-]
-
-[[package]]
-name = "darling_core"
-version = "0.20.10"
+name = "getrandom"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
- "fnv",
- "ident_case",
- "proc-macro2",
- "quote",
- "strsim",
- "syn 2.0.100",
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
]
[[package]]
-name = "darling_macro"
-version = "0.20.10"
+name = "getrandom"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
- "darling_core",
- "quote",
- "syn 2.0.100",
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
]
[[package]]
-name = "dashu"
+name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85b3e5ac1e23ff1995ef05b912e2b012a8784506987a2651552db2c73fb3d7e0"
-dependencies = [
- "dashu-base",
- "dashu-float",
- "dashu-int",
- "dashu-macros",
- "dashu-ratio",
- "rustversion",
-]
-
-[[package]]
-name = "dashu-base"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0b80bf6b85aa68c58ffea2ddb040109943049ce3fbdf4385d0380aef08ef289"
-
-[[package]]
-name = "dashu-float"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85078445a8dbd2e1bd21f04a816f352db8d333643f0c9b78ca7c3d1df71063e7"
-dependencies = [
- "dashu-base",
- "dashu-int",
- "num-modular",
- "num-order",
- "rustversion",
- "static_assertions",
-]
-
-[[package]]
-name = "dashu-int"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee99d08031ca34a4d044efbbb21dff9b8c54bb9d8c82a189187c0651ffdb9fbf"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
- "dashu-base",
- "num-modular",
- "num-order",
- "rustversion",
- "static_assertions",
-]
-
-[[package]]
-name = "dashu-macros"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93381c3ef6366766f6e9ed9cf09e4ef9dec69499baf04f0c60e70d653cf0ab10"
-dependencies = [
- "dashu-base",
- "dashu-float",
- "dashu-int",
- "dashu-ratio",
- "paste",
- "proc-macro2",
- "quote",
- "rustversion",
-]
-
-[[package]]
-name = "dashu-ratio"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47e33b04dd7ce1ccf8a02a69d3419e354f2bbfdf4eb911a0b7465487248764c9"
-dependencies = [
- "dashu-base",
- "dashu-float",
- "dashu-int",
- "num-modular",
- "num-order",
- "rustversion",
-]
-
-[[package]]
-name = "der"
-version = "0.7.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
-dependencies = [
- "const-oid 0.9.6",
- "pem-rfc7468",
- "zeroize",
-]
-
-[[package]]
-name = "deranged"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
-dependencies = [
- "powerfmt",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+ "wasip2",
+ "wasip3",
]
[[package]]
-name = "derivative"
-version = "2.2.0"
+name = "h2"
+version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap 2.14.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
]
[[package]]
-name = "derive_builder"
-version = "0.20.2"
+name = "h2"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
-dependencies = [
- "derive_builder_macro",
-]
-
-[[package]]
-name = "derive_builder_core"
-version = "0.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
-dependencies = [
- "darling",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "derive_builder_macro"
-version = "0.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
-dependencies = [
- "derive_builder_core",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "derive_more"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
-dependencies = [
- "derive_more-impl 1.0.0",
-]
-
-[[package]]
-name = "derive_more"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
-dependencies = [
- "derive_more-impl 2.0.1",
-]
-
-[[package]]
-name = "derive_more-impl"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "derive_more-impl"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
- "unicode-xid",
-]
-
-[[package]]
-name = "digest"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
-dependencies = [
- "generic-array 0.14.7",
-]
-
-[[package]]
-name = "digest"
-version = "0.10.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
-dependencies = [
- "block-buffer 0.10.4",
- "const-oid 0.9.6",
- "crypto-common 0.1.6",
- "subtle",
-]
-
-[[package]]
-name = "digest"
-version = "0.11.0-pre.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "065d93ead7c220b85d5b4be4795d8398eac4ff68b5ee63895de0a3c1fb6edf25"
-dependencies = [
- "block-buffer 0.11.0-pre.5",
- "const-oid 0.10.0-pre.2",
- "crypto-common 0.2.0-pre.5",
-]
-
-[[package]]
-name = "dirs"
-version = "5.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
-dependencies = [
- "dirs-sys",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
-dependencies = [
- "libc",
- "option-ext",
- "redox_users",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "displaydoc"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "downcast-rs"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
-
-[[package]]
-name = "downloader"
-version = "0.2.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ac1e888d6830712d565b2f3a974be3200be9296bc1b03db8251a4cbf18a4a34"
-dependencies = [
- "digest 0.10.7",
- "futures",
- "rand 0.8.6",
- "reqwest 0.12.12",
- "thiserror 1.0.69",
- "tokio",
-]
-
-[[package]]
-name = "dunce"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
-
-[[package]]
-name = "ecdsa"
-version = "0.16.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
-dependencies = [
- "der",
- "digest 0.10.7",
- "elliptic-curve",
- "rfc6979",
- "signature",
- "spki",
-]
-
-[[package]]
-name = "either"
-version = "1.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
-
-[[package]]
-name = "elf"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b"
-
-[[package]]
-name = "elliptic-curve"
-version = "0.13.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
-dependencies = [
- "base16ct",
- "crypto-bigint",
- "digest 0.10.7",
- "ff 0.13.1",
- "generic-array 0.14.7",
- "group 0.13.0",
- "pem-rfc7468",
- "pkcs8",
- "rand_core 0.6.4",
- "sec1",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "encode_unicode"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
-
-[[package]]
-name = "encoding_rs"
-version = "0.8.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "enum-map"
-version = "2.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9"
-dependencies = [
- "enum-map-derive",
- "serde",
-]
-
-[[package]]
-name = "enum-map-derive"
-version = "0.17.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "equivalent"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
-
-[[package]]
-name = "errno"
-version = "0.3.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
-dependencies = [
- "libc",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "esplora-client"
-version = "0.11.0"
-source = "git+https://github.com/BitVM/rust-esplora-client?branch=master#7befb9147b69126edaad8b9dbd0b13259f2e9ea0"
-dependencies = [
- "bitcoin",
- "hex-conservative 0.2.1",
- "log",
- "minreq",
- "reqwest 0.11.27",
- "serde",
- "tokio",
-]
-
-[[package]]
-name = "eyre"
-version = "0.6.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
-dependencies = [
- "indenter",
- "once_cell",
-]
-
-[[package]]
-name = "fastrand"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
-
-[[package]]
-name = "fastrlp"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418"
-dependencies = [
- "arrayvec",
- "auto_impl",
- "bytes",
-]
-
-[[package]]
-name = "fastrlp"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4"
-dependencies = [
- "arrayvec",
- "auto_impl",
- "bytes",
-]
-
-[[package]]
-name = "ff"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
-dependencies = [
- "bitvec",
- "rand_core 0.6.4",
- "subtle",
-]
-
-[[package]]
-name = "ff"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
-dependencies = [
- "bitvec",
- "byteorder",
- "ff_derive",
- "rand_core 0.6.4",
- "subtle",
-]
-
-[[package]]
-name = "ff_derive"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f10d12652036b0e99197587c6ba87a8fc3031986499973c030d8b44fcc151b60"
-dependencies = [
- "addchain",
- "num-bigint 0.3.3",
- "num-integer",
- "num-traits",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "fixed-hash"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534"
-dependencies = [
- "byteorder",
- "rand 0.8.6",
- "rustc-hex",
- "static_assertions",
-]
-
-[[package]]
-name = "fnv"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
-
-[[package]]
-name = "foldhash"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
-
-[[package]]
-name = "foreign-types"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
-dependencies = [
- "foreign-types-shared",
-]
-
-[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
-
-[[package]]
-name = "form_urlencoded"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
-dependencies = [
- "percent-encoding",
-]
-
-[[package]]
-name = "funty"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
-
-[[package]]
-name = "futures"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-channel"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
-dependencies = [
- "futures-core",
- "futures-sink",
-]
-
-[[package]]
-name = "futures-core"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
-
-[[package]]
-name = "futures-executor"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-io"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
-
-[[package]]
-name = "futures-macro"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "futures-sink"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
-
-[[package]]
-name = "futures-task"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
-
-[[package]]
-name = "futures-util"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-macro",
- "futures-sink",
- "futures-task",
- "memchr",
- "pin-project-lite",
- "pin-utils",
- "slab",
-]
-
-[[package]]
-name = "futures-utils-wasm"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9"
-
-[[package]]
-name = "gcd"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a"
-
-[[package]]
-name = "generic-array"
-version = "0.14.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
-dependencies = [
- "typenum",
- "version_check",
- "zeroize",
-]
-
-[[package]]
-name = "generic-array"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96512db27971c2c3eece70a1e106fbe6c87760234e31e8f7e5634912fe52794a"
-dependencies = [
- "serde",
- "typenum",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
-dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
-dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "wasi 0.13.3+wasi-0.2.2",
- "wasm-bindgen",
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "gimli"
-version = "0.31.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
-
-[[package]]
-name = "glob"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
-
-[[package]]
-name = "group"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
-dependencies = [
- "ff 0.12.1",
- "memuse",
- "rand_core 0.6.4",
- "subtle",
-]
-
-[[package]]
-name = "group"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
-dependencies = [
- "ff 0.13.1",
- "rand_core 0.6.4",
- "subtle",
-]
-
-[[package]]
-name = "h2"
-version = "0.3.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
-dependencies = [
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "futures-util",
- "http 0.2.12",
- "indexmap 2.7.1",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
-name = "h2"
-version = "0.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
-dependencies = [
- "atomic-waker",
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "http 1.2.0",
- "indexmap 2.7.1",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
-name = "halo2"
-version = "0.1.0-beta.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a23c779b38253fe1538102da44ad5bd5378495a61d2c4ee18d64eaa61ae5995"
-dependencies = [
- "halo2_proofs",
-]
-
-[[package]]
-name = "halo2_proofs"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e925780549adee8364c7f2b685c753f6f3df23bde520c67416e93bf615933760"
-dependencies = [
- "blake2b_simd",
- "ff 0.12.1",
- "group 0.12.1",
- "pasta_curves 0.4.1",
- "rand_core 0.6.4",
- "rayon",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-
-[[package]]
-name = "hashbrown"
-version = "0.14.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
-dependencies = [
- "ahash",
- "allocator-api2",
- "serde",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
-dependencies = [
- "allocator-api2",
- "equivalent",
- "foldhash",
- "serde",
-]
-
-[[package]]
-name = "heck"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
-
-[[package]]
-name = "heck"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-
-[[package]]
-name = "hermit-abi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
-
-[[package]]
-name = "hex"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "hex-conservative"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd"
-dependencies = [
- "arrayvec",
-]
-
-[[package]]
-name = "hex-conservative"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4afe881d0527571892c4034822e59bb10c6c991cce6abe8199b6f5cf10766f55"
-dependencies = [
- "arrayvec",
-]
-
-[[package]]
-name = "hex_lit"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
-
-[[package]]
-name = "hmac"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
-dependencies = [
- "digest 0.10.7",
-]
-
-[[package]]
-name = "http"
-version = "0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
-dependencies = [
- "bytes",
- "fnv",
- "itoa",
-]
-
-[[package]]
-name = "http"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
-dependencies = [
- "bytes",
- "fnv",
- "itoa",
-]
-
-[[package]]
-name = "http-body"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
-dependencies = [
- "bytes",
- "http 0.2.12",
- "pin-project-lite",
-]
-
-[[package]]
-name = "http-body"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
-dependencies = [
- "bytes",
- "http 1.2.0",
-]
-
-[[package]]
-name = "http-body-util"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
-dependencies = [
- "bytes",
- "futures-util",
- "http 1.2.0",
- "http-body 1.0.1",
- "pin-project-lite",
-]
-
-[[package]]
-name = "http-range-header"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
-
-[[package]]
-name = "httparse"
-version = "1.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
-
-[[package]]
-name = "httpdate"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
-
-[[package]]
-name = "hybrid-array"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9"
-dependencies = [
- "typenum",
-]
-
-[[package]]
-name = "hyper"
-version = "0.14.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-core",
- "futures-util",
- "h2 0.3.26",
- "http 0.2.12",
- "http-body 0.4.6",
- "httparse",
- "httpdate",
- "itoa",
- "pin-project-lite",
- "socket2",
- "tokio",
- "tower-service",
- "tracing",
- "want",
-]
-
-[[package]]
-name = "hyper"
-version = "1.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-util",
- "h2 0.4.8",
- "http 1.2.0",
- "http-body 1.0.1",
- "httparse",
- "httpdate",
- "itoa",
- "pin-project-lite",
- "smallvec",
- "tokio",
- "want",
-]
-
-[[package]]
-name = "hyper-rustls"
-version = "0.27.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
-dependencies = [
- "futures-util",
- "http 1.2.0",
- "hyper 1.6.0",
- "hyper-util",
- "rustls",
- "rustls-pki-types",
- "tokio",
- "tokio-rustls",
- "tower-service",
- "webpki-roots",
-]
-
-[[package]]
-name = "hyper-timeout"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
-dependencies = [
- "hyper 1.6.0",
- "hyper-util",
- "pin-project-lite",
- "tokio",
- "tower-service",
-]
-
-[[package]]
-name = "hyper-tls"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
-dependencies = [
- "bytes",
- "hyper 0.14.32",
- "native-tls",
- "tokio",
- "tokio-native-tls",
-]
-
-[[package]]
-name = "hyper-util"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
+checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
dependencies = [
+ "atomic-waker",
"bytes",
- "futures-channel",
- "futures-util",
- "http 1.2.0",
- "http-body 1.0.1",
- "hyper 1.6.0",
- "pin-project-lite",
- "socket2",
- "tokio",
- "tower-service",
- "tracing",
-]
-
-[[package]]
-name = "iana-time-zone"
-version = "0.1.61"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
-dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "wasm-bindgen",
- "windows-core",
-]
-
-[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "icu_collections"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
-dependencies = [
- "displaydoc",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
-dependencies = [
- "displaydoc",
- "litemap",
- "tinystr",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_locid_transform_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
-
-[[package]]
-name = "icu_normalizer"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
-dependencies = [
- "displaydoc",
- "icu_collections",
- "icu_normalizer_data",
- "icu_properties",
- "icu_provider",
- "smallvec",
- "utf16_iter",
- "utf8_iter",
- "write16",
- "zerovec",
-]
-
-[[package]]
-name = "icu_normalizer_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
-
-[[package]]
-name = "icu_properties"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
-dependencies = [
- "displaydoc",
- "icu_collections",
- "icu_locid_transform",
- "icu_properties_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_properties_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
-
-[[package]]
-name = "icu_provider"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_provider_macros",
- "stable_deref_trait",
- "tinystr",
- "writeable",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_provider_macros"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "ident_case"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
-
-[[package]]
-name = "idna"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
-dependencies = [
- "idna_adapter",
- "smallvec",
- "utf8_iter",
-]
-
-[[package]]
-name = "idna_adapter"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
-dependencies = [
- "icu_normalizer",
- "icu_properties",
-]
-
-[[package]]
-name = "impl-codec"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f"
-dependencies = [
- "parity-scale-codec",
-]
-
-[[package]]
-name = "impl-trait-for-tuples"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http 1.4.0",
+ "indexmap 2.14.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
]
[[package]]
-name = "indenter"
-version = "0.3.3"
+name = "hashbrown"
+version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
-name = "indexmap"
-version = "1.9.3"
+name = "hashbrown"
+version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
- "autocfg",
- "hashbrown 0.12.3",
+ "ahash",
+ "rayon",
+ "serde",
]
[[package]]
-name = "indexmap"
-version = "2.7.1"
+name = "hashbrown"
+version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
+ "allocator-api2",
"equivalent",
- "hashbrown 0.15.2",
- "serde",
+ "foldhash",
]
[[package]]
-name = "indicatif"
-version = "0.17.11"
+name = "hashbrown"
+version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
-dependencies = [
- "console",
- "number_prefix",
- "portable-atomic",
- "unicode-width",
- "web-time",
-]
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
-name = "instant"
-version = "0.1.13"
+name = "hashlink"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
- "cfg-if",
+ "hashbrown 0.15.5",
]
[[package]]
-name = "ipnet"
-version = "2.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
-
-[[package]]
-name = "is_terminal_polyfill"
-version = "1.70.1"
+name = "heck"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
-name = "itertools"
-version = "0.10.5"
+name = "hermit-abi"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
-dependencies = [
- "either",
-]
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
-name = "itertools"
-version = "0.12.1"
+name = "hex"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
-name = "itertools"
-version = "0.13.0"
+name = "hex-conservative"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
dependencies = [
- "either",
+ "arrayvec",
]
[[package]]
-name = "itertools"
-version = "0.14.0"
+name = "hex-conservative"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360"
dependencies = [
- "either",
+ "arrayvec",
]
[[package]]
-name = "itoa"
-version = "1.0.15"
+name = "hex_lit"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
[[package]]
-name = "js-sys"
-version = "0.3.98"
+name = "hkdf"
+version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
- "cfg-if",
- "futures-util",
- "once_cell",
- "wasm-bindgen",
+ "hmac",
]
[[package]]
-name = "jubjub"
-version = "0.9.0"
+name = "hmac"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a575df5f985fe1cd5b2b05664ff6accfc46559032b954529fd225a2168d27b0f"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
- "bitvec",
- "bls12_381",
- "ff 0.12.1",
- "group 0.12.1",
- "rand_core 0.6.4",
- "subtle",
+ "digest",
]
[[package]]
-name = "k256"
-version = "0.13.4"
+name = "home"
+version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
- "cfg-if",
- "ecdsa",
- "elliptic-curve",
- "once_cell",
- "sha2 0.10.8",
- "signature",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "keccak"
-version = "0.1.6"
+name = "http"
+version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
- "cpufeatures",
+ "bytes",
+ "fnv",
+ "itoa",
]
[[package]]
-name = "keccak-asm"
-version = "0.1.4"
+name = "http"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
- "digest 0.10.7",
- "sha3-asm",
+ "bytes",
+ "itoa",
]
[[package]]
-name = "lazy_static"
-version = "1.5.0"
+name = "http-body"
+version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
- "spin",
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite",
]
[[package]]
-name = "libc"
-version = "0.2.170"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
-
-[[package]]
-name = "libloading"
-version = "0.8.6"
+name = "http-body"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
- "cfg-if",
- "windows-targets 0.48.5",
+ "bytes",
+ "http 1.4.0",
]
[[package]]
-name = "libm"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
-
-[[package]]
-name = "libredox"
+name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
- "bitflags 2.9.0",
- "libc",
+ "bytes",
+ "futures-core",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "pin-project-lite",
]
[[package]]
-name = "linux-raw-sys"
-version = "0.9.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9"
-
-[[package]]
-name = "litemap"
-version = "0.7.5"
+name = "http-range-header"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
+checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]]
-name = "lock_api"
-version = "0.4.12"
+name = "httparse"
+version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
-dependencies = [
- "autocfg",
- "scopeguard",
-]
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
-name = "log"
-version = "0.4.26"
+name = "httpdate"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
-name = "lru"
-version = "0.12.5"
+name = "hyper"
+version = "0.14.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
+checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
dependencies = [
- "hashbrown 0.15.2",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2 0.3.27",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.5.10",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
]
[[package]]
-name = "lru-slab"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
-
-[[package]]
-name = "matchers"
-version = "0.2.0"
+name = "hyper"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [
- "regex-automata",
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2 0.4.14",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
]
[[package]]
-name = "matchit"
-version = "0.7.3"
+name = "hyper-named-pipe"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
+dependencies = [
+ "hex",
+ "hyper 1.9.0",
+ "hyper-util",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+ "winapi",
+]
[[package]]
-name = "memchr"
-version = "2.7.4"
+name = "hyper-rustls"
+version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http 1.4.0",
+ "hyper 1.9.0",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots 1.0.7",
+]
[[package]]
-name = "memuse"
-version = "0.2.2"
+name = "hyper-timeout"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964"
+checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
+dependencies = [
+ "hyper 1.9.0",
+ "hyper-util",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
[[package]]
-name = "mime"
-version = "0.3.17"
+name = "hyper-tls"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper 0.14.32",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
[[package]]
-name = "mime_guess"
-version = "2.0.5"
+name = "hyper-util"
+version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
- "mime",
- "unicase",
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "hyper 1.9.0",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2 0.6.3",
+ "tokio",
+ "tower-service",
+ "tracing",
]
[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
-name = "miniz_oxide"
-version = "0.8.5"
+name = "hyperlocal"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
+checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
dependencies = [
- "adler2",
+ "hex",
+ "http-body-util",
+ "hyper 1.9.0",
+ "hyper-util",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
]
[[package]]
-name = "minreq"
-version = "2.13.2"
+name = "iana-time-zone"
+version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da0c420feb01b9fb5061f8c8f452534361dd783756dcf38ec45191ce55e7a161"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
- "base64 0.12.3",
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
"log",
- "serde",
- "serde_json",
+ "wasm-bindgen",
+ "windows-core",
]
[[package]]
-name = "mio"
-version = "1.0.3"
+name = "iana-time-zone-haiku"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
- "libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.52.0",
+ "cc",
]
[[package]]
-name = "multer"
-version = "3.1.0"
+name = "icu_collections"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [
- "bytes",
- "encoding_rs",
- "futures-util",
- "http 1.2.0",
- "httparse",
- "memchr",
- "mime",
- "spin",
- "version_check",
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
]
[[package]]
-name = "native-tls"
-version = "0.2.14"
+name = "icu_locale_core"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [
- "libc",
- "log",
- "openssl",
- "openssl-probe",
- "openssl-sys",
- "schannel",
- "security-framework 2.11.1",
- "security-framework-sys",
- "tempfile",
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
]
[[package]]
-name = "nix"
-version = "0.29.0"
+name = "icu_normalizer"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
dependencies = [
- "bitflags 2.9.0",
- "cfg-if",
- "cfg_aliases",
- "libc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
]
[[package]]
-name = "nohash-hasher"
-version = "0.2.0"
+name = "icu_normalizer_data"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
[[package]]
-name = "nom"
-version = "7.1.3"
+name = "icu_properties"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
dependencies = [
- "memchr",
- "minimal-lexical",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
]
[[package]]
-name = "ntapi"
-version = "0.4.1"
+name = "icu_properties_data"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
-dependencies = [
- "winapi",
-]
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
[[package]]
-name = "nu-ansi-term"
-version = "0.50.3"
+name = "icu_provider"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [
- "windows-sys 0.59.0",
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
]
[[package]]
-name = "num"
-version = "0.4.3"
+name = "id-arena"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
- "num-bigint 0.4.6",
- "num-complex",
- "num-integer",
- "num-iter",
- "num-rational",
- "num-traits",
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
]
[[package]]
-name = "num-bigint"
-version = "0.3.3"
+name = "idna_adapter"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
+ "icu_normalizer",
+ "icu_properties",
]
[[package]]
-name = "num-bigint"
-version = "0.4.6"
+name = "indexmap"
+version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
- "num-integer",
- "num-traits",
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
]
[[package]]
-name = "num-complex"
-version = "0.4.6"
+name = "indexmap"
+version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
- "num-traits",
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
]
[[package]]
-name = "num-conv"
-version = "0.1.0"
+name = "ipnet"
+version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
-name = "num-integer"
-version = "0.1.46"
+name = "itertools"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
- "num-traits",
+ "either",
]
[[package]]
-name = "num-iter"
-version = "0.1.45"
+name = "itertools"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
+ "either",
]
[[package]]
-name = "num-modular"
-version = "0.6.1"
+name = "itoa"
+version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
-name = "num-order"
-version = "1.2.0"
+name = "jobserver"
+version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
- "num-modular",
+ "getrandom 0.3.4",
+ "libc",
]
[[package]]
-name = "num-rational"
-version = "0.4.2"
+name = "js-sys"
+version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [
- "num-bigint 0.4.6",
- "num-integer",
- "num-traits",
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
]
[[package]]
-name = "num-traits"
-version = "0.2.19"
+name = "jwalk"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56"
dependencies = [
- "autocfg",
- "libm",
+ "crossbeam",
+ "rayon",
]
[[package]]
-name = "num_cpus"
-version = "1.16.0"
+name = "keccak-hash"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+checksum = "ce2bd4c29270e724d3eaadf7bdc8700af4221fc0ed771b855eadcd1b98d52851"
dependencies = [
- "hermit-abi",
- "libc",
+ "primitive-types",
+ "tiny-keccak",
]
[[package]]
-name = "num_enum"
-version = "0.5.11"
+name = "lazy_static"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
- "num_enum_derive",
+ "spin",
]
[[package]]
-name = "num_enum_derive"
-version = "0.5.11"
+name = "leb128fmt"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
-dependencies = [
- "proc-macro-crate 1.3.1",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
-name = "number_prefix"
-version = "0.4.0"
+name = "libc"
+version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
-name = "nybbles"
-version = "0.3.4"
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
dependencies = [
- "const-hex",
- "serde",
- "smallvec",
+ "bitflags 2.11.1",
+ "libc",
+ "plain",
+ "redox_syscall 0.7.5",
]
[[package]]
-name = "object"
-version = "0.36.7"
+name = "libsqlite3-sys"
+version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
- "memchr",
+ "pkg-config",
+ "vcpkg",
]
[[package]]
-name = "once_cell"
-version = "1.20.3"
+name = "linux-raw-sys"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
-name = "openssl"
-version = "0.10.79"
+name = "litemap"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
-dependencies = [
- "bitflags 2.9.0",
- "cfg-if",
- "foreign-types",
- "libc",
- "openssl-macros",
- "openssl-sys",
-]
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
-name = "openssl-macros"
-version = "0.1.1"
+name = "lock_api"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "scopeguard",
]
[[package]]
-name = "openssl-probe"
-version = "0.1.6"
+name = "log"
+version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
-name = "openssl-sys"
-version = "0.9.115"
+name = "lru-slab"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
- "vcpkg",
-]
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
-name = "option-ext"
-version = "0.2.0"
+name = "matchit"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
-name = "p256"
-version = "0.13.2"
+name = "md-5"
+version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
- "ecdsa",
- "elliptic-curve",
- "primeorder",
- "sha2 0.10.8",
+ "cfg-if",
+ "digest",
]
[[package]]
-name = "p3-air"
-version = "0.2.0-succinct"
+name = "memchr"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02634a874a2286b73f3e0a121e79d6774e92ccbec648c5568f4a7479a4830858"
-dependencies = [
- "p3-field",
- "p3-matrix",
-]
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
-name = "p3-baby-bear"
-version = "0.2.0-succinct"
+name = "mime_guess"
+version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "080896e9d09e9761982febafe3b3da5cbf320e32f0c89b6e2e01e875129f4c2d"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
- "num-bigint 0.4.6",
- "p3-field",
- "p3-mds",
- "p3-poseidon2",
- "p3-symmetric",
- "rand 0.8.6",
- "serde",
+ "mime",
+ "unicase",
]
[[package]]
-name = "p3-bn254-fr"
-version = "0.2.0-succinct"
+name = "minreq"
+version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8c53da73873e24d751ec3bd9d8da034bb5f99c71f24f4903ff37190182bff10"
+checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d"
dependencies = [
- "ff 0.13.1",
- "num-bigint 0.4.6",
- "p3-field",
- "p3-poseidon2",
- "p3-symmetric",
- "rand 0.8.6",
+ "base64 0.22.1",
"serde",
+ "serde_json",
]
[[package]]
-name = "p3-challenger"
-version = "0.2.0-succinct"
+name = "mio"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f5c497659a7d9a87882e30ee9a8d0e20c8dcd32cd10d432410e7d6f146ef103"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
- "p3-field",
- "p3-maybe-rayon",
- "p3-symmetric",
- "p3-util",
- "serde",
- "tracing",
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "p3-commit"
-version = "0.2.0-succinct"
+name = "multer"
+version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54ec340c5cb17739a7b9ee189378bdac8f0e684b9b5ce539476c26e77cd6a27d"
+checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
- "itertools 0.12.1",
- "p3-challenger",
- "p3-field",
- "p3-matrix",
- "p3-util",
- "serde",
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http 1.4.0",
+ "httparse",
+ "memchr",
+ "mime",
+ "spin",
+ "version_check",
]
[[package]]
-name = "p3-dft"
-version = "0.2.0-succinct"
+name = "native-tls"
+version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "292e97d02d4c38d8b306c2b8c0428bf15f4d32a11a40bcf80018f675bf33267e"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
dependencies = [
- "p3-field",
- "p3-matrix",
- "p3-maybe-rayon",
- "p3-util",
- "tracing",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
]
[[package]]
-name = "p3-field"
-version = "0.2.0-succinct"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f91d8e5f9ede1171adafdb0b6a0df1827fbd4eb6a6217bfa36374e5d86248757"
+name = "node"
+version = "1.1.0"
dependencies = [
- "itertools 0.12.1",
- "num-bigint 0.4.6",
- "num-traits",
- "p3-util",
+ "anyhow",
+ "axum 0.7.9",
+ "bincode",
+ "bitcoin",
+ "bitcoin_hashes 0.16.0",
+ "bitcoincore-zmq",
+ "esplora-client",
+ "futures-util",
+ "hex",
+ "http-body-util",
+ "lazy_static",
"rand 0.8.6",
+ "reqwest 0.12.28",
"serde",
+ "serde_json",
+ "sha2",
+ "shared",
+ "sqlx",
+ "tempfile",
+ "testcontainers",
+ "testcontainers-modules",
+ "tokio",
+ "tokio-tungstenite",
+ "tower",
+ "tower-http 0.5.2",
+ "wiremock",
+ "zkcoins-program-plonky2",
+ "zkcoins-prover-plonky2",
]
[[package]]
-name = "p3-fri"
-version = "0.2.0-succinct"
+name = "num"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ef838ff24d9b3de3d88d0ac984937d2aa2923bf25cb108ba9b2dc357e472197"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
- "itertools 0.12.1",
- "p3-challenger",
- "p3-commit",
- "p3-dft",
- "p3-field",
- "p3-interpolation",
- "p3-matrix",
- "p3-maybe-rayon",
- "p3-util",
- "serde",
- "tracing",
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
]
[[package]]
-name = "p3-interpolation"
-version = "0.2.0-succinct"
+name = "num-bigint"
+version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c806c3afb8d6acf1d3a78f4be1e9e8b026f13c01b0cdd5ae2e068b70a3ba6d80"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
- "p3-field",
- "p3-matrix",
- "p3-util",
+ "num-integer",
+ "num-traits",
+ "rand 0.8.6",
]
[[package]]
-name = "p3-keccak-air"
-version = "0.2.0-succinct"
+name = "num-bigint-dig"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b46cef7ee8ae1f7cb560e7b7c137e272f6ba75be98179b3aa18695705231e0fb"
+checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
- "p3-air",
- "p3-field",
- "p3-matrix",
- "p3-maybe-rayon",
- "p3-util",
- "tracing",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand 0.8.6",
+ "smallvec",
+ "zeroize",
]
[[package]]
-name = "p3-matrix"
-version = "0.2.0-succinct"
+name = "num-complex"
+version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98bf2c7680b8e906a5e147fe4ceb05a11cc9fa35678aa724333bcb35c72483c1"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
- "itertools 0.12.1",
- "p3-field",
- "p3-maybe-rayon",
- "p3-util",
+ "num-traits",
"rand 0.8.6",
- "serde",
- "tracing",
]
[[package]]
-name = "p3-maybe-rayon"
-version = "0.2.0-succinct"
+name = "num-conv"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd9ac6f1d11ad4d3c13cc496911109d6282315e64f851a666ed80ad4d77c0983"
-dependencies = [
- "rayon",
-]
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
-name = "p3-mds"
-version = "0.2.0-succinct"
+name = "num-integer"
+version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "706cea48976f54702dc68dffa512684c1304d1a3606cadea423cfe0b1ee25134"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
- "itertools 0.12.1",
- "p3-dft",
- "p3-field",
- "p3-matrix",
- "p3-symmetric",
- "p3-util",
- "rand 0.8.6",
+ "num-traits",
]
[[package]]
-name = "p3-merkle-tree"
-version = "0.2.0-succinct"
+name = "num-iter"
+version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f4ced385da80dd6b3fd830eaa452c9fa899f2dc3f6463aceba00620d5f071ec"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
- "itertools 0.12.1",
- "p3-commit",
- "p3-field",
- "p3-matrix",
- "p3-maybe-rayon",
- "p3-symmetric",
- "p3-util",
- "serde",
- "tracing",
+ "autocfg",
+ "num-integer",
+ "num-traits",
]
[[package]]
-name = "p3-poseidon2"
-version = "0.2.0-succinct"
+name = "num-rational"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2ce5f5ec7f1ba3a233a671621029def7bd416e7c51218c9d1167d21602cf312"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
- "gcd",
- "p3-field",
- "p3-mds",
- "p3-symmetric",
- "rand 0.8.6",
- "serde",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
]
[[package]]
-name = "p3-symmetric"
-version = "0.2.0-succinct"
+name = "num-traits"
+version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f29dc5bb6c99d3de75869d5c086874b64890280eeb7d3e068955f939e219253"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
- "itertools 0.12.1",
- "p3-field",
- "serde",
+ "autocfg",
+ "libm",
]
[[package]]
-name = "p3-uni-stark"
-version = "0.2.0-succinct"
+name = "num_cpus"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83ceaeef06b0bc97e5af2d220cd340b0b3a72bdf37e4584b73b3bc357cfc9ed3"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
- "itertools 0.12.1",
- "p3-air",
- "p3-challenger",
- "p3-commit",
- "p3-dft",
- "p3-field",
- "p3-matrix",
- "p3-maybe-rayon",
- "p3-util",
- "serde",
- "tracing",
+ "hermit-abi",
+ "libc",
]
[[package]]
-name = "p3-util"
-version = "0.2.0-succinct"
+name = "once_cell"
+version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1b84d324cd4ac09194a9d0e8ab1834e67a0e47dec477c28fcf9d68b2824c1fe"
-dependencies = [
- "serde",
-]
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
-name = "pairing"
-version = "0.22.0"
+name = "openssl"
+version = "0.10.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "135590d8bdba2b31346f9cd1fb2a912329f5135e832a4f422942eb6ead8b6b3b"
+checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
dependencies = [
- "group 0.12.1",
+ "bitflags 2.11.1",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "openssl-macros",
+ "openssl-sys",
]
[[package]]
-name = "parity-scale-codec"
-version = "3.7.4"
+name = "openssl-macros"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
- "arrayvec",
- "bitvec",
- "byte-slice-cast",
- "const_format",
- "impl-trait-for-tuples",
- "parity-scale-codec-derive",
- "rustversion",
- "serde",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "parity-scale-codec-derive"
-version = "3.7.4"
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3"
+checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
dependencies = [
- "proc-macro-crate 3.3.0",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
]
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
[[package]]
name = "parking_lot"
-version = "0.12.3"
+version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -3597,59 +2156,42 @@ dependencies = [
[[package]]
name = "parking_lot_core"
-version = "0.9.10"
+version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall",
+ "redox_syscall 0.5.18",
"smallvec",
- "windows-targets 0.52.6",
+ "windows-link",
]
[[package]]
-name = "pasta_curves"
-version = "0.4.1"
+name = "parse-display"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cc65faf8e7313b4b1fbaa9f7ca917a0eed499a9663be71477f87993604341d8"
+checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a"
dependencies = [
- "blake2b_simd",
- "ff 0.12.1",
- "group 0.12.1",
- "lazy_static",
- "rand 0.8.6",
- "static_assertions",
- "subtle",
+ "parse-display-derive",
+ "regex",
+ "regex-syntax",
]
[[package]]
-name = "pasta_curves"
-version = "0.5.1"
+name = "parse-display-derive"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095"
+checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281"
dependencies = [
- "blake2b_simd",
- "ff 0.13.1",
- "group 0.13.0",
- "lazy_static",
- "rand 0.8.6",
- "static_assertions",
- "subtle",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "regex-syntax",
+ "structmeta",
+ "syn 2.0.117",
]
-[[package]]
-name = "paste"
-version = "1.0.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
-
-[[package]]
-name = "pathdiff"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
-
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@@ -3661,52 +2203,46 @@ dependencies = [
[[package]]
name = "percent-encoding"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
-
-[[package]]
-name = "pest"
-version = "2.7.15"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
-dependencies = [
- "memchr",
- "thiserror 2.0.12",
- "ucd-trie",
-]
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project"
-version = "1.1.10"
+version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
-version = "1.1.10"
+version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
]
[[package]]
name = "pin-project-lite"
-version = "0.2.16"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
-name = "pin-utils"
-version = "0.1.0"
+name = "pkcs1"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
[[package]]
name = "pkcs8"
@@ -3720,136 +2256,136 @@ dependencies = [
[[package]]
name = "pkg-config"
-version = "0.3.32"
+version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
-name = "portable-atomic"
-version = "1.11.0"
+name = "plain"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
-name = "powerfmt"
-version = "0.2.0"
+name = "plonky2"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+checksum = "b512f56329cfde01b7b5c49f092145ae4fbdbb9cd8742f57879315ed7a893d65"
+dependencies = [
+ "ahash",
+ "anyhow",
+ "getrandom 0.2.17",
+ "hashbrown 0.14.5",
+ "itertools 0.11.0",
+ "keccak-hash",
+ "log",
+ "num",
+ "plonky2_field",
+ "plonky2_maybe_rayon",
+ "plonky2_util",
+ "rand 0.8.6",
+ "rand_chacha 0.3.1",
+ "serde",
+ "static_assertions",
+ "unroll",
+ "web-time",
+]
[[package]]
-name = "ppv-lite86"
-version = "0.2.21"
+name = "plonky2_field"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+checksum = "b3ddfe8817d0c5c2d4557979c51c5253dab1b555ecc19833e5b7ea7dd86f39b8"
dependencies = [
- "zerocopy 0.8.23",
+ "anyhow",
+ "itertools 0.11.0",
+ "num",
+ "plonky2_util",
+ "rand 0.8.6",
+ "serde",
+ "static_assertions",
+ "unroll",
]
[[package]]
-name = "prettyplease"
-version = "0.2.30"
+name = "plonky2_maybe_rayon"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a"
+checksum = "9e1e554181dc95243b8d9948ae7bae5759c7fb2502fed28f671f95ef38079406"
dependencies = [
- "proc-macro2",
- "syn 2.0.100",
+ "rayon",
]
[[package]]
-name = "primeorder"
-version = "0.13.6"
+name = "plonky2_util"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
-dependencies = [
- "elliptic-curve",
-]
+checksum = "c32c137808ca984ab2458b612b7eb0462d853ee041a3136e83d54b96074c7610"
[[package]]
-name = "primitive-types"
-version = "0.12.2"
+name = "portable-atomic"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2"
-dependencies = [
- "fixed-hash",
- "impl-codec",
- "uint",
-]
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
-name = "proc-macro-crate"
-version = "1.3.1"
+name = "potential_utf"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [
- "once_cell",
- "toml_edit 0.19.15",
+ "zerovec",
]
[[package]]
-name = "proc-macro-crate"
-version = "3.3.0"
+name = "powerfmt"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
-dependencies = [
- "toml_edit 0.22.24",
-]
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
-name = "proc-macro-error-attr2"
-version = "2.0.0"
+name = "ppv-lite86"
+version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
- "proc-macro2",
- "quote",
+ "zerocopy",
]
[[package]]
-name = "proc-macro-error2"
-version = "2.0.1"
+name = "prettyplease"
+version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
- "proc-macro-error-attr2",
"proc-macro2",
- "quote",
- "syn 2.0.100",
+ "syn 2.0.117",
]
[[package]]
-name = "proc-macro2"
-version = "1.0.94"
+name = "primitive-types"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
+checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373"
dependencies = [
- "unicode-ident",
+ "fixed-hash",
+ "uint",
]
[[package]]
-name = "proptest"
-version = "1.6.0"
+name = "proc-macro2"
+version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
- "bit-set",
- "bit-vec",
- "bitflags 2.9.0",
- "lazy_static",
- "num-traits",
- "rand 0.8.6",
- "rand_chacha 0.3.1",
- "rand_xorshift",
- "regex-syntax",
- "rusty-fork",
- "tempfile",
- "unarray",
+ "unicode-ident",
]
[[package]]
name = "prost"
-version = "0.13.5"
+version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
+checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
dependencies = [
"bytes",
"prost-derive",
@@ -3857,39 +2393,44 @@ dependencies = [
[[package]]
name = "prost-derive"
-version = "0.13.5"
+version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
+checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
dependencies = [
"anyhow",
"itertools 0.14.0",
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
]
[[package]]
-name = "quick-error"
-version = "1.2.3"
+name = "prost-types"
+version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
+dependencies = [
+ "prost",
+]
[[package]]
name = "quinn"
-version = "0.11.6"
+version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
+ "cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"rustls",
- "socket2",
- "thiserror 2.0.12",
+ "socket2 0.6.3",
+ "thiserror 2.0.18",
"tokio",
"tracing",
+ "web-time",
]
[[package]]
@@ -3899,15 +2440,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"bytes",
- "getrandom 0.3.1",
+ "getrandom 0.3.4",
"lru-slab",
"rand 0.9.4",
"ring",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
- "thiserror 2.0.12",
+ "thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
@@ -3915,32 +2456,38 @@ dependencies = [
[[package]]
name = "quinn-udp"
-version = "0.5.10"
+version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
- "socket2",
+ "socket2 0.6.3",
"tracing",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
name = "quote"
-version = "1.0.39"
+version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
-name = "radium"
-version = "0.7.0"
+name = "r-efi"
+version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
@@ -3951,7 +2498,6 @@ dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
- "serde",
]
[[package]]
@@ -3964,6 +2510,17 @@ dependencies = [
"rand_core 0.9.5",
]
+[[package]]
+name = "rand"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.2",
+ "rand_core 0.10.1",
+]
+
[[package]]
name = "rand_chacha"
version = "0.3.1"
@@ -3990,7 +2547,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
- "getrandom 0.2.15",
+ "getrandom 0.2.17",
]
[[package]]
@@ -3999,23 +2556,20 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
- "getrandom 0.3.1",
+ "getrandom 0.3.4",
]
[[package]]
-name = "rand_xorshift"
-version = "0.3.0"
+name = "rand_core"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
-dependencies = [
- "rand_core 0.6.4",
-]
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rayon"
-version = "1.10.0"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
@@ -4023,48 +2577,57 @@ dependencies = [
[[package]]
name = "rayon-core"
-version = "1.12.1"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
-name = "rayon-scan"
-version = "0.1.1"
+name = "redox_syscall"
+version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f87cc11a0140b4b0da0ffc889885760c61b13672d80a908920b2c0df078fa14"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
- "rayon",
+ "bitflags 2.11.1",
]
[[package]]
name = "redox_syscall"
-version = "0.5.10"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
+checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b"
dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.11.1",
]
[[package]]
-name = "redox_users"
-version = "0.4.6"
+name = "ref-cast"
+version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
dependencies = [
- "getrandom 0.2.15",
- "libredox",
- "thiserror 1.0.69",
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
name = "regex"
-version = "1.11.1"
+version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
@@ -4074,9 +2637,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.9"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
@@ -4085,9 +2648,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.8.5"
+version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "reqwest"
@@ -4100,7 +2663,7 @@ dependencies = [
"encoding_rs",
"futures-core",
"futures-util",
- "h2 0.3.26",
+ "h2 0.3.27",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
@@ -4113,7 +2676,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
- "rustls-pemfile 1.0.4",
+ "rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
@@ -4132,30 +2695,25 @@ dependencies = [
[[package]]
name = "reqwest"
-version = "0.12.12"
+version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
- "futures-util",
- "http 1.2.0",
+ "http 1.4.0",
"http-body 1.0.1",
"http-body-util",
- "hyper 1.6.0",
+ "hyper 1.9.0",
"hyper-rustls",
"hyper-util",
- "ipnet",
"js-sys",
"log",
- "mime",
- "once_cell",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
- "rustls-pemfile 2.2.0",
"rustls-pki-types",
"serde",
"serde_json",
@@ -4163,170 +2721,74 @@ dependencies = [
"sync_wrapper 1.0.2",
"tokio",
"tokio-rustls",
- "tokio-util",
- "tower 0.5.2",
+ "tower",
+ "tower-http 0.6.11",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
- "wasm-streams",
"web-sys",
- "webpki-roots",
- "windows-registry",
-]
-
-[[package]]
-name = "reqwest-middleware"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "562ceb5a604d3f7c885a792d42c199fd8af239d0a51b2fa6a78aafa092452b04"
-dependencies = [
- "anyhow",
- "async-trait",
- "http 1.2.0",
- "reqwest 0.12.12",
- "serde",
- "thiserror 1.0.69",
- "tower-service",
-]
-
-[[package]]
-name = "rfc6979"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
-dependencies = [
- "hmac",
- "subtle",
+ "webpki-roots 1.0.7",
]
[[package]]
name = "ring"
-version = "0.17.13"
+version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
- "getrandom 0.2.15",
+ "getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
-name = "rlp"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec"
-dependencies = [
- "bytes",
- "rustc-hex",
-]
-
-[[package]]
-name = "rrs-succinct"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3372685893a9f67d18e98e792d690017287fd17379a83d798d958e517d380fa9"
-dependencies = [
- "downcast-rs",
- "num_enum",
- "paste",
-]
-
-[[package]]
-name = "ruint"
-version = "1.13.1"
+name = "rsa"
+version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "825df406ec217a8116bd7b06897c6cc8f65ffefc15d030ae2c9540acc9ed50b6"
+checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
- "alloy-rlp",
- "ark-ff 0.3.0",
- "ark-ff 0.4.2",
- "bytes",
- "fastrlp 0.3.1",
- "fastrlp 0.4.0",
- "num-bigint 0.4.6",
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
"num-integer",
"num-traits",
- "parity-scale-codec",
- "primitive-types",
- "proptest",
- "rand 0.8.6",
- "rlp",
- "ruint-macro",
- "serde",
- "valuable",
+ "pkcs1",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "signature",
+ "spki",
+ "subtle",
"zeroize",
]
-[[package]]
-name = "ruint-macro"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18"
-
-[[package]]
-name = "rustc-demangle"
-version = "0.1.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
-
[[package]]
name = "rustc-hash"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
-
-[[package]]
-name = "rustc-hash"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
-
-[[package]]
-name = "rustc-hex"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6"
-
-[[package]]
-name = "rustc_version"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
-dependencies = [
- "semver 0.11.0",
-]
-
-[[package]]
-name = "rustc_version"
-version = "0.4.1"
+version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
-dependencies = [
- "semver 1.0.26",
-]
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustix"
-version = "1.0.1"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.11.1",
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
name = "rustls"
-version = "0.23.28"
+version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
+checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
"log",
"once_cell",
@@ -4339,14 +2801,14 @@ dependencies = [
[[package]]
name = "rustls-native-certs"
-version = "0.8.1"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
+checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
- "security-framework 3.2.0",
+ "security-framework",
]
[[package]]
@@ -4358,15 +2820,6 @@ dependencies = [
"base64 0.21.7",
]
-[[package]]
-name = "rustls-pemfile"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
-dependencies = [
- "rustls-pki-types",
-]
-
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
@@ -4390,68 +2843,56 @@ dependencies = [
[[package]]
name = "rustversion"
-version = "1.0.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
-
-[[package]]
-name = "rusty-fork"
-version = "0.3.0"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
-dependencies = [
- "fnv",
- "quick-error",
- "tempfile",
- "wait-timeout",
-]
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
-version = "1.0.20"
+version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
-name = "scale-info"
-version = "2.11.6"
+name = "same-file"
+version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
- "cfg-if",
- "derive_more 1.0.0",
- "parity-scale-codec",
- "scale-info-derive",
+ "winapi-util",
]
[[package]]
-name = "scale-info-derive"
-version = "2.11.6"
+name = "schannel"
+version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
- "proc-macro-crate 3.3.0",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "scc"
-version = "2.3.3"
+name = "schemars"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea091f6cac2595aa38993f04f4ee692ed43757035c36e67c180b6828356385b1"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
- "sdd",
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
]
[[package]]
-name = "schannel"
-version = "0.1.27"
+name = "schemars"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
dependencies = [
- "windows-sys 0.59.0",
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
]
[[package]]
@@ -4460,33 +2901,13 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-[[package]]
-name = "sdd"
-version = "3.0.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21"
-
-[[package]]
-name = "sec1"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
-dependencies = [
- "base16ct",
- "der",
- "generic-array 0.14.7",
- "pkcs8",
- "subtle",
- "zeroize",
-]
-
[[package]]
name = "secp256k1"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
dependencies = [
- "bitcoin_hashes 0.14.0",
+ "bitcoin_hashes 0.14.1",
"rand 0.8.6",
"secp256k1-sys",
"serde",
@@ -4503,25 +2924,12 @@ dependencies = [
[[package]]
name = "security-framework"
-version = "2.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
-dependencies = [
- "bitflags 2.9.0",
- "core-foundation 0.9.4",
- "core-foundation-sys",
- "libc",
- "security-framework-sys",
-]
-
-[[package]]
-name = "security-framework"
-version = "3.2.0"
+version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
- "bitflags 2.9.0",
- "core-foundation 0.10.0",
+ "bitflags 2.11.1",
+ "core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -4529,9 +2937,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
-version = "2.14.0"
+version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
@@ -4539,191 +2947,148 @@ dependencies = [
[[package]]
name = "semver"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
-dependencies = [
- "semver-parser",
-]
-
-[[package]]
-name = "semver"
-version = "1.0.26"
+version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
-dependencies = [
- "serde",
-]
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
-name = "semver-parser"
-version = "0.10.3"
+name = "serde"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
- "pest",
+ "serde_core",
+ "serde_derive",
]
[[package]]
-name = "serde"
-version = "1.0.219"
+name = "serde_core"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.219"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
]
[[package]]
name = "serde_json"
-version = "1.0.140"
+version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
- "ryu",
"serde",
+ "serde_core",
+ "zmij",
]
[[package]]
name = "serde_path_to_error"
-version = "0.1.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
-dependencies = [
- "itoa",
- "serde",
-]
-
-[[package]]
-name = "serde_spanned"
-version = "0.6.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
+version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
- "form_urlencoded",
"itoa",
- "ryu",
"serde",
+ "serde_core",
]
[[package]]
-name = "serial_test"
-version = "3.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
-dependencies = [
- "futures",
- "log",
- "once_cell",
- "parking_lot",
- "scc",
- "serial_test_derive",
-]
-
-[[package]]
-name = "serial_test_derive"
-version = "3.2.0"
+name = "serde_repr"
+version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
]
[[package]]
-name = "server"
-version = "1.1.0"
-dependencies = [
- "anyhow",
- "axum",
- "bincode",
- "bitcoin",
- "bitcoin_hashes 0.16.0",
- "esplora-client",
- "hex",
- "http-body-util",
- "lazy_static",
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
"serde",
- "serde_json",
- "sha2 0.10.8",
- "shared",
- "tokio",
- "tower 0.5.2",
- "tower-http",
- "zkcoins-program",
- "zkcoins-prover",
]
[[package]]
-name = "sha2"
-version = "0.10.8"
+name = "serde_urlencoded"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest 0.10.7",
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
]
[[package]]
-name = "sha2"
-version = "0.11.0-pre.3"
-source = "git+https://github.com/sp1-patches/RustCrypto-hashes#0b79171da599c1bd1b9d4bd45f537f217a2375df"
+name = "serde_with"
+version = "3.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2"
dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest 0.11.0-pre.8",
+ "base64 0.22.1",
+ "bs58",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.14.0",
+ "schemars 0.9.0",
+ "schemars 1.2.1",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
]
[[package]]
-name = "sha3"
-version = "0.10.8"
+name = "serde_with_macros"
+version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac"
dependencies = [
- "digest 0.10.7",
- "keccak",
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "sha3-asm"
-version = "0.1.4"
+name = "sha1"
+version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
- "cc",
"cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
]
[[package]]
-name = "sharded-slab"
-version = "0.1.7"
+name = "sha2"
+version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
- "lazy_static",
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
]
[[package]]
@@ -4735,8 +3100,8 @@ dependencies = [
"hex",
"lazy_static",
"serde",
- "sha2 0.10.8",
- "zkcoins-program",
+ "sha2",
+ "zkcoins-program-plonky2",
]
[[package]]
@@ -4747,10 +3112,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
-version = "1.4.2"
+version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
+ "errno",
"libc",
]
@@ -4760,524 +3126,255 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
- "digest 0.10.7",
+ "digest",
"rand_core 0.6.4",
]
-[[package]]
-name = "size"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fed904c7fb2856d868b92464fc8fa597fce366edea1a9cbfaa8cb5fe080bd6d"
-
[[package]]
name = "slab"
-version = "0.4.9"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
-dependencies = [
- "autocfg",
-]
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
-version = "1.14.0"
+version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
-name = "snowbridge-amcl"
-version = "1.0.2"
+name = "socket2"
+version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "460a9ed63cdf03c1b9847e8a12a5f5ba19c4efd5869e4a737e05be25d7c427e5"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
- "parity-scale-codec",
- "scale-info",
+ "libc",
+ "windows-sys 0.52.0",
]
[[package]]
name = "socket2"
-version = "0.5.8"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "sp1-build"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
- "anyhow",
- "cargo_metadata",
- "chrono",
- "clap",
- "dirs",
+ "lock_api",
]
[[package]]
-name = "sp1-core-executor"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
- "bincode",
- "bytemuck",
- "clap",
- "elf",
- "enum-map",
- "eyre",
- "hashbrown 0.14.5",
- "hex",
- "itertools 0.13.0",
- "log",
- "nohash-hasher",
- "num",
- "p3-baby-bear",
- "p3-field",
- "p3-maybe-rayon",
- "p3-util",
- "rand 0.8.6",
- "rrs-succinct",
- "serde",
- "serde_json",
- "sp1-curves",
- "sp1-primitives",
- "sp1-stark",
- "strum",
- "strum_macros",
- "subenum",
- "thiserror 1.0.69",
- "tiny-keccak",
- "tracing",
- "typenum",
- "vec_map",
+ "base64ct",
+ "der",
]
[[package]]
-name = "sp1-core-machine"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
+name = "sqlx"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
dependencies = [
- "bincode",
- "cbindgen",
- "cc",
- "cfg-if",
- "elliptic-curve",
- "generic-array 1.1.0",
- "glob",
- "hashbrown 0.14.5",
- "hex",
- "itertools 0.13.0",
- "k256",
- "log",
- "num",
- "num_cpus",
- "p256",
- "p3-air",
- "p3-baby-bear",
- "p3-challenger",
- "p3-field",
- "p3-keccak-air",
- "p3-matrix",
- "p3-maybe-rayon",
- "p3-poseidon2",
- "p3-symmetric",
- "p3-uni-stark",
- "p3-util",
- "pathdiff",
- "rand 0.8.6",
- "rayon",
- "rayon-scan",
- "serde",
- "serde_json",
- "size",
- "snowbridge-amcl",
- "sp1-core-executor",
- "sp1-curves",
- "sp1-derive",
- "sp1-primitives",
- "sp1-stark",
- "static_assertions",
- "strum",
- "strum_macros",
- "tempfile",
- "thiserror 1.0.69",
- "tracing",
- "tracing-forest",
- "tracing-subscriber",
- "typenum",
- "web-time",
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
]
[[package]]
-name = "sp1-cuda"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
+name = "sqlx-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
- "bincode",
- "ctrlc",
- "prost",
+ "base64 0.22.1",
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashbrown 0.15.5",
+ "hashlink",
+ "indexmap 2.14.0",
+ "log",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rustls",
"serde",
- "sp1-core-machine",
- "sp1-prover",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "thiserror 2.0.18",
"tokio",
+ "tokio-stream",
"tracing",
- "twirp-rs",
-]
-
-[[package]]
-name = "sp1-curves"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
-dependencies = [
- "cfg-if",
- "dashu",
- "elliptic-curve",
- "generic-array 1.1.0",
- "itertools 0.13.0",
- "k256",
- "num",
- "p256",
- "p3-field",
- "serde",
- "snowbridge-amcl",
- "sp1-primitives",
- "sp1-stark",
- "typenum",
+ "url",
+ "webpki-roots 0.26.11",
]
[[package]]
-name = "sp1-derive"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
+name = "sqlx-macros"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
dependencies = [
+ "proc-macro2",
"quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "sp1-lib"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
-dependencies = [
- "bincode",
- "serde",
- "sp1-primitives",
-]
-
-[[package]]
-name = "sp1-primitives"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
-dependencies = [
- "bincode",
- "hex",
- "lazy_static",
- "num-bigint 0.4.6",
- "p3-baby-bear",
- "p3-field",
- "p3-poseidon2",
- "p3-symmetric",
- "serde",
- "sha2 0.10.8",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 2.0.117",
]
[[package]]
-name = "sp1-prover"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
+name = "sqlx-macros-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
dependencies = [
- "anyhow",
- "bincode",
- "clap",
- "dirs",
- "downloader",
- "eyre",
+ "dotenvy",
+ "either",
+ "heck",
"hex",
- "itertools 0.13.0",
- "lru",
- "num-bigint 0.4.6",
- "p3-baby-bear",
- "p3-bn254-fr",
- "p3-challenger",
- "p3-commit",
- "p3-field",
- "p3-matrix",
- "p3-symmetric",
- "p3-util",
- "rayon",
- "serde",
- "serde_json",
- "serial_test",
- "sha2 0.10.8",
- "sp1-core-executor",
- "sp1-core-machine",
- "sp1-primitives",
- "sp1-recursion-circuit",
- "sp1-recursion-compiler",
- "sp1-recursion-core",
- "sp1-recursion-gnark-ffi",
- "sp1-stark",
- "thiserror 1.0.69",
- "tracing",
- "tracing-appender",
- "tracing-subscriber",
-]
-
-[[package]]
-name = "sp1-recursion-circuit"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
-dependencies = [
- "hashbrown 0.14.5",
- "itertools 0.13.0",
- "num-traits",
- "p3-air",
- "p3-baby-bear",
- "p3-bn254-fr",
- "p3-challenger",
- "p3-commit",
- "p3-dft",
- "p3-field",
- "p3-fri",
- "p3-matrix",
- "p3-symmetric",
- "p3-uni-stark",
- "p3-util",
- "rand 0.8.6",
- "rayon",
- "serde",
- "sp1-core-executor",
- "sp1-core-machine",
- "sp1-derive",
- "sp1-primitives",
- "sp1-recursion-compiler",
- "sp1-recursion-core",
- "sp1-recursion-gnark-ffi",
- "sp1-stark",
- "tracing",
-]
-
-[[package]]
-name = "sp1-recursion-compiler"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
-dependencies = [
- "backtrace",
- "itertools 0.13.0",
- "p3-baby-bear",
- "p3-bn254-fr",
- "p3-field",
- "p3-symmetric",
- "serde",
- "sp1-core-machine",
- "sp1-primitives",
- "sp1-recursion-core",
- "sp1-recursion-derive",
- "sp1-stark",
- "tracing",
- "vec_map",
-]
-
-[[package]]
-name = "sp1-recursion-core"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
-dependencies = [
- "backtrace",
- "cbindgen",
- "cc",
- "cfg-if",
- "ff 0.13.1",
- "glob",
- "hashbrown 0.14.5",
- "itertools 0.13.0",
- "num_cpus",
- "p3-air",
- "p3-baby-bear",
- "p3-bn254-fr",
- "p3-challenger",
- "p3-commit",
- "p3-dft",
- "p3-field",
- "p3-fri",
- "p3-matrix",
- "p3-maybe-rayon",
- "p3-merkle-tree",
- "p3-poseidon2",
- "p3-symmetric",
- "p3-util",
- "pathdiff",
- "rand 0.8.6",
- "serde",
- "sp1-core-machine",
- "sp1-derive",
- "sp1-primitives",
- "sp1-stark",
- "static_assertions",
- "thiserror 1.0.69",
- "tracing",
- "vec_map",
- "zkhash",
-]
-
-[[package]]
-name = "sp1-recursion-derive"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
-dependencies = [
+ "once_cell",
+ "proc-macro2",
"quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "sp1-recursion-gnark-ffi"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
-dependencies = [
- "anyhow",
- "bincode",
- "bindgen",
- "cc",
- "cfg-if",
- "hex",
- "log",
- "num-bigint 0.4.6",
- "p3-baby-bear",
- "p3-field",
- "p3-symmetric",
"serde",
"serde_json",
- "sha2 0.10.8",
- "sp1-core-machine",
- "sp1-recursion-compiler",
- "sp1-stark",
- "tempfile",
+ "sha2",
+ "sqlx-core",
+ "sqlx-postgres",
+ "syn 2.0.117",
+ "tokio",
+ "url",
]
[[package]]
-name = "sp1-sdk"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
+name = "sqlx-mysql"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
- "alloy-primitives",
- "alloy-signer",
- "alloy-signer-local",
- "alloy-sol-types",
- "anyhow",
- "async-trait",
- "backoff",
- "bincode",
- "cfg-if",
- "dirs",
- "futures",
- "hashbrown 0.14.5",
+ "atoi",
+ "base64 0.22.1",
+ "bitflags 2.11.1",
+ "byteorder",
+ "bytes",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
"hex",
- "indicatif",
- "itertools 0.13.0",
+ "hkdf",
+ "hmac",
+ "itoa",
"log",
- "p3-baby-bear",
- "p3-field",
- "p3-fri",
- "prost",
- "reqwest 0.12.12",
- "reqwest-middleware",
- "serde",
- "serde_json",
- "sp1-build",
- "sp1-core-executor",
- "sp1-core-machine",
- "sp1-cuda",
- "sp1-primitives",
- "sp1-prover",
- "sp1-stark",
- "strum",
- "strum_macros",
- "tempfile",
- "thiserror 1.0.69",
- "tokio",
- "tonic",
- "tracing",
- "twirp-rs",
-]
-
-[[package]]
-name = "sp1-stark"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
-dependencies = [
- "arrayref",
- "hashbrown 0.14.5",
- "itertools 0.13.0",
- "num-bigint 0.4.6",
- "num-traits",
- "p3-air",
- "p3-baby-bear",
- "p3-challenger",
- "p3-commit",
- "p3-dft",
- "p3-field",
- "p3-fri",
- "p3-matrix",
- "p3-maybe-rayon",
- "p3-merkle-tree",
- "p3-poseidon2",
- "p3-symmetric",
- "p3-uni-stark",
- "p3-util",
- "rayon-scan",
- "serde",
- "sp1-derive",
- "sp1-primitives",
- "strum",
- "strum_macros",
- "sysinfo",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand 0.8.6",
+ "rsa",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.18",
"tracing",
+ "whoami",
]
[[package]]
-name = "sp1-zkvm"
-version = "4.1.2"
-source = "git+https://github.com/succinctlabs/sp1?tag=v4.1.2#24e5bd8bd7d8c96f7d6f33582c4d0abc10aab863"
+name = "sqlx-postgres"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
- "cfg-if",
- "getrandom 0.2.15",
- "lazy_static",
- "libm",
- "p3-baby-bear",
- "p3-field",
+ "atoi",
+ "base64 0.22.1",
+ "bitflags 2.11.1",
+ "byteorder",
+ "crc",
+ "dotenvy",
+ "etcetera 0.8.0",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
"rand 0.8.6",
- "sha2 0.10.8",
- "sp1-lib",
- "sp1-primitives",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.18",
+ "tracing",
+ "whoami",
]
[[package]]
-name = "spin"
-version = "0.9.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
-
-[[package]]
-name = "spki"
-version = "0.7.3"
+name = "sqlx-sqlite"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [
- "base64ct",
- "der",
+ "atoi",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde_urlencoded",
+ "sqlx-core",
+ "thiserror 2.0.18",
+ "tracing",
+ "url",
]
[[package]]
name = "stable_deref_trait"
-version = "1.2.0"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions"
@@ -5286,43 +3383,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
-name = "strsim"
-version = "0.11.1"
+name = "stringprep"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
[[package]]
-name = "strum"
-version = "0.26.3"
+name = "strsim"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
-dependencies = [
- "strum_macros",
-]
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
-name = "strum_macros"
-version = "0.26.4"
+name = "structmeta"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329"
dependencies = [
- "heck 0.5.0",
"proc-macro2",
"quote",
- "rustversion",
- "syn 2.0.100",
+ "structmeta-derive",
+ "syn 2.0.117",
]
[[package]]
-name = "subenum"
-version = "1.1.2"
+name = "structmeta-derive"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f5d5dfb8556dd04017db5e318bbeac8ab2b0c67b76bf197bfb79e9b29f18ecf"
+checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
- "heck 0.4.1",
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.117",
]
[[package]]
@@ -5344,27 +3441,15 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.100"
+version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
-[[package]]
-name = "syn-solidity"
-version = "0.8.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac9f9798a84bca5cd4d1760db691075fda8f2c3a5d9647e8bfd29eb9b3fabb87"
-dependencies = [
- "paste",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
[[package]]
name = "sync_wrapper"
version = "0.1.2"
@@ -5382,28 +3467,13 @@ dependencies = [
[[package]]
name = "synstructure"
-version = "0.13.1"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "sysinfo"
-version = "0.30.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
-dependencies = [
- "cfg-if",
- "core-foundation-sys",
- "libc",
- "ntapi",
- "once_cell",
- "rayon",
- "windows",
+ "syn 2.0.117",
]
[[package]]
@@ -5428,23 +3498,75 @@ dependencies = [
]
[[package]]
-name = "tap"
-version = "1.0.1"
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tempfile"
-version = "3.18.0"
+version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
- "cfg-if",
"fastrand",
- "getrandom 0.3.1",
+ "getrandom 0.4.2",
"once_cell",
"rustix",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "testcontainers"
+version = "0.27.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e"
+dependencies = [
+ "astral-tokio-tar",
+ "async-trait",
+ "bollard",
+ "bytes",
+ "docker_credential",
+ "either",
+ "etcetera 0.11.0",
+ "ferroid",
+ "futures",
+ "http 1.4.0",
+ "itertools 0.14.0",
+ "log",
+ "memchr",
+ "parse-display",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "url",
+]
+
+[[package]]
+name = "testcontainers-modules"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5985fde5befe4ffa77a052e035e16c2da86e8bae301baa9f9904ad3c494d357"
+dependencies = [
+ "testcontainers",
]
[[package]]
@@ -5458,11 +3580,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.12"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
- "thiserror-impl 2.0.12",
+ "thiserror-impl 2.0.18",
]
[[package]]
@@ -5473,65 +3595,46 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
]
[[package]]
name = "thiserror-impl"
-version = "2.0.12"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "thread_local"
-version = "1.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
-dependencies = [
- "cfg-if",
- "once_cell",
-]
-
-[[package]]
-name = "threadpool"
-version = "1.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
-dependencies = [
- "num_cpus",
+ "syn 2.0.117",
]
[[package]]
name = "time"
-version = "0.3.41"
+version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
- "serde",
+ "serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
-version = "0.1.4"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
-version = "0.2.22"
+version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
@@ -5548,9 +3651,9 @@ dependencies = [
[[package]]
name = "tinystr"
-version = "0.7.6"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
dependencies = [
"displaydoc",
"zerovec",
@@ -5558,9 +3661,9 @@ dependencies = [
[[package]]
name = "tinyvec"
-version = "1.9.0"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
@@ -5573,31 +3676,29 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.44.2"
+version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [
- "backtrace",
"bytes",
"libc",
"mio",
- "parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2",
+ "socket2 0.6.3",
"tokio-macros",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
-version = "2.5.0"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
]
[[package]]
@@ -5612,9 +3713,9 @@ dependencies = [
[[package]]
name = "tokio-rustls"
-version = "0.26.2"
+version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
@@ -5634,20 +3735,36 @@ dependencies = [
[[package]]
name = "tokio-stream"
-version = "0.1.17"
+version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
+[[package]]
+name = "tokio-tungstenite"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
+dependencies = [
+ "futures-util",
+ "log",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tungstenite",
+ "webpki-roots 0.26.11",
+]
+
[[package]]
name = "tokio-util"
-version = "0.7.13"
+version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
@@ -5658,113 +3775,92 @@ dependencies = [
[[package]]
name = "toml"
-version = "0.8.20"
+version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
- "toml_edit 0.22.24",
+ "toml_edit",
]
[[package]]
name = "toml_datetime"
-version = "0.6.8"
+version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
-version = "0.19.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
-dependencies = [
- "indexmap 2.7.1",
- "toml_datetime",
- "winnow 0.5.40",
-]
-
-[[package]]
-name = "toml_edit"
-version = "0.22.24"
+version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
- "indexmap 2.7.1",
+ "indexmap 2.14.0",
"serde",
"serde_spanned",
"toml_datetime",
- "winnow 0.7.3",
+ "winnow",
]
[[package]]
name = "tonic"
-version = "0.12.3"
+version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
+checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
dependencies = [
- "async-stream",
"async-trait",
- "axum",
+ "axum 0.8.9",
"base64 0.22.1",
"bytes",
- "h2 0.4.8",
- "http 1.2.0",
+ "h2 0.4.14",
+ "http 1.4.0",
"http-body 1.0.1",
"http-body-util",
- "hyper 1.6.0",
+ "hyper 1.9.0",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
- "prost",
- "rustls-native-certs",
- "rustls-pemfile 2.2.0",
- "socket2",
+ "socket2 0.6.3",
+ "sync_wrapper 1.0.2",
"tokio",
- "tokio-rustls",
"tokio-stream",
- "tower 0.4.13",
+ "tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
-name = "tower"
-version = "0.4.13"
+name = "tonic-prost"
+version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
dependencies = [
- "futures-core",
- "futures-util",
- "indexmap 1.9.3",
- "pin-project",
- "pin-project-lite",
- "rand 0.8.6",
- "slab",
- "tokio",
- "tokio-util",
- "tower-layer",
- "tower-service",
- "tracing",
+ "bytes",
+ "prost",
+ "tonic",
]
[[package]]
name = "tower"
-version = "0.5.2"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
+ "indexmap 2.14.0",
"pin-project-lite",
+ "slab",
"sync_wrapper 1.0.2",
"tokio",
+ "tokio-util",
"tower-layer",
"tower-service",
"tracing",
@@ -5776,10 +3872,10 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.11.1",
"bytes",
"futures-util",
- "http 1.2.0",
+ "http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"http-range-header",
@@ -5795,6 +3891,24 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags 2.11.1",
+ "bytes",
+ "futures-util",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
[[package]]
name = "tower-layer"
version = "0.3.3"
@@ -5809,9 +3923,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
-version = "0.1.41"
+version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
@@ -5819,203 +3933,182 @@ dependencies = [
"tracing-core",
]
-[[package]]
-name = "tracing-appender"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
-dependencies = [
- "crossbeam-channel",
- "thiserror 1.0.69",
- "time",
- "tracing-subscriber",
-]
-
[[package]]
name = "tracing-attributes"
-version = "0.1.28"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
]
[[package]]
name = "tracing-core"
-version = "0.1.33"
+version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
- "valuable",
]
[[package]]
-name = "tracing-forest"
-version = "0.1.6"
+name = "try-lock"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee40835db14ddd1e3ba414292272eddde9dad04d3d4b65509656414d1c42592f"
-dependencies = [
- "ansi_term",
- "smallvec",
- "thiserror 1.0.69",
- "tracing",
- "tracing-subscriber",
-]
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
-name = "tracing-log"
-version = "0.2.0"
+name = "tungstenite"
+version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http 1.4.0",
+ "httparse",
"log",
- "once_cell",
- "tracing-core",
+ "rand 0.8.6",
+ "rustls",
+ "rustls-pki-types",
+ "sha1",
+ "thiserror 1.0.69",
+ "utf-8",
]
[[package]]
-name = "tracing-subscriber"
-version = "0.3.20"
+name = "typenum"
+version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
-dependencies = [
- "matchers",
- "nu-ansi-term",
- "once_cell",
- "regex-automata",
- "sharded-slab",
- "smallvec",
- "thread_local",
- "tracing",
- "tracing-core",
- "tracing-log",
-]
+checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
-name = "try-lock"
-version = "0.2.5"
+name = "uint"
+version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52"
+dependencies = [
+ "byteorder",
+ "crunchy",
+ "hex",
+ "static_assertions",
+]
[[package]]
-name = "twirp-rs"
-version = "0.13.0-succinct"
+name = "unicase"
+version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27dfcc06b8d9262bc2d4b8d1847c56af9971a52dd8a0076876de9db763227d0d"
-dependencies = [
- "async-trait",
- "axum",
- "futures",
- "http 1.2.0",
- "http-body-util",
- "hyper 1.6.0",
- "prost",
- "reqwest 0.12.12",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
- "tokio",
- "tower 0.5.2",
- "url",
-]
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
-name = "typenum"
-version = "1.18.0"
+name = "unicode-bidi"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
-name = "ucd-trie"
-version = "0.1.7"
+name = "unicode-ident"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
-name = "uint"
-version = "0.9.5"
+name = "unicode-normalization"
+version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
- "byteorder",
- "crunchy",
- "hex",
- "static_assertions",
+ "tinyvec",
]
[[package]]
-name = "unarray"
+name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
-name = "unicase"
-version = "2.8.1"
+name = "unicode-xid"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
-name = "unicode-ident"
-version = "1.0.18"
+name = "unroll"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+checksum = "5ad948c1cb799b1a70f836077721a92a35ac177d4daddf4c20a633786d4cf618"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
[[package]]
-name = "unicode-width"
-version = "0.2.0"
+name = "untrusted"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
-name = "unicode-xid"
-version = "0.2.6"
+name = "ureq"
+version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
+dependencies = [
+ "base64 0.22.1",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "ureq-proto",
+ "utf8-zero",
+]
[[package]]
-name = "untrusted"
-version = "0.9.0"
+name = "ureq-proto"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
+dependencies = [
+ "base64 0.22.1",
+ "http 1.4.0",
+ "httparse",
+ "log",
+]
[[package]]
name = "url"
-version = "2.5.4"
+version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
+ "serde",
+ "serde_derive",
]
[[package]]
-name = "utf16_iter"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
-
-[[package]]
-name = "utf8_iter"
-version = "1.0.4"
+name = "utf-8"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
-name = "utf8parse"
-version = "0.2.2"
+name = "utf8-zero"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]]
-name = "valuable"
-version = "0.1.1"
+name = "utf8_iter"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "vcpkg"
@@ -6024,13 +4117,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
-name = "vec_map"
-version = "0.8.2"
+name = "version-compare"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
-dependencies = [
- "serde",
-]
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
[[package]]
name = "version_check"
@@ -6039,12 +4129,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
-name = "wait-timeout"
-version = "0.2.1"
+name = "walkdir"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
- "libc",
+ "same-file",
+ "winapi-util",
]
[[package]]
@@ -6058,19 +4149,34 @@ dependencies = [
[[package]]
name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
+version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
-name = "wasi"
-version = "0.13.3+wasi-0.2.2"
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
- "wit-bindgen-rt",
+ "wit-bindgen 0.51.0",
]
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
[[package]]
name = "wasm-bindgen"
version = "0.2.121"
@@ -6086,14 +4192,12 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.45"
+version = "0.4.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
+checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8"
dependencies = [
- "cfg-if",
"js-sys",
"wasm-bindgen",
- "web-sys",
]
[[package]]
@@ -6115,7 +4219,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
"wasm-bindgen-shared",
]
@@ -6129,23 +4233,44 @@ dependencies = [
]
[[package]]
-name = "wasm-streams"
-version = "0.4.2"
+name = "wasm-encoder"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
- "futures-util",
- "js-sys",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap 2.14.0",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.11.1",
+ "hashbrown 0.15.5",
+ "indexmap 2.14.0",
+ "semver",
]
[[package]]
name = "web-sys"
-version = "0.3.72"
+version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
+checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -6163,13 +4288,32 @@ dependencies = [
[[package]]
name = "webpki-roots"
-version = "0.26.8"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.7",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9"
+checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
dependencies = [
"rustls-pki-types",
]
+[[package]]
+name = "whoami"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
+dependencies = [
+ "libredox",
+ "wasite",
+]
+
[[package]]
name = "winapi"
version = "0.3.9"
@@ -6186,6 +4330,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@@ -6193,58 +4346,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
-name = "windows"
-version = "0.52.0"
+name = "windows-core"
+version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
- "windows-core",
- "windows-targets 0.52.6",
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
]
[[package]]
-name = "windows-core"
-version = "0.52.0"
+name = "windows-implement"
+version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
- "windows-targets 0.52.6",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "windows-link"
-version = "0.1.0"
+name = "windows-interface"
+version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
[[package]]
-name = "windows-registry"
-version = "0.2.0"
+name = "windows-link"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
-dependencies = [
- "windows-result",
- "windows-strings",
- "windows-targets 0.52.6",
-]
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
-version = "0.2.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
- "windows-targets 0.52.6",
+ "windows-link",
]
[[package]]
name = "windows-strings"
-version = "0.1.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
- "windows-result",
- "windows-targets 0.52.6",
+ "windows-link",
]
[[package]]
@@ -6267,11 +4424,11 @@ dependencies = [
[[package]]
name = "windows-sys"
-version = "0.59.0"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "windows-targets 0.52.6",
+ "windows-link",
]
[[package]]
@@ -6397,18 +4554,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
-version = "0.5.40"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "winnow"
-version = "0.7.3"
+version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
@@ -6424,145 +4572,234 @@ dependencies = [
]
[[package]]
-name = "wit-bindgen-rt"
-version = "0.33.0"
+name = "wiremock"
+version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
+checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
dependencies = [
- "bitflags 2.9.0",
+ "assert-json-diff",
+ "base64 0.22.1",
+ "deadpool",
+ "futures",
+ "http 1.4.0",
+ "http-body-util",
+ "hyper 1.9.0",
+ "hyper-util",
+ "log",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_json",
+ "tokio",
+ "url",
]
[[package]]
-name = "write16"
-version = "1.0.0"
+name = "wit-bindgen"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
[[package]]
-name = "writeable"
-version = "0.5.5"
+name = "wit-bindgen"
+version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
-name = "wyz"
-version = "0.5.1"
+name = "wit-bindgen-core"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
- "tap",
+ "anyhow",
+ "heck",
+ "wit-parser",
]
[[package]]
-name = "yoke"
-version = "0.7.5"
+name = "wit-bindgen-rust"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
- "serde",
- "stable_deref_trait",
- "yoke-derive",
- "zerofrom",
+ "anyhow",
+ "heck",
+ "indexmap 2.14.0",
+ "prettyplease",
+ "syn 2.0.117",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
]
[[package]]
-name = "yoke-derive"
-version = "0.7.5"
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
+ "anyhow",
+ "prettyplease",
"proc-macro2",
"quote",
- "syn 2.0.100",
- "synstructure",
+ "syn 2.0.117",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
]
[[package]]
-name = "zerocopy"
-version = "0.7.35"
+name = "wit-component"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
- "zerocopy-derive 0.7.35",
+ "anyhow",
+ "bitflags 2.11.1",
+ "indexmap 2.14.0",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
]
[[package]]
-name = "zerocopy"
-version = "0.8.23"
+name = "wit-parser"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
- "zerocopy-derive 0.8.23",
+ "anyhow",
+ "id-arena",
+ "indexmap 2.14.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
]
[[package]]
-name = "zerocopy-derive"
-version = "0.7.35"
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.23"
+version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
]
[[package]]
name = "zerofrom"
-version = "0.1.6"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
"synstructure",
]
[[package]]
name = "zeroize"
-version = "1.8.1"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zeromq-src"
+version = "0.2.6+4.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+checksum = "fc120b771270365d5ed0dfb4baf1005f2243ae1ae83703265cb3504070f4160b"
dependencies = [
- "zeroize_derive",
+ "cc",
+ "dircpy",
]
[[package]]
-name = "zeroize_derive"
-version = "1.4.2"
+name = "zerotrie"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "displaydoc",
+ "yoke",
+ "zerofrom",
]
[[package]]
name = "zerovec"
-version = "0.10.4"
+version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [
"yoke",
"zerofrom",
@@ -6571,60 +4808,58 @@ dependencies = [
[[package]]
name = "zerovec-derive"
-version = "0.10.3"
+version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.117",
]
[[package]]
-name = "zkcoins-program"
-version = "0.1.0"
+name = "zkcoins-program-plonky2"
+version = "0.0.1"
dependencies = [
+ "anyhow",
"bincode",
- "derive_builder",
- "lazy_static",
- "rand 0.8.6",
+ "plonky2",
"serde",
- "sha2 0.11.0-pre.3",
- "sp1-zkvm",
]
[[package]]
-name = "zkcoins-prover"
-version = "1.1.0"
+name = "zkcoins-prover-plonky2"
+version = "0.0.1"
dependencies = [
- "sp1-sdk",
- "tracing",
- "zkcoins-program",
+ "anyhow",
+ "plonky2",
+ "zkcoins-program-plonky2",
]
[[package]]
-name = "zkhash"
-version = "0.2.0"
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zmq"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4352d1081da6922701401cdd4cbf29a2723feb4cfabb5771f6fee8e9276da1c7"
+checksum = "dd3091dd571fb84a9b3e5e5c6a807d186c411c812c8618786c3c30e5349234e7"
dependencies = [
- "ark-ff 0.4.2",
- "ark-std 0.4.0",
- "bitvec",
- "blake2",
- "bls12_381",
- "byteorder",
- "cfg-if",
- "group 0.12.1",
- "group 0.13.0",
- "halo2",
- "hex",
- "jubjub",
- "lazy_static",
- "pasta_curves 0.5.1",
- "rand 0.8.6",
- "serde",
- "sha2 0.10.8",
- "sha3",
- "subtle",
+ "bitflags 1.3.2",
+ "libc",
+ "zmq-sys",
+]
+
+[[package]]
+name = "zmq-sys"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e8351dc72494b4d7f5652a681c33634063bbad58046c1689e75270908fdc864"
+dependencies = [
+ "libc",
+ "system-deps",
+ "zeromq-src",
]
diff --git a/Cargo.toml b/Cargo.toml
index 2c8ac0a7..39ee92e8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,9 +1,10 @@
[workspace]
members = [
- "program",
- "script",
- "server",
- "shared"]
+ "program-plonky2",
+ "script-plonky2",
+ "node",
+ "shared",
+]
resolver = "2"
[workspace.dependencies]
@@ -13,8 +14,7 @@ serde = { version = "1.0", features = ["derive"] }
rand = "0.8"
blake3 = "1.6.1"
lazy_static = "1.5.0"
-bitcoin = { version = "0.32.5", features = ["rand", "rand-std"] }
-sp1-sdk = "4.0.0"
+bitcoin = { version = "0.32.5", features = ["rand", "rand-std", "serde"] }
[profile.dev]
opt-level = 3
@@ -22,22 +22,3 @@ opt-level = 3
[workspace.package]
version = "1.1.0"
edition = "2021"
-
-[patch.crates-io]
-sp1-zkvm = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-lib = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-primitives = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-sdk = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-build = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-core-executor = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-curves = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-stark = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-derive = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-core-machine = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-cuda = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-prover = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-recursion-circuit = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-recursion-compiler = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-recursion-core = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-recursion-derive = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
-sp1-recursion-gnark-ffi = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" }
diff --git a/Dockerfile b/Dockerfile
index 07c3d096..f70718f1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,25 +1,69 @@
-FROM rust:1.81-bookworm AS builder
+# Multi-stage Docker build for the zkCoins node post Plonky2 migration.
+#
+# The Plonky2 toolchain pin is `nightly` (see `rust-toolchain` at the
+# repo root). rustup respects that file and installs the right channel
+# automatically when cargo is first invoked — no manual `rustup install`
+# step needed.
+#
+# Build:
+# docker build -t zkcoins/node:latest .
+# docker build -t zkcoins/node:beta .
+#
+# Both DEV (`:beta`) and PRD (`:latest`) ship the MVP-only binary
+# (no Cargo features beyond the always-on mint and username routes).
+# The `FEATURES` build-arg below stays in place as an opt-in escape
+# hatch for self-hosters who want to compile non-MVP routes locally
+# (e.g. `--build-arg FEATURES=address-list,lnurl`).
+# Run:
+# docker run -p 4242:4242 \
+# -e ESPLORA_URL=http://electrs:3000 \
+# -e PUBLISHER_KEY= \
+# -v zkcoins-data:/data \
+# zkcoins/node:latest
+
+FROM rust:bookworm AS builder
WORKDIR /app
+
+# `sqlx::migrate!("./migrations")` is compile-time, so the migrations
+# directory must exist when `cargo build` runs (the COPY below pulls
+# it in). The current `db.rs` uses runtime-checked `sqlx::query` /
+# `sqlx::query_as`, so no `.sqlx/` offline cache is needed; setting
+# `SQLX_OFFLINE=true` is defensive — if a future change introduces a
+# compile-checked `sqlx::query!` macro, the build will surface the
+# missing `.sqlx/` immediately rather than trying (and failing) to
+# reach a live database from the builder.
+ENV SQLX_OFFLINE=true
+
+# Copy just the toolchain file first so rustup can fetch the right
+# channel before the slow source copy. Cuts a few seconds off cold
+# builds; layer-caches well across source-only changes.
+COPY rust-toolchain ./
+RUN rustup show
+
COPY . .
-# Cargo features for non-MVP routes. Empty by default — the PRD image
-# ships only the MVP feature set. The DEV image build passes a comma-
-# separated list (e.g. `address-list,faucet,usernames,lnurl`). Features
-# not listed here are excluded from the binary at compile time, so the
-# disabled code cannot run, crash, or be exploited at runtime.
+# Cargo features for non-MVP routes. Empty by default — both DEV and
+# PRD images ship the MVP-only feature set so the two environments run
+# the identical binary. Self-hosters who want to enable non-MVP routes
+# in a local build can pass a comma-separated list
+# (e.g. `--build-arg FEATURES=address-list,lnurl`). Features not listed
+# here are excluded from the binary at compile time, so the disabled
+# code cannot run, crash, or be exploited at runtime.
ARG FEATURES=
RUN if [ -z "$FEATURES" ]; then \
- cargo build --release -p server; \
+ cargo build --release -p node; \
else \
- cargo build --release -p server --features "$FEATURES"; \
+ cargo build --release -p node --features "$FEATURES"; \
fi
FROM debian:bookworm-slim
-RUN apt-get update && apt-get install -y ca-certificates wget && rm -rf /var/lib/apt/lists/*
-COPY --from=builder /app/target/release/server /usr/local/bin/zkcoins-server
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates wget \
+ && rm -rf /var/lib/apt/lists/*
+COPY --from=builder /app/target/release/node /usr/local/bin/zkcoins-node
ENV RUST_LOG=info
WORKDIR /data
EXPOSE 4242
-ENTRYPOINT ["zkcoins-server"]
+ENTRYPOINT ["zkcoins-node"]
diff --git a/LIGHTNING_ATOMIC_SWAP.md b/LIGHTNING_ATOMIC_SWAP.md
new file mode 100644
index 00000000..d6d8c7b2
--- /dev/null
+++ b/LIGHTNING_ATOMIC_SWAP.md
@@ -0,0 +1,1216 @@
+# Lightning ↔ zkCoins Atomic Swap — Design Document
+
+**Status:** Design draft. No code yet. Companion to `SPEC.md`,
+`MIGRATION_RESEARCH.md`, `ROADMAP.md`, and
+[`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md).
+
+**Authoritative source for:** *how* trustless LN ↔ zkCoins swaps work
+— not for the wider zkCoins protocol itself.
+
+**Audience:** Engineers picking up swap implementation. Assumes
+familiarity with `SPEC.md` (account model, coin format, inscription
+mechanics) and basic Bitcoin/Lightning HTLC mechanics.
+
+> **Branch note.** This document presupposes the Plonky2 migration
+> currently on `feat/plonky2-migration` (PR #17). `SPEC.md`,
+> `MIGRATION_RESEARCH.md`, and `ROADMAP.md` live on that branch and
+> will resolve on `develop` only after PR #17 lands. Until then, view
+> cross-references against `feat/plonky2-migration`.
+
+---
+
+## 1. Scope
+
+This document specifies the design of **trustless atomic swaps** between
+Lightning Network bitcoin and zkCoins. It covers:
+
+- Why the swap mechanism cannot live on the zkCoins coin layer
+- Where the atomicity primitive actually lives (the Bitcoin funding tx of
+ the `4242`-prefix Taproot inscription)
+- Two concrete swap directions (LN → zkCoins, zkCoins → LN) with full
+ step-by-step protocols
+- Bitcoin script construction and timing coordination
+- Failure-mode analysis and recovery paths
+- Provider operational considerations
+- Privacy analysis
+- The single open zkCoins-side dependency (D7 reorg safety) that affects
+ swap timing but not swap design
+
+It does **not** cover:
+
+- Generic cross-chain swaps not involving Lightning
+- BitVM-style federated bridges (different trust model, different
+ document)
+- Implementation in any specific language or repository layout
+
+---
+
+## 2. Executive Summary
+
+A trustless atomic swap between LN and zkCoins is **buildable with
+today's Bitcoin/Lightning toolchain**, using a standard HTLC on the
+Bitcoin funding tx of the zkCoins inscription. The construction is
+isomorphic to a Boltz reverse-submarine swap with one twist: instead of
+the on-chain side being a P2WSH that pays bitcoin to the user, it is a
+P2WSH/P2TR whose spend includes the zkCoins inscription payload in its
+witness data.
+
+The swap design is **orthogonal to the Plonky2 migration** (PR #17). The
+24-hour LN CLTV budget dwarfs even SP1's minute-scale proof times by
+three orders of magnitude; sub-second proofs are nice-to-have, not a
+gating factor.
+
+The **only zkCoins-side blocker** is D7 (reorg safety, see `SPEC.md` §15,
+`MIGRATION_RESEARCH.md` D7). Until D7 is fixed, the provider must wait
+for deep Bitcoin confirmation of the inscription before settling the
+Lightning side, lengthening the swap's wall-clock time but not affecting
+correctness or trust.
+
+PTLCs (point time-locked contracts) would be an upgrade — better on-chain
+privacy, fungibility with normal single-sig spends — but are not
+required for trustlessness and not available in production Lightning
+implementations as of 2026-05.
+
+---
+
+## 3. Problem Statement
+
+A user wants to convert between Lightning bitcoin and a zkCoins coin
+without trusting any single counterparty with custody of either asset at
+any point during the swap. Equivalently:
+
+- If the user's funds leave Lightning, zkCoins must arrive in their
+ account, or the user can recover the Lightning funds via timeout.
+- If the user's zkCoins leave their account, Lightning bitcoin must
+ arrive, or the user can recover the zkCoins via some refund path.
+
+Symmetrically for the swap provider.
+
+The "single counterparty" referred to is a swap provider (a liquidity
+operator who runs both a zkCoins server and a Lightning node), analogous
+to Boltz's role in BTC ↔ LN submarine swaps.
+
+---
+
+## 4. zkCoins Architecture Recap (Constraints Relevant for Swaps)
+
+### 4.1 Coin model
+
+Per `SPEC.md` §3.2 and `program/src/lib.rs::Coin`:
+
+```rust
+struct Coin {
+ identifier: HashDigest, // = H(sender_next_asth ‖ u32_be(idx))
+ recipient: HashDigest, // = H(initial_pubkey) of the recipient account
+ amount: u64,
+}
+```
+
+There are **no spending conditions, no scripts, no hash-locks, no
+time-locks** on a zkCoins coin. The only constraint enforced at receive
+time is `apply_coin`'s `coin.recipient == self.owner` check
+(`program/src/lib.rs:154`). This matches the upstream Shielded CSV
+paper's `CoinEssence` (pure value transfer) — see
+`MIGRATION_RESEARCH.md` §2.
+
+**Implication:** a zkCoins coin cannot, by itself, carry HTLC semantics.
+There is no protocol-level way to say "this coin can only be spent by
+revealing preimage `x` such that `H(x) = H`".
+
+### 4.2 Send mechanics
+
+Per `SPEC.md` §5 and §11:
+
+1. The sender's server generates a state-transition proof (`ProofData`)
+ covering balance update, output coin creation, and history extension.
+2. The sender's wallet signs `SHA256(serialize(asth) ‖ serialize(ocr))`
+ with BIP-340 Schnorr. Here `asth` is the account state hash and
+ `ocr` is the output coins root (the Merkle root of the SMT
+ containing the send's output coin identifiers); both abbreviations
+ match `SPEC.md`'s glossary.
+3. The server (or any party with the signed `Commitment`) constructs a
+ Taproot commit-reveal pair where the commit tx's txid hex begins
+ with `4242`, and the reveal tx's witness contains the inscription
+ payload (signed `Commitment`).
+4. Both txs are broadcast to Bitcoin.
+5. The scanner picks up `4242`-prefix commit-txs, extracts inscription
+ content from the corresponding reveal-tx, deserialises as
+ `Commitment`, verifies the Schnorr signature, and inserts the
+ commitment into the global SMT.
+
+**Implication 1:** the inscription publication is a **plain Bitcoin
+transaction**. It can have any standard Bitcoin script lock on its
+inputs.
+
+**Implication 2:** the "moment of finality" for a zkCoins send is when
+the scanner has processed the inscription. That is a function of (a)
+the reveal-tx getting sufficient Bitcoin confirmations and (b) the
+scanner running. Until then, the send has not happened from the
+recipient's perspective.
+
+### 4.3 What the wallet knows vs. what the server knows
+
+- **Wallet:** holds the account commitment private key; signs the
+ Schnorr commitment over `SHA256(asth ‖ ocr)`. Holds no Poseidon
+ state, no SMT/MMR data.
+- **Server:** holds the entire state (SMT + MMR), generates proofs,
+ holds the inscription-publishing Bitcoin wallet, runs the scanner.
+
+This split is locked by the server-side-compute architecture decision
+(`MIGRATION_RESEARCH.md` §5; `feedback_zkcoins_server_side_compute`).
+
+For swap design this matters because:
+
+- Anything that requires "the wallet signs after seeing something" is
+ cheap (one round-trip to wallet).
+- Anything that requires "the server constructs and signs a Bitcoin tx
+ that publishes the inscription" can be replaced with "the server
+ constructs the inscription payload and lets a different party
+ publish".
+
+---
+
+## 5. Why Atomicity Cannot Live on the Coin Layer
+
+A naïve design would say: "extend the coin model to carry a hash-lock,
+prove preimage knowledge in the circuit, atomic swap solved." This does
+not work for three independent reasons.
+
+### 5.1 Protocol-level reason
+
+Adding spending conditions to the coin model would be a 12th divergence
+from the published Shielded CSV protocol. The protocol's coin model is
+intentionally minimal — `CoinEssence { address, amount, idx }` (see
+`ShieldedCSV/ShieldedCSV/src/lib.rs:24`). Departing from this is
+appropriate for the MVP only when the divergence has been triaged and
+documented (D1–D11). A 12th divergence to enable swaps would need to be
+designed alongside D2/D10 (recipient hiding) because both touch the
+recipient-side spending check.
+
+### 5.2 Cost reason
+
+Lightning HTLCs use SHA256 preimages. A coin-level hash-lock would
+require either:
+
+- **SHA256 in-circuit:** ~262k gates in Plonky2 per hash (see
+ [Plonky2 SHA256 benchmarks](https://hackmd.io/@clientsideproving/Plonky2MobileBench)).
+ Poseidon-2 hashing two field elements costs ~150–200 constraints.
+ Adding SHA256-preimage proof to every send would inflate proof costs
+ by ~3 orders of magnitude and destroy the sub-second performance
+ target.
+- **Poseidon hash-lock:** cheap in-circuit, but Lightning HTLCs are
+ SHA256. To bridge them would need a hash-translation provider (a
+ trusted party who unlocks the SHA256 HTLC and locks a Poseidon HTLC),
+ which negates trustlessness.
+
+### 5.3 Architectural reason
+
+The only on-chain anchor zkCoins has is the Taproot inscription with
+txid prefix `4242`. There is no on-chain UTXO representing an individual
+coin. Even if a coin had spending conditions in the circuit, enforcement
+of those conditions on-chain would require a separate mechanism the
+protocol does not have.
+
+### 5.4 Conclusion
+
+Atomicity must come from somewhere else. That somewhere is the **Bitcoin
+funding transaction of the inscription reveal**, which is an ordinary
+Bitcoin tx and can carry any standard script lock.
+
+---
+
+## 6. Where Atomicity Lives: The Inscription Funding Tx
+
+Every zkCoins send currently requires the publisher to broadcast a
+Taproot commit-reveal pair. The commit tx has txid prefix `4242`, the
+reveal tx carries the inscription payload (signed `Commitment`) in its
+Taproot script-path witness.
+
+**Key observation:** the commit tx's input(s) come from a Bitcoin UTXO
+the publisher controls. If that UTXO is locked with an HTLC script, then
+the reveal tx is only broadcastable by whoever can satisfy the HTLC's
+spending condition.
+
+This is the lever. The swap design rests entirely on coupling the
+inscription publication to a Bitcoin script lock that, in turn, is
+coupled (via preimage or adapter sig) to a Lightning HTLC/PTLC.
+
+### 6.1 The funding-utxo lock
+
+For an LN → zkCoins reverse submarine swap, the provider locks a UTXO
+with a standard reverse-submarine-swap script. The script has two
+spending paths:
+
+- **Claim path (recipient):** `user_pubkey + preimage(H)`
+- **Refund path (provider):** `provider_pubkey + on_chain_timeout`
+
+The user spends the UTXO via the claim path to publish the inscription;
+the provider can recover via the refund path if the user does not claim
+in time.
+
+### 6.2 Who broadcasts what
+
+| Action | Pre-swap | Lock confirmed | User claims | Provider claims LN |
+| ----- | -------- | -------------- | ----------- | ------------------ |
+| LN payment | — | User → Provider HTLC | — | Provider claims, preimage now on LN-side |
+| On-chain funding UTXO | Provider creates locked UTXO | UTXO confirmed | User spends with preimage; tx contains inscription | — |
+| Inscription | — | — | Published via user's spend tx | Already published in previous step |
+| Scanner state | unchanged | unchanged | Updated to include user's new coin | unchanged |
+
+The non-obvious bit is row 3: the user is the one who publishes the
+inscription, *not* the provider. The provider has prepared everything
+(send proof, inscription payload, Schnorr signature on
+`H(asth ‖ ocr)`), but the act of broadcasting is the user's, and that
+broadcast is gated on knowledge of the preimage.
+
+---
+
+## 7. Atomicity Primitives — HTLC vs PTLC
+
+### 7.1 HTLC (Hash Time-Locked Contract)
+
+The classical Bitcoin/Lightning primitive. Two parties agree on
+`H = SHA256(x)` where `x` is a 32-byte preimage known initially to one
+party (the one initiating the swap or the one receiving funds, depending
+on direction). The lock is satisfied by revealing `x` such that
+`SHA256(x) == H` in the witness; revealing `x` on-chain or via a
+Lightning hop's HTLC settlement makes `x` observable to the other
+party.
+
+- **Availability:** standard since 2017, supported everywhere.
+- **On-chain footprint:** P2WSH with `OP_SHA256 OP_EQUALVERIFY ...`
+ or Taproot script path with equivalent semantics. Hash is visible
+ on-chain.
+- **Privacy:** lookups across chains can correlate by hash. A single
+ hash appearing on Bitcoin L1 (in a swap claim) and within a
+ Lightning channel state (visible to the channel counterparty) is a
+ known privacy leak.
+
+### 7.2 PTLC (Point Time-Locked Contract)
+
+Schnorr-era replacement for HTLC. Two parties agree on a curve point
+`Y = y·G` where `y` is a discrete log known initially to one party. The
+lock is "satisfied" not by revealing `y` in a witness but by completing
+a Schnorr signature whose adaptor was committed to `Y`: the resulting
+on-chain signature, combined with the adaptor signature `s'`, reveals
+`y = s − s'` to anyone who sees both.
+
+- **Availability:** Bitcoin-side fine (BIP-340 Schnorr is standard
+ since Taproot). Lightning-side blocked on widespread PTLC support
+ (`lightning-dev` mailing list, ongoing as of 2026-05).
+- **On-chain footprint:** indistinguishable from a normal single-sig
+ Taproot key-path spend. No script revealed, no hash exposed.
+- **Privacy:** strong — neither the swap's existence nor the linkage
+ between LN payment and on-chain spend is observable on Bitcoin L1.
+
+### 7.3 Which one to build first
+
+HTLC. Three reasons:
+
+1. Production-ready toolchain (Boltz backend, BOLT-11 invoices, all
+ wallets support it).
+2. Trustlessness is identical to PTLC for this design — the on-chain
+ privacy upgrade does not change the security argument.
+3. PTLCs over Lightning depend on third-party progress (LDK, CLN
+ maintainers, Lightning Labs roadmap). Building the LN-side ourselves
+ is out of scope.
+
+PTLC is a future upgrade tracked as an open item, not a v1 dependency.
+
+---
+
+## 8. Detailed Flow A: LN → zkCoins (User Buys zkCoins with LN Bitcoin)
+
+This is the **reverse submarine** direction by Boltz nomenclature: the
+user holds the off-chain asset (LN bitcoin) and wants the on-chain-anchored
+asset (zkCoins). The user generates the preimage, the provider locks
+the on-chain side.
+
+### 8.1 Parties and pre-conditions
+
+- **User:** Lightning node, zkCoins wallet, has an existing zkCoins
+ account (so `recipient = H(initial_pubkey)` is known to them and the
+ provider).
+- **Provider:** Lightning node with inbound liquidity from the user,
+ zkCoins server with sufficient inventory in some operator account,
+ Bitcoin wallet for funding UTXO.
+- **Pre-agreed:** swap amount `A` (in sats), provider fee `F`, swap
+ timeout parameters (`T_lock` for on-chain CLTV, `T_ln` for
+ Lightning CLTV-delta — see §12).
+
+### 8.2 Protocol steps
+
+```
+Step 1. User generates preimage x ←$ {0,1}^256. Computes H = SHA256(x).
+ User sends to provider:
+ - H
+ - user_zkcoins_recipient_address (an Address = H(pubkey))
+ - amount A
+ - user_btc_refund_pubkey for the funding UTXO
+
+Step 2. Provider's zkCoins server prepares the send:
+ - Loads the operator account state
+ - Builds out_coins with one entry: { identifier, recipient =
+ user_zkcoins_recipient_address, amount = A }
+ - Generates the send proof (SP1 or Plonky2 post-cutover)
+ - Computes asth, ocr
+ - Provider's wallet signs H(asth ‖ ocr) with the operator
+ account's commitment pubkey, producing Schnorr signature σ
+ - Assembles full inscription payload P =
+ Commitment { public_key, signature: σ, message: asth‖ocr }
+
+Step 3. Provider's Bitcoin wallet creates a funding UTXO with script:
+
+ OP_IF
+ OP_SHA256 OP_EQUALVERIFY
+ OP_CHECKSIG
+ OP_ELSE
+ OP_CHECKLOCKTIMEVERIFY OP_DROP
+ OP_CHECKSIG
+ OP_ENDIF
+
+ funded with exactly (fee_to_pay_for_reveal_tx +
+ dust_threshold). Call this UTXO U_lock.
+
+Step 4. Provider constructs the unsigned commit-reveal pair for the
+ inscription:
+ - Commit tx: spends U_lock + any provider fee inputs, has
+ one Taproot output committing to the inscription script
+ tree, and a vanity-grind on (input set, output amounts,
+ change scripts) to ensure txid prefix = "4242".
+ - Reveal tx: spends the commit tx's Taproot output via the
+ script path, the script path witness containing inscription
+ payload P.
+
+ The commit tx's spend of U_lock requires the IF-branch
+ (preimage). Provider hands the user:
+ - Unsigned commit tx
+ - Reveal tx (unsigned, will be signed by the inscription
+ script path which is part of the Taproot output)
+ - Provider's pre-signature on the OP_ELSE refund path
+ (so the user can verify the refund script is well-formed,
+ though the user will never need to use it)
+
+Step 5. User verifies:
+ - U_lock is on-chain and matches the script in Step 3 with
+ the correct H, T_lock, and pubkeys
+ - The unsigned commit-reveal pair, once the user adds their
+ preimage + signature to the commit tx's input, would
+ broadcast a tx with txid prefix "4242" whose reveal tx
+ publishes inscription payload P
+ - Inscription payload P contains a Schnorr signature on
+ H(asth ‖ ocr) that verifies against the operator's
+ commitment pubkey
+ - The asth and ocr values, opened by P, are consistent with
+ a send proof that creates a coin to user_zkcoins_recipient_address
+ of amount A
+
+ If any check fails, the user aborts. No funds at risk —
+ nothing has been sent on the LN side yet.
+
+Step 6. User pays the Lightning HTLC:
+ - User → Provider, hash H, amount A + F, CLTV-delta T_ln
+
+Step 7. User waits for U_lock to reach the agreed confirmation depth
+ (see §12 and §16). Then user broadcasts the commit tx:
+ - Witness for U_lock spend: , IF-branch
+ - Commit tx now in mempool
+
+Step 8. Commit tx confirms. User broadcasts the reveal tx, which
+ publishes inscription P on-chain.
+
+Step 9. zkCoins scanner picks up the `4242`-prefix commit tx, follows
+ through to the reveal tx, extracts P, verifies the Schnorr
+ signature, calls State::update([P]). The user's
+ zkcoins_recipient_address now holds the new coin.
+
+Step 10. The user's preimage x is now visible on-chain (in the witness
+ of the commit tx's spend of U_lock). The provider's Lightning
+ node either:
+ - Observes the preimage on-chain and uses it to claim the
+ LN HTLC (preimage-watch pattern)
+ - Or the user explicitly reveals x via off-band channel; the
+ user has every incentive to do so since the swap is now
+ complete from their perspective and reveal-then-settle
+ reduces both parties' channel risk
+
+Step 11. Provider settles the LN HTLC, capturing A + F. Swap complete.
+```
+
+### 8.3 What can go wrong
+
+| Failure | Who has what | Recovery |
+| ------- | ------------ | -------- |
+| User aborts at Step 5 | Provider has funded U_lock; nothing else moved | Provider refunds U_lock at T_lock (Step 3 ELSE branch). Cost: on-chain fee for U_lock creation. |
+| User pays LN (Step 6) but never broadcasts commit (Step 7) | Provider has incoming LN HTLC, U_lock still locked | LN HTLC times out at T_ln, user gets LN funds back. Provider refunds U_lock at T_lock. Both whole. |
+| User broadcasts commit but it doesn't confirm before T_lock | User has paid LN, U_lock is being refunded by provider; user's tx might or might not eventually confirm | This is the race condition T_lock is designed to prevent. See §12. With margin, this should not happen; if it does, provider claims U_lock refund and user claims LN refund. Provider has zkCoins still in inventory (no send actually happened since inscription never landed). |
+| Provider's server crashes between Step 2 and Step 4 | User has H, has not paid anything | User aborts, no loss. |
+| Provider's Bitcoin wallet runs out of funds for U_lock | Pre-condition failure | Provider rejects swap initiation. No loss. |
+| Provider refuses to settle LN at Step 11 despite preimage visible | Provider has zkCoins inventory still committed, user has zkCoins (Step 9 succeeded), preimage on-chain | LN HTLC will time out and refund to user. User keeps zkCoins **and** gets LN funds back. **Net: provider loses A+F to itself.** This is asymmetric — provider has no incentive to do this. Documented as provider-side discipline. |
+
+### 8.4 Why this is trustless
+
+At no point does either party transfer custody of an asset to the other
+party where the other party can withhold reciprocation:
+
+- User commits LN payment **after** seeing the funded U_lock with the
+ correct script.
+- User claims zkCoins-side **before** revealing preimage (preimage is
+ in the spend witness, so revealing happens at the moment of
+ on-chain publication).
+- Provider's refund path is gated on T_lock, which is shorter than
+ T_ln, so provider cannot get U_lock back via timeout while
+ simultaneously claiming LN.
+
+The only scenarios where someone loses funds are (a) the user pays LN
+and then never claims on-chain, in which case both sides time out and
+both are made whole, or (b) one party broadcasts a refund tx with a
+fee too low to confirm, which is a fee-management concern not a trust
+concern.
+
+---
+
+## 9. Detailed Flow B: zkCoins → LN (User Sells zkCoins for LN Bitcoin)
+
+This is the **forward submarine** direction: the user holds the on-chain
+asset (zkCoins) and wants the off-chain asset (LN bitcoin). The
+direction matters because the user is the one initiating the
+zkCoins-side send, which means the user controls the inscription
+publication — flipping who broadcasts what.
+
+### 9.1 The role inversion
+
+In Flow A the user was the inscription broadcaster (Step 7–8). In Flow
+B the user is the inscription *originator* (they own the source coins)
+but the provider is the LN payer. The naïve "provider generates the
+preimage" construction (mirroring Boltz forward submarine swaps)
+introduces a non-trustless gap when applied to inscription publication
+— see §9.3 for why. The recommended construction is a direct mirror
+of Flow A with the swap roles reversed; the preimage generator stays
+on the on-chain-asset-acquirer's side. This is detailed in §9.2.
+
+### 9.2 Recommended pattern: mirror of Flow A
+
+```
+Step 1. Provider generates preimage x ←$ {0,1}^256. Computes
+ H = SHA256(x). Provider sends to user:
+ - H
+ - provider_zkcoins_recipient_address
+ - amount A
+ - provider's LN invoice for amount A − F (standard, not hold)
+
+Step 2. User's zkCoins server prepares the send proof to
+ provider_zkcoins_recipient_address with amount A. User signs
+ Schnorr σ over H(asth ‖ ocr) with their commitment pubkey.
+
+Step 3. User funds a Bitcoin UTXO U_lock' from their own wallet with
+ the same Taproot two-leaf construction as Flow A:
+
+ IF-branch (claim): +
+ ELSE-branch (refund): after T_lock
+
+ User constructs the unsigned commit-reveal pair such that
+ the commit tx spends U_lock' via the IF-branch and the
+ reveal tx publishes the inscription containing σ.
+
+Step 4. User hands provider:
+ - (asth, ocr, σ)
+ - U_lock' outpoint
+ - Unsigned commit-reveal pair
+
+Step 5. Provider verifies:
+ - σ verifies against user's commitment pubkey
+ - asth + ocr describe a send to provider's address of
+ amount A
+ - U_lock' is on-chain with the correct script
+ - Commit tx spends U_lock' and has txid prefix 4242
+
+Step 6. Provider pays the Lightning HTLC to user with hash H,
+ amount A − F.
+
+Step 7. User claims the LN HTLC. The settlement reveals x to
+ provider via the LN channel mechanics (preimage-watch
+ pattern, or explicit reveal off-band).
+
+Step 8. Provider broadcasts the commit tx with witness
+ (IF-branch satisfied).
+
+Step 9. Commit tx confirms. Provider broadcasts reveal tx;
+ inscription publishes on-chain; zkCoins scanner picks up
+ and credits provider's address.
+
+Step 10. Swap complete.
+```
+
+#### Failure modes for Flow B (Pattern 9.2)
+
+| Failure | Who has what | Recovery |
+| ------- | ------------ | -------- |
+| Provider does not pay LN | U_lock' is locked; nothing else moved | User refunds U_lock' at T_lock. Cost: on-chain fee for U_lock' creation. |
+| Provider pays LN, user claims, provider broadcasts | Happy path | Swap completes. |
+| User claims LN but provider does not broadcast commit tx | Provider has x and own signature; they can broadcast any time before T_lock. If they don't, U_lock' refunds to user. User keeps LN funds; provider keeps zkCoins inventory (no inscription landed). | Provider has no incentive to withhold — they would forgo the zkCoins inflow they already paid for in LN. Documented as provider-side discipline. |
+| User funds U_lock' but never sends provider the commit-reveal pair | Pre-condition failure | User can refund U_lock' at T_lock. No LN payment was made. |
+| Commit tx stuck in mempool past T_lock | Race condition | Avoided by the ordering constraint of §12.2; if exhausted, U_lock' refunds to user and provider keeps LN funds. Provider must factor this risk into fee pricing. |
+
+The last failure mode of the table is worth flagging in code: if the
+inscription never lands, the zkCoins state never updates. The user's
+server-side state shows the send as "prepared" but not "committed",
+because the corresponding `Commitment` was never broadcast. The
+swap-aware server must release the prepared state if it observes that
+the corresponding U_lock' has been refunded, so the user can re-use
+those coins for another swap or send.
+
+### 9.3 Why we rejected the "provider generates preimage" pattern
+
+A pattern that more closely mirrors Boltz forward submarine swaps —
+where the provider generates the preimage and the user constructs the
+locked UTXO — does not yield trustlessness for inscription
+publication. The reason is structural:
+
+- If the commit tx is spendable by ``, then after
+ provider claims LN (and learns x), the user cannot broadcast the
+ commit tx on the provider's behalf when provider stalls — only
+ provider has the signature. T_lock expires, U_lock' refunds, but
+ the LN payment was already settled, so the user is out A − F.
+- If the commit tx is spendable by `` instead, the user
+ can broadcast at any time after learning x — but x is generated by
+ provider, so the user only learns it after LN settlement. Same
+ asymmetry, flipped: provider could broadcast a fake LN payment
+ flow and steal the zkCoins.
+- A 2-of-2 IF-branch (``) lets either
+ party grief: the preimage reveal alone is no longer sufficient to
+ unilaterally publish.
+
+A patch using an **LN hold invoice** to make the user the LN
+settlement-controller also fails to close the gap cleanly, because
+the user's reveal of x to settle the hold invoice and the provider's
+broadcast of the commit tx remain two separate events with no
+on-chain coupling between them.
+
+Pattern 9.2 avoids all of this by having the same party (provider)
+control both the LN claim and the on-chain broadcast — the preimage
+reveal through LN settlement directly enables that party to broadcast.
+
+---
+
+## 10. Bitcoin Script Construction
+
+### 10.1 Script template (legacy P2WSH for clarity)
+
+```
+OP_IF
+ OP_SHA256 ; H = SHA256(preimage)
+ OP_EQUALVERIFY
+ ; whoever can claim via preimage
+ OP_CHECKSIG
+OP_ELSE
+ ; absolute or relative timeout
+ OP_CHECKLOCKTIMEVERIFY ; CLTV (absolute) or CSV (relative)
+ OP_DROP
+ ; whoever can refund after timeout
+ OP_CHECKSIG
+OP_ENDIF
+```
+
+Bytes: ~83 (claim + refund) for compressed-pubkey + 32-byte hash.
+
+### 10.2 Taproot variant (recommended for production)
+
+Use a Taproot output with two leaves:
+
+- **Leaf A (claim):** `OP_SHA256 OP_EQUALVERIFY
+ OP_CHECKSIGVERIFY`
+- **Leaf B (refund):** ` OP_CHECKLOCKTIMEVERIFY OP_DROP
+ OP_CHECKSIGVERIFY`
+
+Internal key: NUMS point (provably-unknown discrete log) or a
+2-of-2 MuSig of claim+refund keys (allows cooperative key-path spend
+that hides the script entirely — Boltz's V2 swap design does this).
+
+Cooperative key-path spending makes successful swaps look like normal
+single-sig Taproot spends, improving fungibility. Script-path is the
+fallback for non-cooperative resolution.
+
+### 10.3 Vanity-grinding txid prefix `4242`
+
+The commit tx of the inscription pair must have txid hex starting with
+`4242`. This is a 2-byte prefix, so on average 65k brute-force attempts
+to find a matching nonce. zkCoins's existing publisher
+(`node/src/publisher.rs`) handles this by varying the commit tx's
+output amount (sat-level) until the prefix matches.
+
+For the swap design, the variable that can be ground is the commit
+tx's change output amount (the difference between U_lock + fee-input
+and the Taproot commit output amount, sent back to a change address
+controlled by whoever is broadcasting). Either the provider (Flow A
+pre-construction) or the user (Flow A Step 7 broadcast time, if the
+commit tx is finalised then) handles the grind.
+
+Caveat: changing the change-amount changes the tx hash, but it also
+slightly changes the fee, which is fine in mempool. Standardness rules
+to watch: the change output must remain ≥ dust threshold (~330 sat for
+Taproot).
+
+### 10.4 Funding the U_lock UTXO
+
+In Flow A, the provider funds U_lock from their own Bitcoin wallet.
+The amount is just enough to cover the commit tx fee + dust threshold
+for the commit tx's outputs. The reveal tx pays for itself from the
+Taproot output.
+
+The actual zkCoins coin value (A) is not transferred via Bitcoin —
+zkCoins state lives entirely off-chain in the SMT/MMR. The on-chain
+piece is the inscription, which is essentially a 64-byte signature
+plus envelope overhead. Total on-chain Bitcoin cost per swap is
+roughly the same as a Boltz swap minus the actual L1 payout: ~250
+sats at current fee rates.
+
+### 10.5 Pubkey choices
+
+- **claim_pubkey:** the user's Bitcoin spending pubkey for Flow A, or
+ the provider's for Flow B. Should be a fresh key per swap for
+ unlinkability.
+- **refund_pubkey:** the counterparty's. Same fresh-key recommendation.
+
+In a Taproot internal-key construction, the cooperative key is a MuSig
+of (claim_pubkey, refund_pubkey).
+
+---
+
+## 11. The Inscription Reveal Tx — Anatomy
+
+For completeness, the reveal tx that ultimately publishes the
+`Commitment` payload:
+
+- **Input:** the commit tx's Taproot output.
+- **Witness:** Taproot script-path spend, providing
+ - The inscription script (Ordinals-style envelope: `OP_FALSE OP_IF
+ "ord" OP_ENDIF`, with `` being the serialised
+ `Commitment` plus zkCoins-specific envelope tag)
+ - The internal pubkey
+ - The control block proving the script is in the Taproot script tree
+- **Output:** a P2WPKH or P2TR output of dust value going back to the
+ publisher (the reveal tx is a "burn the inscription" tx; the output
+ is just there because every tx needs an output).
+
+This is unchanged from the current zkCoins publisher implementation;
+the only thing the swap design touches is the commit tx's input
+(U_lock), not the reveal tx itself.
+
+---
+
+## 12. Timing Coordination (CLTV Deltas)
+
+### 12.1 The two timeouts
+
+- **`T_lock`:** absolute Bitcoin block height at which the on-chain
+ U_lock UTXO becomes refundable to the provider (Flow A) or user
+ (Flow B). Set at swap creation time.
+- **`T_ln`:** the CLTV-delta of the Lightning HTLC, in blocks. The LN
+ payment is refundable to the payer after the HTLC's expiry block,
+ which is the most recently locked-in block height + `T_ln`.
+
+### 12.2 The ordering constraint
+
+The fundamental requirement for trustlessness:
+
+```
+T_lock < (current_height + T_ln) - safety_margin
+```
+
+Equivalently: the on-chain refund path must mature *before* the LN
+refund path matures.
+
+Why: imagine the alternative, `T_lock > current_height + T_ln`. Then
+LN refunds first. Suppose the user pays LN, never claims on-chain. LN
+refunds the user at `T_ln`. Provider's U_lock is still locked until
+`T_lock`. But by then, the user has their LN funds back AND can still
+broadcast the commit tx (they have the preimage they generated, plus
+their claim signature). User publishes inscription, scanner credits
+user, user has both LN-refunded funds and new zkCoins. Provider loses
+inventory.
+
+With `T_lock < current_height + T_ln − safety_margin`, the order is:
+T_lock fires first → provider refunds U_lock → user can no longer
+claim → LN refunds at `T_ln` later. Both whole.
+
+### 12.3 Typical values
+
+- LN CLTV-delta: most modern nodes use 40 blocks final + up to 144 per
+ hop. End-to-end on a single-hop swap (user ↔ provider direct
+ channel) typically ~144 blocks ≈ 24 hours.
+- On-chain `T_lock`: should be ~24h or less from now to leave a clear
+ margin. Typical Boltz value: 144 blocks from creation.
+- Safety margin: at least 6 blocks (~1 hour) to allow for confirmation
+ delays at the boundary. Boltz uses ~12-block margin.
+
+### 12.4 Required confirmation depth for U_lock
+
+Before the user broadcasts the claim tx (Flow A Step 7), U_lock must
+be confirmed to a depth where the provider cannot RBF or double-spend
+it. Standard recommendation: 1 confirmation is sufficient if U_lock's
+funding tx is below RBF threshold and confirmed in a non-reorg-prone
+context; 2-3 confirmations for higher-value swaps. This is independent
+of the D7 reorg-safety question, which concerns confirmation depth of
+the *inscription publication*, not U_lock.
+
+### 12.5 The proof-time question
+
+Provider's send proof generation (zkCoins server side):
+
+- SP1 today: tens of seconds to a few minutes warm.
+- Plonky2 post-cutover target: ≤1 second warm.
+
+This happens between Step 1 (user requests swap) and Step 4 (provider
+hands user the commit-reveal pair). Even with SP1, the proof time
+is negligible compared to the 24-hour swap window. **Plonky2 is not
+a swap dependency.**
+
+(The proof time *would* matter for some hypothetical
+ultra-low-latency swap product — pay LN, get zkCoins balance within
+3 seconds. Such a product is not on the roadmap and would require
+solving D7 at the same time anyway.)
+
+---
+
+## 13. Failure Modes Matrix (Both Flows)
+
+Summary of all scenarios. "User" and "Provider" refer to the swap
+counterparties regardless of direction.
+
+| Scenario | Who lost what | Recovery mechanism |
+| -------- | ------------- | ------------------ |
+| Both parties cooperate, all txs confirm | Nothing lost; everyone gets expected outcome | Happy path |
+| User aborts before LN payment | Provider has funded U_lock + spent proof time | U_lock refund at T_lock; proof time is a sunk cost (~free) |
+| LN payment fails to route | No state change | LN-layer retry or refund |
+| LN payment succeeds, user fails to claim on-chain (Flow A) | Provider has LN HTLC pending, user has paid LN | LN HTLC times out at T_ln, user refunded; U_lock refunds at T_lock |
+| User claims on-chain but commit tx stuck in mempool past T_lock | Race condition | Avoided by §12.2 ordering constraint with margin; if margin exhausted, both refund — provider via U_lock refund, user via LN refund (assuming commit tx also evicted from mempool) |
+| Provider's Bitcoin wallet outage between Step 3 and broadcast | Pre-condition failure | Swap not initiated; no loss |
+| Bitcoin reorg removes the confirmed commit tx | See §16 (D7 dependency) | Provider waits ≥6 confirms before claiming LN |
+| zkCoins scanner is offline | Inscription is on-chain but state lags | Scanner catches up on restart; no swap-mechanism impact |
+| Provider claims LN but withholds inscription broadcast (Flow B) | Provider has LN, has not delivered zkCoins | Provider has no incentive — they would forgo the zkCoins inflow they already paid for in LN. If they do withhold past T_lock, U_lock' refunds to user; user keeps LN funds. See §9.2 failure-mode table. |
+| Provider sets up Sybil swaps to grief | None directly | DoS mitigation: rate-limit, optionally require small upfront fee or deposit |
+
+---
+
+## 14. Provider Operational Considerations
+
+### 14.1 Liquidity management
+
+The provider needs two inventories simultaneously:
+
+- **LN liquidity (outbound + inbound):** outbound for Flow B (paying
+ user), inbound for Flow A (receiving user's payment). Standard LN
+ channel management. Boltz publishes inbound/outbound LP rates
+ dynamically.
+- **zkCoins inventory:** one or more operator accounts with sufficient
+ balance in zkCoins to honour Flow A swaps. Inventory rebalances:
+ Flow B replenishes the operator account (user sends zkCoins to
+ provider's address); Flow A depletes it. Net flows over time should
+ be matched by an out-of-band rebalancing flow (provider mints new
+ zkCoins by depositing BTC, or burns zkCoins for BTC, via whatever
+ L1-zkCoins bridge mechanism is in place).
+
+zkCoins does not currently have a published bridge mechanism. The
+MVP-era assumption is that the provider is also the minter (the
+holder of `MINTING_ADDRESS`), which trivially provides inventory.
+Once the protocol has a real bridge (BitVM-style or otherwise), the
+provider can be any party with that bridge's deposit/withdraw
+capability.
+
+### 14.2 Fee model
+
+Three components, mirroring Boltz:
+
+- **On-chain fee:** the actual Bitcoin tx fee for the commit-reveal
+ pair. Paid out of U_lock funding amount; the user effectively pays
+ this since they are the asset-acquirer in Flow A.
+- **Routing fee:** LN routing cost on the provider's payment in Flow B,
+ or absorbed if Flow A receives a direct payment.
+- **Provider margin:** a percentage of swap amount, the actual revenue
+ source for the provider.
+
+Typical Boltz total fees: 0.1–0.5% of swap amount + ~250 sat on-chain.
+
+### 14.3 Inventory locked during swap
+
+Between Step 2 (provider prepares send) and Step 9 (inscription
+confirms), the provider's zkCoins inventory is committed but
+not-yet-published. The provider must not initiate another swap that
+would also commit the same balance — server-side concurrency control
+required.
+
+Concretely, the operator account's "soft balance" must reflect:
+`balance − Σ(pending_swap_amounts)`, where `pending_swap_amounts`
+includes all amounts for prepared-but-not-confirmed sends.
+
+This is the "stuck inventory" problem of any submarine swap provider;
+Boltz solves it with parallel HTLC tracking. zkCoins-side it requires
+the swap-aware server to track prepared swaps until inscription
+confirms (or refund completes).
+
+### 14.4 Watching the chain
+
+The provider's Bitcoin watcher must monitor:
+
+- U_lock UTXOs they have created (for refund-at-T_lock)
+- Commit txs spending U_lock UTXOs (to extract preimages and claim LN
+ in Flow A, or to confirm completion in Flow B)
+- Reveal txs (to confirm scanner-pickup)
+- Bitcoin reorgs affecting any of the above
+
+LND's `chainntfn` or BTCD's notification API are the standard tools.
+Boltz's backend repo (`BoltzExchange/boltz-backend`) has a battle-tested
+watcher implementation that could be forked.
+
+### 14.5 The grind for `4242` prefix
+
+The vanity-grind (§10.3) takes time — at 65k attempts average, a
+modern CPU can grind a single 4242-prefix tx in ~1 second. Not a
+bottleneck, but should be parallelised if the provider expects high
+swap volume. Easy to GPU-accelerate; not necessary for v1.
+
+---
+
+## 15. Privacy Analysis
+
+### 15.1 What the provider learns
+
+- **Recipient zkCoins address** (Flow A) or sender's zkCoins address
+ (Flow B). The full `Address = H(initial_pubkey)`. Acceptable for
+ regulated providers who already perform KYC on swap counterparties.
+- **Amount.** Necessarily, since it's the swap amount.
+- **The user's Bitcoin pubkey** (claim/refund pubkey on U_lock).
+ Recommend fresh key per swap.
+- **The user's LN node identity** for the LN payment. Single-hop direct
+ channel reveals; multi-hop preserves payer anonymity to the same
+ extent any LN payment does.
+
+### 15.2 What is on-chain
+
+- The funded U_lock UTXO (a 2-leaf Taproot output).
+- The commit tx spending U_lock (Taproot output to inscription, with
+ txid prefix `4242`).
+- The reveal tx with inscription payload in witness.
+- If swap fails: a refund tx spending U_lock via the ELSE branch.
+
+A chain observer sees:
+- A Taproot input being spent with either script path (failure case)
+ or — if cooperative key-path is used (§10.2) — what looks like a
+ normal single-sig Taproot spend
+- A subsequent commit tx with txid prefix `4242`, which is
+ zkCoins-protocol-specific and identifies the spend as a zkCoins
+ send
+
+So the swap, on the Bitcoin side, is publicly identifiable as a zkCoins
+send. Whether it's a *swap* (vs. a direct user-initiated send) is
+inferable from the U_lock script structure if non-cooperative. With
+cooperative key-path resolution, the swap looks identical to a direct
+zkCoins send.
+
+### 15.3 What is in Lightning
+
+A standard Lightning HTLC of amount A ± F with hash H. Same privacy
+properties as any LN payment of similar size. If the LN counterparty
+is the provider directly, the provider sees both ends; if routed
+through hops, intermediate hops see the hash and amounts (standard LN
+payment privacy).
+
+### 15.4 What PTLCs would change
+
+PTLCs would eliminate (a) the on-chain hash visibility and (b) the LN
+hash → on-chain hash correlation. The on-chain spend would be
+indistinguishable from any single-sig Taproot key-path spend, and the
+LN payment would use a point lock that does not appear on Bitcoin L1
+in plaintext.
+
+This is purely an upgrade; HTLC v1 is already trustless.
+
+### 15.5 zkCoins-internal privacy: D2/D10
+
+D2 (plaintext recipient) is a pre-mainnet blocker for general zkCoins
+privacy, but for the swap design it does not introduce any new
+linkability — the provider already knows the recipient address by
+construction (the user told them in Step 1). When D2/D10 are fixed
+with hiding commitments, the swap protocol must include the per-coin
+randomness in the Step 1 user-to-provider message so the provider can
+build a coin opening to the hidden recipient. This is a minor protocol
+update, not a redesign.
+
+---
+
+## 16. D7 Reorg Safety — The Open Dependency
+
+### 16.1 What D7 is
+
+From `SPEC.md` §15 and `MIGRATION_RESEARCH.md` §3, D7:
+
+> No conditional-noop path. Paper supports `conditional_nav` — if the
+> claimed nullifier-accum is no longer a prefix of the chain's, the tx
+> becomes a no-op.
+
+In zkCoins-as-implemented, when the scanner processes an inscription
+and updates the SMT, that update is taken as final. If Bitcoin reorgs
+and the inscription tx is reorganised out, the scanner has no graceful
+way to undo the SMT update. The protocol "trusts" the scanner's
+view of the chain.
+
+### 16.2 What this means for swaps
+
+For Flow A, between Step 8 (commit tx confirms) and Step 11 (provider
+settles LN), there is a window where:
+
+- Inscription is on-chain at depth `d` (where `d` is small immediately
+ after confirmation)
+- Provider sees preimage on-chain
+- If provider settles LN now and Bitcoin reorgs at depth ≥ d, the
+ inscription is no longer in the chain — but the scanner already
+ ingested it. zkCoins state has the new coin (assigned to user) but
+ the chain does not.
+
+This is a soundness problem for zkCoins (D7), not for the swap. The
+swap-level mitigation is: **provider waits for sufficient confirmation
+depth before settling LN**.
+
+### 16.3 Required confirmation depth
+
+This is the operationally interesting question. Options:
+
+- **Same as Boltz BTC ↔ LN swaps:** Boltz settles after ~3 BTC
+ confirmations. The argument is that 3 confirmations is sufficient
+ against routine reorgs; deeper reorgs are rare-enough events that
+ the residual risk is absorbed by the provider as part of operational
+ cost.
+- **More conservative:** wait for 6 confirmations (Bitcoin's
+ traditional "confirmed" threshold) to align with bitcoin custodial
+ practice.
+- **Most conservative:** wait for `CONFIRMS_TO_FINALITY` set by
+ zkCoins protocol parameters; could be 6 or 100 depending on threat
+ model.
+
+A regulated provider should default to **6 confirmations** (~1 hour
+wait) until D7 is fixed. After D7 is fixed (the scanner can gracefully
+handle inscription reorg by rolling back state and re-inserting), the
+depth can drop back to 3 or even 1 with appropriate scanner logic.
+
+### 16.4 LN CLTV must accommodate this wait
+
+The LN-side `T_ln` must comfortably exceed the wait time. With
+6-confirm depth (~1 hour) + safety margin + variable Bitcoin block
+times (could be 2x mean), an LN CLTV of 144 blocks (~24h) is more
+than sufficient.
+
+### 16.5 D7 fix is tracked separately
+
+D7 is in the Pre-Mainnet Hardening block (`ROADMAP.md`), estimated
+4–5 days of work. It is independent of the swap design and required
+for mainnet regardless.
+
+The dependency for the swap launch is: **swap can ship before D7 is
+fixed, with conservative confirmation-depth gating**. D7 fix later
+just allows lower latency.
+
+---
+
+## 17. Plonky2 Relevance — Orthogonal to the Swap Design
+
+The PR #17 Plonky2 migration is **not a blocker** for swap
+implementation. Specifically:
+
+- **Performance:** SP1 minute-scale proofs fit comfortably in the
+ 24-hour LN CLTV window. Plonky2 sub-second proofs reduce
+ provider-side inventory-locked-time from minutes to seconds, which
+ is a per-swap operational improvement, not a correctness condition.
+- **Hash function (Poseidon vs SHA256):** does not touch the swap
+ mechanism. SHA256 is used by Lightning (HTLC preimage) and BIP-340
+ Schnorr (commitment signature). Poseidon is used internally for
+ Merkle structures. The swap construction is hash-agnostic.
+- **Coin model:** unchanged by Plonky2. The swap design's core insight
+ (atomicity on the Bitcoin funding tx, not the coin layer) is forced
+ by the coin model and persists across proof-system migrations.
+- **Schnorr signing:** unchanged. The signature on H(asth ‖ ocr) is
+ BIP-340 over secp256k1, exactly the signature that goes into the
+ inscription payload, exactly the signature the scanner verifies.
+
+Implementation can therefore run in parallel to PR #17 without
+contention. The swap code touches `server/` (new endpoints) and adds a
+new operational component (Bitcoin script construction, LN node
+integration). Neither touches `program-plonky2/` or `program/`.
+
+If swap implementation starts before PR #17 lands, it should be done
+behind feature flags or in a side-branch to be merged after the
+Plonky2 cutover; this avoids dealing with two simultaneous major
+refactors.
+
+---
+
+## 18. Comparison Tables
+
+### 18.1 vs. Boltz BTC ↔ LN
+
+| Property | Boltz BTC ↔ LN | This (LN ↔ zkCoins) |
+| -------- | -------------- | ------------------- |
+| Trust model | Trustless | Trustless |
+| On-chain side primitive | P2WSH/P2TR HTLC | P2WSH/P2TR HTLC gating inscription publication |
+| What's swapped on-chain side | Native BTC value | zkCoins coin (off-chain state update triggered by inscription) |
+| On-chain footprint per swap | ~250 sat fees | ~250 sat fees |
+| LN side | Standard HTLC | Standard HTLC |
+| Wait for confirmation depth | ~3 confirms | ~6 confirms (D7 mitigation, until fixed) |
+| Provider role | Liquidity provider, custodian of *neither* side | Same |
+| PTLC upgrade path | Boltz V3 (announced) | Trivial mirror once LN PTLC matures |
+
+### 18.2 vs. Taproot Assets atomic swaps
+
+| Property | Taproot Assets | This (LN ↔ zkCoins) |
+| -------- | -------------- | ------------------- |
+| Asset locked on Bitcoin L1 | Yes (in Taproot leaves) | No (zkCoins state is off-chain) |
+| Asset issuance | On-chain proofs | Off-chain proofs (PCD) |
+| Swap primitive | PSBT-based, atomic | HTLC on inscription funding tx |
+| Cross-chain step | None needed (asset lives on BTC) | The "chain" boundary is Bitcoin (LN funds + inscription) ↔ zkCoins state |
+| RFQ-style quote mechanism | Yes, native | Easy to add as out-of-band layer |
+
+### 18.3 vs. naïve "trusted swap service"
+
+| Property | Trusted custodial service | Trustless HTLC |
+| -------- | ------------------------- | --------------- |
+| Trust assumption | The custodian honours its claims | None (cryptographic) |
+| Bitcoin-script complexity | None | Standard P2TR with 2 leaves |
+| Build effort | Low (just an exchange API) | Medium (Boltz-backend fork + zkCoins integration) |
+| Risk if provider compromised | User funds at risk | None — cryptographic atomicity |
+| Suitable for production | Yes, with appropriate insurance / disclosures | Yes |
+
+---
+
+## 19. Implementation Roadmap
+
+A draft sequence; not a commitment.
+
+### 19.1 Phase 0: prerequisites
+
+- D7 reorg fix in zkCoins (pre-mainnet hardening block; can be deferred
+ if conservative confirm-depth gating is used)
+- Operator account funded with sufficient zkCoins inventory
+- Provider Bitcoin wallet with Lightning channel(s)
+- LND or CLN node running (standard HTLC support sufficient; hold
+ invoices not required by the recommended Pattern 9.2)
+
+### 19.2 Phase 1: swap engine
+
+- Bitcoin script construction module (P2WSH + P2TR variants, both
+ flows)
+- Watcher: monitor U_lock UTXOs, commit txs, reveal txs, refund-window
+- Vanity-grinder for `4242` prefix (or reuse existing
+ `node/src/publisher.rs` logic if it can be extracted)
+- Inscription payload generator that can produce a `Commitment` for a
+ *specified* recipient and amount, signed by the operator key,
+ *without* publishing on-chain — Step 2 of Flow A
+
+### 19.3 Phase 2: API surface
+
+- `POST /api/swap/quote` — user requests quote, provider returns
+ amount + fee + expected timeouts
+- `POST /api/swap/initiate` (Flow A) — user submits H + recipient
+ address + amount + refund pubkey, gets back commit-reveal pair +
+ U_lock funded outpoint
+- `POST /api/swap/lock` (Flow B) — provider gives user the H and
+ provider's claim pubkey; user constructs their side and notifies
+- `GET /api/swap/{id}` — status (waiting-for-confirms, settled,
+ refunded, etc.)
+- WebSocket for live status updates
+
+### 19.4 Phase 3: LN integration
+
+- Hook the swap engine into LND/CLN's HTLC settlement
+- Configure routing fee thresholds, channel rebalancing alerts
+- Define the rate-card (provider margin)
+
+### 19.5 Phase 4: production hardening
+
+- Rate limits per IP / per user
+- Sybil resistance: optional small upfront fee
+- Monitoring + alerting (Grafana board for in-flight swaps, alert on
+ stuck/expiring swaps)
+- Recovery tooling for stuck swaps (manual operator intervention if
+ watcher fails)
+
+### 19.6 Estimated effort
+
+- Phase 1: 2–3 weeks
+- Phase 2: 1 week
+- Phase 3: 1 week
+- Phase 4: 1–2 weeks
+- Total: 5–7 weeks for a production-grade implementation, assuming
+ Boltz-backend code can be partially reused for watcher/grinder
+
+---
+
+## 20. Open Questions
+
+1. **Required confirmation depth for inscription.** Set initially to
+ 6 confirms (~1 hour wait); re-evaluate after D7 fix lands.
+
+2. **Cooperative key-path for U_lock Taproot internal key.** MuSig of
+ (claim_pubkey, refund_pubkey) gives best on-chain privacy but adds
+ protocol complexity (round of MuSig key aggregation per swap). For
+ v1, recommend NUMS internal key (cheaper, less private). Revisit
+ for v2 alongside PTLC.
+
+3. **Where does the operator account's privkey live?** The Schnorr
+ signature on H(asth ‖ ocr) (Step 2 of Flow A) needs to happen
+ server-side, because the operator is the sender. This means the
+ operator account's commitment key is server-resident. Same
+ architectural assumption as for any operator-issued zkCoins coin;
+ should be documented in ops runbook.
+
+4. **Cross-swap correlation.** If a single operator account is reused
+ for many swaps, all those swaps' inscriptions chain through the
+ same account state. A chain analyst can correlate them. Mitigation:
+ rotate operator accounts periodically. Not a blocker.
+
+5. **D7 fix interaction.** Once D7 lands with `conditional_nav`-style
+ logic, the scanner can roll back. The swap design's confirm-depth
+ parameter should drop, and the swap engine should subscribe to
+ reorg notifications. Sketch the rollback-aware swap state machine
+ when D7 is implemented; not now.
+
+6. **Fee market integration.** Should swap quotes include a
+ user-selected fee tier (fast/slow Bitcoin confirmation, expected
+ wait time)? Boltz does this. Adds UI but not protocol complexity.
+
+7. **Maximum swap size.** Bounded by (a) operator zkCoins inventory,
+ (b) operator LN inbound liquidity. Define soft and hard limits.
+ Boltz publishes these on an info endpoint.
+
+---
+
+## 21. References
+
+- [Shielded CSV paper (Nick, Eagen, Linus)](https://eprint.iacr.org/2025/068)
+- [Shielded CSV reference implementation](https://github.com/ShieldedCSV/ShieldedCSV)
+- [Boltz backend (HTLC-based submarine swap reference implementation)](https://github.com/BoltzExchange/boltz-backend)
+- [Boltz lifecycle docs](https://github.com/BoltzExchange/boltz-backend/blob/master/docs/lifecycle.md)
+- [Boltz blog: Lightning ↔ Liquid via submarine swaps](https://bitcoinmagazine.com/business/between-bitcoin-layers-boltz-builds-trustless-transfers)
+- [Submarine Swaps — Lightning Engineering Builder's Guide](https://docs.lightning.engineering/the-lightning-network/multihop-payments/understanding-submarine-swaps)
+- [Multi-Party Submarine Swaps (conduition.io)](https://conduition.io/scriptless/multi-party-submarine-swaps/)
+- [PTLCs — Bitcoin Optech](https://bitcoinops.org/en/topics/ptlc/)
+- [Adaptor signatures — Bitcoin Optech](https://bitcoinops.org/en/topics/adaptor-signatures/)
+- [Scriptless Scripts multi-hop locks (BlockstreamResearch)](https://github.com/BlockstreamResearch/scriptless-scripts/blob/master/md/multi-hop-locks.md)
+- [Multichain Taprootized Atomic Swaps (Distributed Lab, arXiv 2402.16735)](https://arxiv.org/abs/2402.16735)
+- [comit-network/xmr-btc-swap (adaptor-sig atomic swap reference)](https://github.com/comit-network/xmr-btc-swap)
+- [Taproot Assets Trustless Swap (Lightning Labs)](https://docs.lightning.engineering/the-lightning-network/taproot-assets/trustless-swap)
+- [Taproot Assets RFQ protocol](https://docs.lightning.engineering/lightning-network-tools/taproot-assets/rfq)
+- [Plonky2 SHA256 benchmarks](https://hackmd.io/@clientsideproving/Plonky2MobileBench)
+- [BIP-340 Schnorr signatures](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)
+- [BIP-341 Taproot](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)
+- [BIP-65 OP_CHECKLOCKTIMEVERIFY](https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki)
+
+---
+
+## 22. Change Log
+
+| Date | Change |
+| ---------- | ------ |
+| 2026-05-17 | Initial draft. |
+| 2026-05-17 | Consistency audit pass: add branch note at the top explaining that `SPEC.md` / `MIGRATION_RESEARCH.md` / `ROADMAP.md` currently live on `feat/plonky2-migration` only. |
+| 2026-05-17 | Audit round 2: restructure §9 from a stream-of-consciousness exploration of four candidate patterns to a single recommended construction (§9.2 mirror of Flow A) plus a brief §9.3 explaining why the alternatives were rejected. Promote §9.2 to the canonical Flow B; remove §9.3 (LN hold invoice) and §9.4 (renamed to §9.2) as numbered alternatives. Fix four broken internal cross-references (§10/§15 corrected to §12/§16). Renumber open-questions list to drop the gap left after removing the pattern-choice question. |
+| 2026-05-17 | Audit round 3: harmonise header structure across all three bridge docs (Status / Authoritative source / Audience / Branch note, in that order). Remove organisation-specific references ("DFX", a personal name) — replace with generic operator/issuer wording, consistent with the rest of the repo where the same convention is followed (`MIGRATION_RESEARCH.md` is the single exception with one such mention). Define `asth` / `ocr` at first use in §4.2. Tighten §17 heading. |
diff --git a/MIGRATION_RESEARCH.md b/MIGRATION_RESEARCH.md
new file mode 100644
index 00000000..c05d124a
--- /dev/null
+++ b/MIGRATION_RESEARCH.md
@@ -0,0 +1,1333 @@
+# Migration Research: References and Adoption Decisions
+
+Companion document to [`SPEC.md`](./SPEC.md). Summarises what we can take from the upstream references, and — more importantly — flags where our current implementation has diverged from the published Shielded CSV protocol. Read this before writing any Plonky2 code.
+
+> **Fresh session?** Start with [`CONTRIBUTING.md`](./CONTRIBUTING.md)
+> § "Working on the Plonky2 Migration" first for the project invariants
+> and reading order. This file's §7 (Lessons Learned) is the *required
+> reading before touching the affected code areas*.
+
+---
+
+## TL;DR
+
+1. **`BitVM/zkCoins` is a 182-LOC IVC toy, not a zkCoins prototype.** It gives us a Plonky2 version pin and a cyclic-recursion code recipe, nothing more.
+2. **The real normative reference is `ShieldedCSV/ShieldedCSV`** — a non-circuit Rust implementation of the paper's PCD predicate.
+3. **Our current SP1 implementation has departed from the published protocol in 11 distinct ways.** Some are simplifications (Schnorr commitment on a Taproot inscription instead of half-aggregate nullifier publication), some are arguably regressions (recipient is plaintext `Address`, linkable across coins), some are missing features (fee output, conditional-noop on reorg).
+4. **Decision point for the maintainers:** Are we implementing _Shielded CSV as published_, or are we shipping a zkCoins MVP that intentionally diverges? Both are defensible; we just need to pick before we re-implement the circuit in Plonky2, otherwise we lock in design choices that aren't reviewable against any spec.
+
+---
+
+## 1. `BitVM/zkCoins` Plonky2 Prototype
+
+**Location (local clone):** `~/Documents/GitHub/zkcoins/BitVM-zkCoins-reference/`
+**Upstream:** https://github.com/BitVM/zkCoins
+**Size:** 1 crate, 1 file, 182 LOC, 10 commits, last commit `bd8a8c2 "Recursive proving kinda works"` — WIP/abandoned.
+
+### What it is
+
+A Plonky2 IVC skeleton (`fn main()` with `println!` demos, no tests) that:
+- Pins `plonky2 = "0.2.0"`, `D = 2`, `PoseidonGoldilocksConfig`, `CircuitConfig::standard_recursion_config()`.
+- Uses `conditionally_verify_cyclic_proof_or_dummy` to verify two recursive proofs against the same circuit digest.
+- Has a placeholder `mul_add` payload (computes a running sum).
+- Demonstrates `add_verifier_data_public_inputs` for circuit-digest pinning.
+
+### What it isn't
+
+Despite the repo name, it contains **none** of: SMT, MMR, AccountState, Coin, ProofData, Schnorr verification, recipient model, Bitcoin link, tests, server, scanner. The single `main.rs` is a Plonky2 tutorial-grade IVC demo with no zkCoins semantics.
+
+### Adoption decisions
+
+| Aspect | Decision | Why |
+| --- | --- | --- |
+| `plonky2 = "0.2.0"` version pin | **Adopt** | Same version as the upstream `BitVM/zkCoins` reference; ecosystem-current. |
+| `PoseidonGoldilocksConfig`, `D = 2` | **Adopt** | Standard Plonky2 recursion setup. Matches SPEC §12.1. |
+| `standard_recursion_config()` | **Adopt as starting point** | Re-evaluate gate budget once we know our N-coin fanout. |
+| `common_data_for_recursion()` two-pass build pattern | **Adopt with adaptation** | Plonky2 idiom to stabilise public-input count under cyclic recursion. Need to extend to our (prev account proof + N coin proofs) fanout. |
+| `conditionally_verify_cyclic_proof_or_dummy` for Initial vs. Update branch | **Adapt** | Correct shape but only 2 verification slots in the demo; we need 1 + max_in_coins. |
+| `add_verifier_data_public_inputs` | **Adopt** | Direct realisation of SPEC §10's "same-circuit" assertion. |
+| Balance logic (commit `60e9d94`) | **Discard** | Toy `mul_add`, no relation to our model. |
+| Everything else | **Write from scratch, basing on our SP1 modules** | The reference doesn't have it. |
+
+**Bottom line:** the BitVM repo saves us maybe 20-30 lines of Plonky2 boilerplate. It does not give us the SMT, MMR, AccountState, or Coin logic for free — those have to be ported from our SP1 modules to Plonky2 constraints by hand.
+
+---
+
+## 2. Shielded CSV Paper (eprint 2025/068)
+
+### Sources used
+
+The eprint PDF returned HTTP 403 to automated fetches; instead the analysis relied on:
+- **`github.com/ShieldedCSV/ShieldedCSV`** — the **upstream reference implementation** of the PCD compliance predicate, by Nick/Eagen/Linus. This is the normative source.
+- Blockstream blog ("Bitcoin's Shielded CSV Protocol Explained")
+- Bitcoin Magazine technical article on Shielded CSV
+- Bitcoindev mailing-list summary
+- Independent analyses (Fairgate newsletter, eliel.nfinic.com)
+
+Items below cite **[REF-IMPL]** when the source is the upstream Rust code, **[SECONDARY]** when from blogs/list posts.
+
+### Protocol primitives the paper actually uses
+
+From `ShieldedCSV/ShieldedCSV/lib.rs`:
+
+```rust
+pub struct AggregateNullifier {
+ pub pks: Vec, // each pk = one account update
+ pub sig: Signature, // half-aggregate BIP-340 Schnorr
+ pub fee_acct_comm: Commitment, // hiding commitment to publisher's acct
+}
+
+pub struct CoinEssence {
+ pub address: Commitment, // HIDING commit(acct_id, rand) — not a plain Address
+ pub amount: u64,
+ pub idx: [u8; 2], // 2-byte coin index in tx
+ // FEE_IDX = [0xff, 0xff]
+}
+
+type CoinID = [u8; 34]; // tx_hash(32) || idx(2)
+type CoinIDOnChain = [u8; 8]; // blockchain_loc(6) || idx(2)
+ // 21 bits block height + 22 bits in-block idx
+```
+
+And from `primitives.rs`:
+
+- **`AccM` (strong A-SEC accumulator)** for spent coins, keyed by `CoinIDOnChain`, **lexicographically ordered = creation-order ordered**, supports `verify_non_membership_and_insert`. Order matters because it lets managers prune historical subtrees.
+- **`ToSAcc` (tuple-of-sets accumulator)** for the on-chain nullifier history, holding `(pk, sig_commitment, blockchain_location, fee_acct_comm)` tuples, supporting `append_set`, `prove_union_membership`, `prove_is_prefix`, `prove_distinct_element`.
+- `Commitment` (Pedersen-style, hiding+binding) wraps every recipient address with per-coin randomness for unlinkability.
+
+### Hash function and field choice
+
+The reference implementation leaves `hash` and `Commitment::commit` as **unimplemented stubs** — the paper is hash-agnostic, requires only CRH/RO behaviour for `hash` and hiding+binding for `Commitment`. Only BIP-340 Schnorr (secp256k1) is mandatory, because Bitcoin verifies it. **Conclusion:** Poseidon over Goldilocks is within the paper's allowed instantiation space; SHA256 was not normative either. ✓
+
+### Recursion
+
+Paper uses PCD (Proof-Carrying Data) as the abstraction — explicitly **agnostic between recursive SNARKs and folding schemes (Nova-style)**. No mandated recursion-depth bound. **Conclusion:** Plonky2 cyclic recursion is fine. ✓
+
+### Account model
+
+`AcctStateEssence { id: PublicKey, balance: u64, nullifier_pk: PublicKey }` — matches our `AccountState { owner, balance, public_key }` structurally, with two differences:
+
+- The paper's `id` is itself a `PublicKey` (XOnlyPublicKey), **not** `H(initial_pk)`. We added the extra hash; the paper doesn't.
+- Each `AcctState` carries both `spent_accum` (≈ our `coin_history_root`) **and** a claimed `nullifier_accum` snapshot — we carry only the former, which is one of the divergences below.
+
+---
+
+## 3. The 11 Divergences (Our SPEC vs. the Paper)
+
+Numbered D1–D11. Each is a concrete protocol-level departure. Some are deliberate MVP simplifications, some are accidental, some have security implications. We need to triage them explicitly.
+
+| # | Our SPEC says | Paper says | Severity |
+| --- | --- | --- | --- |
+| **D1** | `identifier = H(asth ‖ u32_be(idx))` (32 B), tied to sender's next account-state hash | `CoinID = tx_hash ‖ idx_2B` (34 B); `CoinIDOnChain = blockchain_loc(6 B) ‖ idx_2B` (8 B) for accumulator efficiency. | **Protocol-level**: paper IDs are short on purpose. |
+| **D2** | `Coin { recipient: Address = H(initial_pk) }` — plaintext recipient | `coin.essence.address = Commitment::commit(acct_id, rand)` — **hiding** commit, per-coin random. | **Privacy regression**: without `rand`, multiple coins to the same recipient are trivially linkable. |
+| **D3** | Single Schnorr commitment over `H(asth ‖ ocr)` posted as Taproot inscription, txid prefix `4242` | `AggregateNullifier` — **half-aggregate BIP-340 Schnorr** posted by third-party publishers, no inscription envelope mandate, no `H(asth ‖ ocr)` message. | **Architectural**: we replaced the paper's publisher layer with self-publishing. |
+| **D4** | Global state = SMT keyed by `H(pk)`, value `H(asth ‖ ocr)`; MMR over `H(smt_root ‖ prev_mmr_root)` | Global state = `ToSAcc` over `(pk, sig_comm, blockchain_loc, fee_acct_comm)` tuples, with prefix and union-membership proofs. | **Protocol-level**: coin proofs in the paper prefix-prove against `ToSAcc`; our SMT/MMR shape doesn't expose the prefix interface. |
+| **D5** | SMT depth 256, hash-keyed (uniform random) | `AccM` is lex-ordered by `CoinIDOnChain` — explicitly to enable pruning old subtrees. | **Scalability**: uniform hash-keyed SMT cannot prune. |
+| **D6** | No fee field; no fee output | `fee: u64` field; `FEE_IDX = 0xffff` reserved index; `payment_finalize_fee` mints exactly one coin to the publisher. | **Missing feature**: our circuit cannot produce a fee output. |
+| **D7** | No conditional-noop path | Paper supports `conditional_nav` — if the claimed nullifier-accum is no longer a prefix of the chain's, the tx becomes a no-op. | **Reorg safety**: our impl doesn't degrade gracefully under reorgs. |
+| **D8** | `Coin` doesn't carry a `nullifier_accum` snapshot | Paper's `Coin` carries the `nullifier_accum` it was minted under; receiver checks this is in their local nullifier-accum history. | **Soundness**: without this snapshot, recipients trust the proof's history-root rather than verifying it independently. |
+| **D9** | No range/uniqueness checks on `coin_index` | `idx` is strictly increasing within a tx; `idx == FEE_IDX` reserved. | **Soundness**: malformed coins not rejected. |
+| **D10** | `apply_coin` checks `coin.recipient == self.owner` against plaintext owner | Paper opens `Commitment::commit(acct_id, rand)` with per-coin `acct_comm_rand` provided as witness. | **Tied to D2**: same hiding-commit issue. |
+| **D11** | `MINTING_ADDRESS` hard-coded; one allowed minter | Paper has explicit `issuance(IssuanceProof)` predicate branch (currently stub in upstream); `payment_init_newacct` starts fresh accounts at `balance = 0, nullifier_pk = acct_id`. | **Architectural**: the minting model is left more open in the paper. |
+
+### Triage recommendation
+
+For a Plonky2 MVP shipping in weeks-not-months:
+
+- **Keep as deliberate simplifications (document in README + this file):** D1, D3, D5, D6, D11. These trade flexibility for shipping speed; explicitly call them out so reviewers know.
+- **Should-fix before mainnet:** D2 + D10 (privacy regression — recipient unlinkability is a stated zkCoins selling point), D7 (reorg safety — Bitcoin reorgs happen), D8 (soundness — receivers should be able to verify coin age locally).
+- **Open / discuss with the maintainers:** D4 (does the SMT+MMR scanner model actually give the same security properties as `ToSAcc` for our threat model?), D9 (cheap to add).
+
+---
+
+## 4. Combined Adoption Decisions
+
+### From `BitVM/zkCoins`
+- Cargo manifest: `plonky2 = "0.2.0"`, no other deps from there.
+- IVC scaffolding: `common_data_for_recursion`, `conditionally_verify_cyclic_proof_or_dummy`, `add_verifier_data_public_inputs`.
+- Public-input-count stabilisation pattern (the two-pass `builder.print_gate_counts(0)` / build / discard / re-build trick).
+
+### From `ShieldedCSV/ShieldedCSV` (paper reference impl)
+- **Data-type shapes** for `CoinEssence`, `AcctStateEssence`, `AggregateNullifier`. Even if we stick with our simpler publisher model (D3), align field names + types so cross-reading is possible.
+- The `verify_non_membership_and_insert` accumulator API as the canonical SMT operation signature.
+- The PCD predicate as the canonical list of asserts. Even if our circuit is structurally different, the **set of facts proven** should be a superset.
+- The `payment_init_newacct` flow as the basis for a real (non-hard-coded) account-creation path (addresses D11 long-term).
+- Test cases: copy/port their predicate tests as a soundness baseline.
+
+### From our existing SP1 code (`program/src/`)
+- The current `SparseMerkleTree` / `MerkleMountainRange` algorithms (modulo hash swap to Poseidon and lex-ordering for AccM if we go that route).
+- The `AccountState`, `Coin`, `Invoice` data shapes (modulo D1, D2 fixes).
+- The Account → coin_queue → send flow in `node/src/account_node.rs` — this is host-side glue, no circuit changes here except wiring to the new Plonky2 prover.
+- The 12 tests in `program/src/merkle/sparse_merkle_tree.rs::tests` — survive as-is once `hash_concat` is Poseidon-backed.
+
+### Newly required work (no upstream donor)
+- Plonky2 circuit gadgets for: Poseidon-SMT membership/non-membership/insert, Poseidon-MMR append+prove, Schnorr verification or — if we keep BIP-340 — an in-circuit SHA256 gadget over the Schnorr message (cheap because the message is exactly 64 bytes).
+- Range checks on coin indices, balances (u64), and amounts.
+- Domain-separation tags as field-element prefixes for leaf/node/identifier/MMR-leaf hashes (cheap with Poseidon, fixes the implicit-tagging issue called out in SPEC §10.5).
+- Fixed-shape padding for variable-length input vectors (`in_coins` becomes `[Coin; MAX_IN_COINS]` with no-op slots).
+
+---
+
+## 5. Design Decisions (locked for v1)
+
+The following decisions are taken. Each is reversible but reversing them means a full circuit rebuild — they will not be re-litigated within v1.
+
+1. **Paper-fidelity vs. zkCoins variant** → **zkCoins MVP variant for v1.** Paper fidelity (`ToSAcc`, half-aggregate publishers, fee economics, hiding recipient commitments) is deferred to v2. SPEC.md §15 documents the divergences D1–D11.
+
+2. **Max input coins per send** → **8.** Plonky2 circuits are fixed-shape; the bound has to be a constant. 8 covers >99% of real wallet sends (most are 1–2 in-coins). Coin slots beyond the actual count are filled with `amount = 0` dummies; the circuit treats those as no-ops.
+
+3. **Hash function** → **Poseidon over Goldilocks (`PoseidonGoldilocksConfig`, `D = 2`)** everywhere in the protocol's Merkle structures — both in-circuit *and* in the scanner state (SMT + MMR). Aligns with the Plonky2 ecosystem default and the BitVM reference config.
+
+4. **Schnorr message hash** → **BIP-340 secp256k1 stays unchanged.** The wallet signs `SHA256(serialize(asth) ‖ serialize(ocr))` where `asth` and `ocr` are 4-element Poseidon outputs serialised big-endian to 32 bytes each. SHA256 lives only at this boundary; everything inside the circuit is Poseidon. No in-circuit SHA256 gadget is needed because the circuit never verifies the BIP-340 signature itself — that happens off-circuit in the scanner.
+
+5. **Privacy (D2/D10)** → **deferred to v2.** Plaintext recipient addresses for v1. Linkability across multiple coins to the same recipient is a known limitation, called out as a mainnet blocker in SPEC §15.
+
+6. **Fee model (D6)** → **no fee in v1.** We are the publisher (DFX/zkCoins-operated server), so there is no publisher to compensate. Self-funded operation.
+
+Hash-function boundary visualisation:
+
+```
+ in-circuit (Poseidon) off-circuit (BIP-340 secp256k1)
+ ---------------------- --------------------------------
+ ProofData wallet derives x-only privkey
+ ┌──────────┐ wallet computes
+ │ asth │ ────────┐ msg = SHA256(asth_bytes || ocr_bytes)
+ │ ocr │ ────────┼──→ sig = schnorr_sign(privkey, msg)
+ └──────────┘ │ scanner verifies sig
+ │ scanner inserts (pk, msg) into Poseidon-SMT
+ └─── serialize each field elt big-endian → 32 B
+```
+
+---
+
+## 6. Sequencing — moved to ROADMAP.md
+
+The original 9-step strategic outline that lived here was superseded by
+the detailed 16-row breakdown in [`ROADMAP.md`](./ROADMAP.md) once
+implementation started. The ROADMAP is now authoritative for the
+execution plan (status, effort, files, risks).
+
+Key adjustments made since the original outline:
+
+- **Step ordering of gadgets** (was: hash → SMT non-inclusion+insert → MMR-append → SHA256). Actual: MMR inclusion → SMT inclusion → SMT non-inclusion verify. The original list mentioned an MMR-append and a SHA256 gadget which turned out to not be needed (MMR is built off-circuit by the scanner; SHA256 lives at the Bitcoin-signing boundary, not in-circuit — see §5.4).
+- **No Cargo feature flag for dual backend.** The closed-test-environment decision means step 7 replaces SP1 with Plonky2 outright (see ROADMAP step 7).
+- **Server scanner + state DO change** (Poseidon SMT/MMR, not SHA256). Only the on-chain commitment *format* — a single Schnorr inscription with txid prefix `4242` — stays unchanged.
+
+---
+
+## 7. Lessons Learned (during implementation)
+
+Gotchas, design discoveries, and "would have been nice to know" findings
+that emerged while porting steps 1–4d. Each entry includes what it
+costs (concrete: a regression test, a comment, a constraint) so a later
+contributor can verify the lesson is still load-bearing.
+
+### 7.1 Poseidon zero-state collision in SMT defaults — **HIGH severity**
+
+**Discovered:** SMT port (commit `6215009`), failing test
+`test_verify_non_inclusion_proofs` at iter=1 (2 leaves).
+
+**Symptom:** `debug_assert!(node_1 == *parent || node_0 == *parent)` in
+the chase loop of `generate_non_inclusion_proof` failed. Investigation
+showed the chase had silently diverged from the inserted leaf's path
+because *both* children at some level appeared equal to `parent`.
+
+**Root cause:** Plonky2's Poseidon sponge with state width 12 and zero
+capacity init has the property that
+`PoseidonHash::hash_no_pad(&[F::ZERO])`,
+`PoseidonHash::hash_no_pad(&[F::ZERO, F::ZERO])`,
+`PoseidonHash::two_to_one(ZERO_HASH, ZERO_HASH)`, and any other
+absorption that leaves the state at all-zeros before permutation all
+produce **the same output** — call it `Z = Poseidon(0)`.
+
+If `DEFAULT_HASHES[TREE_DEPTH] = ZERO_HASH`, then `DEFAULT_HASHES[L]`
+for every `L < TREE_DEPTH` is `Z` (after sufficient self-concatenation,
+this stabilises in two steps). Any leaf whose value+key are themselves
+hashes of zero-derived inputs (very common in tests, but also possible
+for real Poseidon-derived keys hitting that exact image) collides with
+`DEFAULT_HASHES[TREE_DEPTH - 1]`. The chase loop then sees both default
+sibling and propagated leaf-hash as equal and picks the wrong path.
+
+**Fix:** seed `DEFAULT_HASHES[TREE_DEPTH]` with a domain-separated
+non-zero value (verbatim from `program-plonky2/src/merkle/sparse_merkle_tree.rs`):
+
+```rust
+const EMPTY_LEAF_TAG: &[u8] = b"zkcoins:smt:empty-leaf:v1";
+
+pub static DEFAULT_HASHES: LazyLock> = LazyLock::new(|| {
+ let depth = TREE_DEPTH;
+ let empty_leaf = hash_bytes(EMPTY_LEAF_TAG);
+ let mut default_hashes = vec![empty_leaf; depth + 1];
+ for level in (0..depth).rev() {
+ default_hashes[level] = hash_concat(&default_hashes[level + 1], &default_hashes[level + 1]);
+ }
+ default_hashes
+});
+```
+
+**Regression guard:** `leaf_hash_never_collides_with_defaults` in
+`sparse_merkle_tree.rs` iterates 50 sample keys × values and asserts
+none collides with any `DEFAULT_HASHES[L]`.
+
+**Generalisation for future gadgets:** any time the protocol uses
+"zero" as a sentinel inside a Poseidon hash chain, sanity-check that
+the resulting sentinel isn't also a natural image of zero-derived
+input. Domain separators are cheap insurance.
+
+### 7.2 Variable vs. fixed depth in SMT proofs — **MEDIUM severity, decision pending**
+
+**Discovered:** when porting `verify_smt_non_inclusion` and writing
+`verify_and_insert` plans (steps 4c, 4c+).
+
+**Tension:** the off-circuit SMT uses **path compression**. A single-leaf
+subtree at level L stores `leaf_hash` rather than a real `hash_concat`
+of children, and `generate_inclusion_proof` / `generate_non_inclusion_proof`
+break early when they detect this pattern. The resulting proof has
+variable length `K ≤ TREE_DEPTH`.
+
+Plonky2 circuits are **fixed-shape**: a gadget that processes a path
+must commit to its length at circuit-build time. The current gadgets
+accept any `path.len()` at *test* time, but the monolithic circuit
+(step 5) needs one fixed depth.
+
+**Two options for step 5:**
+
+1. **Remove path compression off-circuit.** Every leaf path is hashed
+ up the full TREE_DEPTH; proofs are uniformly TREE_DEPTH siblings
+ long. Pros: trivial in-circuit logic; uniform. Cons: changes
+ `tree.root()` semantics (root is no longer leaf-hash for single-leaf
+ trees); we'd need to retrofit the test suite and any host code
+ reading the root.
+2. **Keep path compression off-circuit, pre-pad for circuit consumption.**
+ The host produces a "padded" proof of length TREE_DEPTH where
+ levels below path compression are filled with computed
+ `hash_concat(leaf_h, default)` values at each level. Pros: keeps
+ off-circuit `tree.root()` semantics. Cons: host code complexity;
+ the padding must be computed correctly (subtle).
+
+**Status:** unresolved. Decision deferred to step 5 (monolithic circuit).
+The risk register R6 flags this; the ROADMAP's 4c+ entry notes the plan
+is option 2 unless we hit issues.
+
+**Concrete cost so far:** the verify gadget accepts variable depth and
+works for tests, but the insert gadget hasn't been written yet
+precisely because the depth question is unsettled.
+
+### 7.3 `pw.set_target` returns `Result` in plonky2 1.x — **LOW severity**
+
+**Discovered:** smoke test for `program-plonky2/src/lib.rs` (commit
+`984580f`).
+
+**Surprise:** the BitVM reference uses plonky2 0.2.0 where
+`pw.set_target(target, value)` returns `()`. In plonky2 1.x it returns
+`Result<(), anyhow::Error>` and clippy's `unused_must_use` rejects the
+old call shape.
+
+**Fix:** always `.unwrap()` (or properly handle) the result. The error
+case shouldn't fire in correctly-written code; the Result is there for
+target-overwrite detection.
+
+```rust
+// 0.2.0: pw.set_target(t, v);
+// 1.x: pw.set_target(t, v).unwrap();
+```
+
+### 7.4 Field-element packing conventions (canonical-reduction safety) — **MEDIUM, codified**
+
+**Discovered:** during `hash.rs` design.
+
+**Constraint:** Goldilocks modulus is `p = 2^64 - 2^32 + 1 ≈ 2^64`. A
+u64 value just below `2^64` exceeds `p` and `F::from_canonical_u64`
+panics in debug builds (release: silent reduction).
+
+**Packing rules** used throughout this crate:
+
+| Operation | Bytes per field elt | Why |
+| ---------------------------------- | ------------------- | ------------------------------- |
+| `hash_bytes` | **7** (LE) | 7*8 = 56 bits, safe ceiling. |
+| `digest_to_bytes` / `from_bytes` | **8** (BE) | Only works because Poseidon outputs are canonical (< p). Asserted by the protocol invariant; if a user-supplied byte string is fed through `digest_from_bytes`, it MUST come from a prior `digest_to_bytes` of a real digest. |
+| `u64_to_limbs` (balance / amount) | **4** (2 limbs) | u32 chunks, never exceeds p. |
+| `pubkey_to_limbs` (33-byte pubkey) | **7** (5 limbs LE) | Same as `hash_bytes`. |
+
+**Invariant to enforce in any future packing function:** input chunks
+that fill a Goldilocks element must be ≤ 56 bits unless the value's
+canonical reduction is independently guaranteed.
+
+### 7.5 The Schnorr / Poseidon boundary lives at byte serialisation — **codified**
+
+**Discovered:** §5.4 decision, then refined while writing
+`CommitmentMerkleProofs::verify_commitment`.
+
+**Rule:** the wallet signs `SHA256(serialize(asth) ‖ serialize(ocr))`
+where `serialize` is `digest_to_bytes` (32 bytes big-endian per field
+element). The scanner verifies the BIP-340 signature and then inserts
+the 32-byte message into the global SMT keyed by `H(serialize(pubkey))`
+(Poseidon hash of compressed pubkey bytes, then taken as a 32-byte
+SMT key).
+
+There is **no in-circuit SHA256**, **no in-circuit Schnorr verify**.
+The boundary is enforced entirely off-circuit, and the proof's public
+output (`ProofData`'s `account_state_hash` + `output_coins_root`)
+provides the values that the wallet signs.
+
+**Consequence for D2/D10 fix (privacy):** if we later add hiding
+recipient commitments, the commitment construction lives off-circuit
+too. The wallet computes `Commitment::commit(acct_id, rand)` and the
+randomness is a regular witness — no in-circuit Pedersen needed unless
+we're verifying commitment openings inside the predicate.
+
+### 7.6 Tests serialised, memory-resident binaries linger — **LOW, but operationally costly**
+
+**Discovered:** orphan `server-f8087395d1b79585` process consuming 35 GB
+of swap reservation hours after `cargo test` finished.
+
+**Cause:** when a background `cargo test` is aborted (or completes but
+its child test binary doesn't terminate cleanly), the test binary
+keeps its allocated arenas in memory and shows up as a giant resident
+process in Activity Monitor.
+
+**Mitigation:** see `program-plonky2/CONTRIBUTING.md` § "Test runtime
+characteristics" and the `feedback_cleanup_test_binaries` memory entry.
+After long test runs:
+
+```bash
+pgrep -f "target/debug/deps/zkcoins_program_plonky2"
+# If any output: kill -TERM
+```
+
+### 7.7 `gh` needs `--repo` in background tasks — **LOW, operational**
+
+**Discovered:** while running a CI watcher via `Bash` with
+`run_in_background: true`. Background processes lose cwd-read
+permission in this sandbox, so `cd ... && gh ...` fails with "Unable
+to read current working directory: Operation not permitted".
+
+**Mitigation:** always pass `--repo zk-coins/node` explicitly to gh
+commands run in background contexts. Captured in memory as
+`feedback_ci_monitor_after_push`.
+
+### 7.8 Reference repos: BitVM/zkCoins is a 182-LOC toy, ShieldedCSV/ShieldedCSV is the real one — **codified**
+
+**Re-stated for emphasis:** the upstream `BitVM/zkCoins` reference
+repo is a Plonky2 IVC scaffold (182 LOC, no SMT/MMR/AccountState/Coin/
+Schnorr/tests). The actual normative reference implementation is
+`github.com/ShieldedCSV/ShieldedCSV`. Our implementation diverges from
+the paper in 11 ways (see §3 of this doc / SPEC.md §15).
+
+§3 is authoritative for "what does the paper say"; §3's divergence
+table D1–D11 is authoritative for "where do we differ and why".
+
+### 7.9 Defensive bounds checks collapse coverage regions — **codified**
+
+**Discovered:** while pushing `program-plonky2` from 96.43% to 100%
+line coverage (commit `e14d9df`).
+
+**Symptom:** the MMR's `append` and `get_proof` had explicit
+`if 2*idx+1 < len { levels[level][2*idx+1] } else { ZERO_HASH }`
+defensive branches. The `else` arm is unreachable in correctly-
+maintained state (the capacity-doubling guarantees `len` is always a
+power of two ≥ `2*idx+2`), but llvm-cov sees it as an uncovered
+region — perpetually below 100%.
+
+**Fix:** rewrite as
+`self.levels[level].get(idx).copied().unwrap_or(ZERO_HASH)`.
+
+`Option::unwrap_or` is hashed as a single region by llvm-cov — the
+"unreachable" path shares the region of the success path. The safety
+fallback is preserved (`ZERO_HASH` returned if `get` ever fires the
+`None`), but the branch no longer carries its own coverage debt.
+
+**Generalisation for future code:** when you have a defensive
+`if in_bounds { container[i] } else { sentinel }` pattern, prefer
+`container.get(i).copied().unwrap_or(sentinel)`. The semantics are
+identical and the coverage shape is cleaner.
+
+### 7.10 Coverage-on-tests: annotate `#[cfg(test)] mod tests` with `coverage(off)` — **codified**
+
+**Discovered:** same context as 7.9. After closing all genuine
+production-side coverage gaps, the crate still measured ~99% lines
+because llvm-cov tracks the panic-message-evaluation region inside
+`assert!(cond, "msg")`, `assert_eq!`, `assert_ne!`, `should_panic`
+macros as a separate region from the success path. Inside a passing
+test the `"msg"` region is never executed, so it counts as uncovered.
+
+**Fix:** add `#[cfg_attr(coverage_nightly, coverage(off))]` to every
+test module (i.e. every `#[cfg(test)] mod tests { … }`). This requires
+two prerequisites:
+
+1. `src/lib.rs` declares the feature gate:
+ `#![cfg_attr(coverage_nightly, feature(coverage_attribute))]`.
+ The crate must be built on a nightly toolchain that supports the
+ `coverage_attribute` feature (we're on `nightly-2025-04-15`).
+2. `Cargo.toml` registers the cfg key so the compiler doesn't warn
+ when building outside the coverage tool:
+
+ ```toml
+ [lints.rust]
+ unexpected_cfgs = { level = "warn", check-cfg = ["cfg(coverage_nightly)"] }
+ ```
+
+The `coverage_nightly` cfg is set automatically by `cargo-llvm-cov`
+when it instruments the build; in normal `cargo build` / `cargo test`
+runs the attribute is a no-op.
+
+**Generalisation:** test modules SHOULD always carry the
+`coverage(off)` annotation in this codebase; production module-level
+docs should not need it. New modules added in the future must include
+this annotation if they ship a `#[cfg(test)] mod tests` block — see
+`program-plonky2/CONTRIBUTING.md` § "Coverage gate" for the rule.
+
+### 7.11 Hardware target is a Mac Studio M3 Ultra, single host — **codified**
+
+**Discovered:** explicit architecture decision (commit `79bd39e`,
+clarified shortly after).
+
+**Constraint:** zkCoins runs on a single Mac Studio M3 Ultra (96 GB
+unified RAM). On-box compute includes Performance + Efficiency cores,
+the integrated Apple Silicon GPU (reachable via Metal), Neural Engine,
+and AMX. **External** hardware (NVIDIA, CUDA, GPU farms) and external
+cloud proving services (Succinct Prover Network, AWS GPU, Lambda Labs)
+are **not** available. If a design overshoots the performance budget,
+the design changes; we do not add external hardware.
+
+**Important caveat about "GPU":** the M3 Ultra has a substantial
+integrated GPU (60- or 80-core depending on bin) usable via Metal.
+That GPU is on-box and would be fair game *if our prover library
+supported it*. Plonky2 currently ships only CPU and CUDA backends —
+no Metal — so the GPU sits idle for proving. This is a library
+property, not a constraint we imposed. If a Plonky2 Metal backend
+becomes available (or we port to Plonky3 which has more options), we
+may use the GPU.
+
+**Implications for design choices made earlier in this document:**
+
+- §5.3 (Hash function): Poseidon-Goldilocks performance must be
+ acceptable on the M3 Ultra. Today that's CPU performance, since
+ Plonky2 has no Metal backend.
+- §5.4 (Schnorr boundary): unchanged — boundary lives at byte
+ serialisation, no in-circuit secp256k1.
+- §6 sequencing: step 9's performance budget (`ROADMAP.md` step 9) is
+ explicitly M3-Ultra-warm-proof ≤ 5 s, ideal ≤ 1 s, memory peak
+ < 64 GB. If missed, knobs are design-level (reduce `MAX_IN_COINS`,
+ drop in-coin recursion, switch to folding) — never external hardware.
+
+**Implication for the Plonky3 post-MVP path** (`ROADMAP.md`):
+BabyBear's GPU-friendliness in the broader literature usually means
+CUDA-friendliness, which doesn't help us on Apple Silicon. The
+motivation for switching to Plonky3 reduces to "matches SP1-era field
+choice / Plonky3-native ecosystem". A separate question is whether
+Plonky3's GPU paths might include Metal — if so, that would change
+the calculation.
+
+### 7.12 BitVM's `common_data_for_recursion` is broken under Plonky2 1.1.0 — **codified**
+
+**Discovered:** building the stage-5a cyclic-recursion PoC (commit
+`83fa0c1`).
+
+**Symptom:** copying BitVM/zkCoins's `common_data_for_recursion`
+verbatim into `circuit/main.rs` and calling
+`builder.build::()` on the outer cyclic circuit panics with
+`Failed to build circuit` at `plonky2/src/plonk/circuit_builder.rs:1067`.
+No useful error message; the panic comes from a shape-mismatch deep
+in the verifier-data wiring.
+
+**Root cause:** BitVM is pinned to **Plonky2 0.2.0**. In that version
+the canonical `common_data_for_recursion` is **two `verify_proof`
+calls in pass 2 and three in pass 3, plus a `ConstantGate` added to
+the gate set**. Plonky2 1.1.0's
+`conditionally_verify_cyclic_proof_or_dummy` produces a different
+gate set and public-input shape, so the BitVM-shaped common-data is
+no longer a fixed point. The library's outer build then rejects the
+mismatch.
+
+**Fix:** port Plonky2 1.1.0's own canonical
+`recursion::cyclic_recursion::tests::common_data_for_recursion`
+verbatim — **one `verify_proof` call per pass plus `NoopGate`
+padding to `1 << 12` gates**. See
+`program-plonky2/src/circuit/main.rs::common_data_for_recursion_c`
+for the working implementation with full source comments.
+
+**Why we keep both versions in mind:** if anyone later restores
+BitVM's three-pass shape (e.g., on the theory that "more verifies =
+more robust"), the build will fail again. The 1.1.0 canonical shape
+is the only one that works with 1.1.0's `conditionally_verify_*`
+machinery; this is not a stylistic preference.
+
+**Ordering subtlety:** the BitVM reference order is
+`add_virtual_public_input` → `add_verifier_data_public_inputs` →
+`common_data_for_recursion` → `common_data.num_public_inputs = …`.
+Plonky2 1.1.0's own canonical test orders it
+`add_virtual_public_input` → `common_data_for_recursion` →
+`add_verifier_data_public_inputs` → `common_data.num_public_inputs = …`
+instead. The `common_data_for_recursion` function is stateless w.r.t.
+the outer builder, so logically the order shouldn't matter — but
+match the canonical order to avoid surprises.
+
+### 7.13 Coverage debt from unreachable Plonky2 `Result<()>` calls — **codified**
+
+**Discovered:** stage-5a (`83fa0c1`) initial draft used `?` to
+propagate the `Result` of
+`conditionally_verify_cyclic_proof_or_dummy`. `cargo llvm-cov` flagged
+the `Err` arm as uncovered, dropping line coverage below the 100 %
+gate.
+
+**The pattern:** Plonky2 library functions like
+`conditionally_verify_cyclic_proof_or_dummy`,
+`pw.set_target`, `pw.set_proof_with_pis_target`,
+`pw.set_verifier_data_target` all return `Result<…>` even though, in
+correct usage, they only return `Err` under invariants we control by
+construction (e.g., "common_data well-formed", "target not already
+set"). These are unreachable error paths in our code, but `llvm-cov`
+counts the branch.
+
+**Fix recipe — analogous to §7.9 (Option-based defensive checks):**
+- For functions that exist only for error propagation (like
+ `build_cyclic_circuit`), make the function infallible by `.expect`-ing
+ the unreachable `Err` and dropping `Result<…>` from the signature.
+ The `expect` message documents the invariant that makes `Err` impossible.
+- For witness-population calls inside helpers that already return
+ `Result<…>` for other reasons (e.g. `data.prove`), keep `.unwrap()`
+ inline; the surrounding `Result` covers the rest of the contract.
+
+**Why this is *not* a fallback** (per `feedback_no_fallbacks`):
+`.expect` doesn't replace bad output with default output — it
+*panics* if the invariant ever breaks. The function's contract is
+"this never returns Err under our usage"; making that explicit via
+`.expect("…")` is documentation, not silent recovery. If the
+invariant later breaks (e.g., library API changes), tests will catch
+it via the panic, not a wrong-result soft failure.
+
+**Residual region not covered:** the `.expect` itself still produces
+one llvm-cov region for the panic branch (the `.unwrap_or_else(panic)`
+expansion). That's 1 missed region per call. For the line-based MVP
+gate (`cargo llvm-cov --fail-under-lines 100`) this is fine; for the
+region-coverage stretch it's the unavoidable cost of unreachable
+defensive paths in `Result`-returning library APIs.
+
+### 7.14 Path-compressed SMTs are incompatible with cyclic recursion — **codified**
+
+**Discovered:** stage-5c+ work in progress. The SMT shipped in
+`6cf949c` used path compression — a single-leaf subtree at level *K*
+had its level-*K* root equal to the leaf hash directly (no hashing
+through default siblings down to depth `TREE_DEPTH`). Off-circuit
+proofs had variable length *K* ≤ 256.
+
+**Why it broke:** Plonky2 cyclic recursion requires a stable
+`circuit_digest` across builds. The verifier shape — including the
+number of hash levels processed by the SMT-inclusion gadget — must
+be fixed at build time. Variable-length proofs would have produced
+a circuit with `circuit_digest` depending on proof shape, breaking
+the recursion fixed-point.
+
+**Fix:** rewrite the off-circuit SMT to produce always-`TREE_DEPTH`
+sibling proofs (`refactor: SMT to uncompressed fixed-256-depth
+paths`). Empty subtrees contribute `DEFAULT_HASHES[level + 1]`
+siblings, so the on-the-wire proof is 256 × 32 B = 8 KiB regardless
+of sparsity. The off-circuit `insert` removes the `current != leaf_h
+&& sibling == default → skip hash` short-circuit. Case A/B logic in
+`NonInclusionProof` is gone too — non-inclusion is now a proof that
+the depth-256 slot holds `DEFAULT_HASHES[TREE_DEPTH]`, full stop.
+
+**Operational consequence:** roots produced by the new `insert`
+differ from the pre-refactor compressed roots. The closed-test-env
+strategy (`feedback_zkcoins_closed_test_env`) makes this a free
+choice — no on-the-wire compatibility to preserve.
+
+**Lesson for future merkle structures:** if a structure will be
+verified inside a cyclic-recursive circuit, build the off-circuit
+proof generator to emit *fixed-shape* proofs from day one. Path
+compression and similar size-saving tricks save bytes off-chain but
+cost a redesign once you need ZK over the same data.
+
+### 7.15 Conditional constraints via `select_hash` masking — **codified**
+
+**Discovered:** stage-5c+ added SPEC §8 (c)(d)(e) checks that fire
+only on the AccountUpdate branch (`condition = true`). The
+`verify_smt_inclusion` / `verify_mmr_inclusion` gadgets internally do
+`connect_hashes(computed, expected_root)`, which is unconditional —
+they cannot be "switched off" by a guard.
+
+**Fix recipe:** expose a "compute-only" variant of each verify
+gadget (`smt_inclusion_root`, `mmr_inclusion_root`) that returns the
+reconstructed root *without* asserting equality. The caller then
+constructs the masked target via
+
+```rust
+let target = select_hash(builder, condition, expected_witness, computed);
+builder.connect_hashes(computed, target);
+```
+
+When `condition = false`, `select_hash` collapses to `computed` and
+the resulting constraint `connect_hashes(computed, computed)` is
+trivially satisfied. When `condition = true`, `target = expected_witness`
+and the honest check fires.
+
+**Why not skip-via-builder-condition:** Plonky2's `CircuitBuilder`
+doesn't have a "conditional region" primitive — every gate fires.
+Masking via `select` over the *target value* is the standard pattern
+(used by Plonky2's own `conditionally_verify_cyclic_proof_or_dummy`,
+the cyclic recursion machinery, etc.).
+
+**Witness-population implication:** the masked-off branch still needs
+*some* witness in the placeholders. Stage-5c+ uses a `dummy_cmp()`
+helper that constructs a syntactically valid but semantically empty
+`CommitmentMerkleProofs` (all `ZERO_HASH`, all-zero indices). The
+masked equality constraints accept any witness when `condition = false`.
+
+### 7.16 MMR root_extended / extend_to for fixed-depth verification — **codified**
+
+**Discovered:** stage-5c+ needed the in-circuit MMR-inclusion gadget
+to run at a fixed depth (`MMR_PROOF_PATH_LEN = MMR_MAX_DEPTH - 1 = 31`),
+but the off-circuit `MerkleMountainRange` uses capacity-doubling and
+produces variable-depth proofs (typically much shorter — `log2(N)`
+for a tree with `N` leaves).
+
+**Fix:** keep the MMR's natural shape (capacity doubles on demand)
+but add two helpers:
+- `MerkleMountainRange::root_extended(target_path_len)` — start from
+ the natural root, then walk up additional levels of
+ `hash_concat(current, ZERO_HASH)` until the path reaches
+ `target_path_len`. This is what the in-circuit gadget compares
+ against.
+- `MMRProof::extend_to(target_path_len)` — pad the proof's
+ `path` with `ZERO_HASH` siblings to `target_path_len`. The padded
+ proof verifies against `root_extended(target_path_len)`.
+
+The MMR root committed at the protocol boundary (e.g. inside
+`ProofData::commitment_history_root`) is always the extended root at
+the chosen `MMR_MAX_DEPTH`; everyone — off-circuit MMR users and the
+in-circuit verifier — agrees on the same value.
+
+**Why this beats redesigning the MMR:** the off-circuit MMR's
+capacity-doubling shape is convenient for incremental appends
+(O(log N) updates). A fixed-shape rewrite would re-allocate the full
+tree up front. The `_extended` / `extend_to` helpers preserve the
+fast off-circuit path while making the value the in-circuit verifier
+needs trivially derivable.
+
+### 7.17 Per-slot `active`-bit masking for variable-count loops — **codified**
+
+**Discovered:** stage-5d needed to support a per-account state
+transition processing 0..`MAX_IN_COINS` input coins, but the circuit
+shape must be fixed (otherwise `circuit_digest` changes per
+transaction → cyclic recursion breaks).
+
+**Pattern:** declare a constant `MAX_IN_COINS` slot count at the
+circuit-builder level. Each slot reserves witness targets including
+an `active: BoolTarget`. The slot's predicate is wrapped so that
+`active = false` makes every constraint trivially satisfied:
+
+- Equality / hash-match checks: `connect_hashes(computed, select_hash(active, expected, computed))`.
+- Value-update accumulators: `running = select_hash(active, new_value, running)`.
+
+This is the same `select_hash` masking pattern from §7.15, scaled
+out across a fixed list of slots. The off-circuit prover decides at
+runtime how many slots are active — the unused ones get a dummy
+witness (zeroed coin id, zero-filled proof path) that the masked
+constraints accept.
+
+**Caller ergonomics:** for the common case where all slots are
+inactive (e.g. Init proofs without in-coins), provide a thin wrapper
+`prove_*(args)` that delegates to the explicit
+`prove_*_with_in_coins(args, &inactive_dummies)`. The explicit
+variant remains available for tests and callers that need to control
+slot activity directly.
+
+**Performance cost:** each masked slot adds the *full* gate count of
+the underlying predicate (the masking doesn't save gates — it only
+makes the result vacuously satisfied). For stage 5d's SMT
+non-inclusion + insert this is ~512 Poseidon hashes per slot at
+`TREE_DEPTH = 256`. Bumping `MAX_IN_COINS` from 1 to 8 grows the
+circuit by ~3500 hashes — measure before committing to a target.
+
+### 7.18 `add_virtual_target` requires explicit witnessing; prefer `split_le` — **codified**
+
+**Discovered:** stage-5d-next initially implemented the balance
+overflow check by declaring `new_lo`, `new_hi`, `carry`, `overflow`
+as `add_virtual_target()` / `add_virtual_bool_target_safe()`
+targets, range-checking them, and `connect()`ing the recomposed
+value to the precomputed `sum`. The test failed at proof generation
+with `22 generators weren't run` — Plonky2 had no way to fill the
+virtual targets.
+
+**Root cause:** `add_virtual_*` reserves a witness slot but does NOT
+attach a generator. The prover must explicitly populate every
+virtual target via `pw.set_target` / `pw.set_bool_target`. If the
+target's value is determined by other witnesses, the prover would
+have to recompute it off-circuit and supply it manually — fragile
+and error-prone.
+
+**Fix:** use `builder.split_le(t, n_bits)`. It internally adds a
+`BaseSumGate` whose generator decomposes `t` into `n_bits` bits at
+prove time, and constrains each bit to be `{0, 1}` plus the
+recomposition `t == Σ bit[i] * 2^i`. The bits come back as
+`BoolTarget`s the caller can use, but no explicit witnessing is
+needed — given `t`, the bits are uniquely determined.
+
+For the balance check, `sum_lo ∈ [0, 2^33)` decomposes into 33 bits;
+`bits[32]` is the carry; `new_lo = sum_lo - 2^32 * carry` is the
+low 32 bits and stays in range by construction. Same pattern for
+the hi limb with an `assert_zero(overflow)` at the top.
+
+**Rule of thumb:** if a target's value is *uniquely determined* by
+other targets (low/high decomposition, range checks, comparisons),
+look for a Plonky2 gate that ships its own generator
+(`split_le`, `range_check`, `add_many`, `arithmetic` family).
+Reserve `add_virtual_*` for prover-driven witnesses (e.g. real
+secret-key inputs, side channels, off-circuit results that you must
+trust the prover for).
+
+### 7.19 `account_state.hash` lifecycle inside a transition — **codified**
+
+**Discovered:** stage 5d-next-3 (out-coins). The same
+`AccountState::hash` value plays three different roles inside the
+SPEC §8 state-transition predicate, and conflating them broke a
+positive test with a cryptic "Partition was set twice with different
+values" Plonky2 error.
+
+**The three hashes:**
+
+| Role | Inputs | Used by |
+| --- | --- | --- |
+| `initial_account_state_hash` | `owner` + INITIAL balance + INITIAL pubkey | SPEC §8 (b) state continuity, (c) commitment-witness check |
+| `interim_account_state_hash` | `owner` + POST-in-coins-AND-out-coins balance + INITIAL pubkey | Out-coin identifier derivation: `out_coin.identifier == H(interim_asth || index)` |
+| `final_account_state_hash` | `owner` + POST-in-coins-AND-out-coins balance + NEW pubkey | Public output `ProofData.account_state_hash` |
+
+**Why three not one:**
+- The in-coin loop mutates the running balance via `apply_coin`.
+- The out-coin loop further mutates it via `send_coins`.
+- The pubkey is rotated *after* identifier derivation, *before* the
+ final commit.
+
+So:
+- (b) and (c) compare against `prev.account_state_hash` and
+ `mp.commitment_account_state_hash`, both of which witness the
+ state at *start* of the transition. Use INITIAL balance + INITIAL
+ pubkey.
+- The out-coin identifier `H(account_hash || index)` is computed
+ *after* subtractions per SPEC §8 step 3. Use POST-subtraction
+ balance + INITIAL pubkey (rotation happens *after* the loop).
+- The committed public output is the state at the *end* of the
+ transition. Use POST-subtraction balance + NEW pubkey.
+
+**Common test mistake:** computing the off-circuit expected
+identifier `H(account_hash || index)` using the INITIAL balance.
+The in-circuit identifier-equality check then fails with a wire
+conflict because the prover-supplied identifier doesn't match the
+in-circuit `H(interim_asth || index)`. Catch: when writing the
+out-coin test fixture, always pre-compute the interim balance from
+`initial - out_coin_amount` before hashing.
+
+### 7.21 Stage 5d-next-4 source-side verification blocked on Plonky2 1.1.0 — **resolved in §7.22**
+
+**Discovered:** when attempting Stage 5d-next-4 — adding per-in-coin
+recursive verification of the source state-transition proof per
+SPEC §8 step 2 — two distinct Plonky2 1.1.0 limitations made the
+full implementation infeasible for MVP timeline.
+
+#### Attempted approach A: 8 cyclic verifies in outer circuit
+
+Added `MAX_IN_COINS = 8` additional `conditionally_verify_cyclic_proof_or_dummy::`
+calls inside `build_circuit` (one per slot) plus an extended
+`common_data_for_recursion_c` with `N_RECURSIVE_VERIFIES = 9`
+`verify_proof` calls in pass 3 (1 prev_account + 8 sources).
+
+The outer's gate count crossed the per-gate-config constants budget
+and Plonky2 emitted `ConstantGate { num_consts: 2 }` in the
+`common_data.gates` list. But Plonky2's `dummy_circuit` (called from
+`dummy_proof_and_vk` inside `_or_dummy`) rebuilds a circuit with just
+NoopGate + `add_gate_to_gate_set`, so its `circuit.common.gates`
+excludes `ConstantGate`. The `assert_eq!` in `dummy_circuit.rs:116`
+fires:
+
+```
+assertion `left == right` failed
+ left: CommonCircuitData { gates: [NoopGate, ConstantGate { num_consts: 2 }, ...] }
+ right: CommonCircuitData { gates: [NoopGate, PoseidonMdsGate, ...] }
+```
+
+Both `cyclic_base_proof` AND `conditionally_verify_cyclic_proof_or_dummy`
+trigger this assertion. So in Plonky2 1.1.0, **circuits that emit
+`ConstantGate` are limited to exactly ONE `_or_dummy` call per outer
+build**.
+
+#### Attempted approach B: in-circuit data-only source check (no cyclic verify)
+
+Dropped the recursive verify; kept only the SMT inclusion of the
+coin in the witnessed `source_output_coins_root` + SPEC §8 (c)(d)(e)
+chain for the source's commitment in `history_root`. Idea: the
+"source is a valid prior transition" property is enforced by the
+trusted server only folding validly-proved commitments into the
+history MMR — sufficient for server-heavy MVP.
+
+The outer build then failed with a different error: the cyclic
+fixed-point check `goal_data != common` failed at `circuit_builder.rs:1067`
+("Failed to build circuit"). The added source-side gates (SMT
+inclusion path of 256 levels + CMP chain per slot) pushed outer's
+gate count from ~10 k (Stage 5d-next-3) to ~30 k, but the resulting
+`CommonCircuitData` shape didn't exactly match what
+`common_data_for_recursion_c`'s pass 3 produced — multiple
+`INNER_PAD_BITS` values (14, 15, 16, 17) all triggered the mismatch
+because the gate-set composition (selector groups, constant counts)
+diverged in ways that NoopGate padding alone cannot reconcile.
+
+#### Decision
+
+**Defer to Stage 5d-next-5 (post-MVP).** For the zkCoins server-heavy
+MVP architecture (server generates all proofs, wallet holds only
+private key, single trusted server), the security property "in-coin
+came from a valid prior transition" can be enforced **off-circuit**:
+the server only folds commitments of validly-proved transitions into
+the history MMR. So in-circuit SMT inclusion of the coin in the
+witnessed `source_output_coins_root` + CMP chain for the source's
+commitment in `history_root` would be sufficient — but even that
+hit the build-time `goal_data != common` mismatch.
+
+Stage 5d-next-3 already implements:
+- Prev-account cyclic recursion (1 verify, `condition` selects Init vs Update).
+- Full coin-history-side in-coin predicate (SMT non-inclusion + insert,
+ apply_coin with recipient + balance-overflow).
+- Full out-coin processing (SMT non-inclusion + insert, balance
+ subtraction with underflow, identifier derivation, pubkey rotation).
+- SPEC §8 (c)(d)(e) chain for the **prev_account**'s commitment.
+- All 10 of 11 SPEC §13 negatives covered (only "source-not-in-history"
+ is deferred).
+
+This is sufficient for shipping the MVP. Stage 5d-next-5 paths
+forward when revisited:
+1. **Aggregator pattern**: separate non-cyclic aggregator circuit
+ bundling N source verifies, outer verifies one aggregator proof.
+ Avoids the multi-`_or_dummy` issue.
+2. **Plonky2 patch**: upstream fix to make `dummy_circuit` reproduce
+ `ConstantGate`-containing `common_data` shapes. Significant work.
+3. **Single-source build constraints**: rebuild outer so its
+ `common_data` matches pass-3's exactly even with the additional
+ source-side gates. Requires understanding Plonky2's selector
+ group formation.
+
+**Rule of thumb:** for `conditionally_verify_cyclic_proof_or_dummy`
+to work, the outer's actual `common_data` after build must EXACTLY
+match the `common_data` you passed in. Adding constraints / constants
+to the outer changes selector groups and can break the match
+unrecoverably even with NoopGate padding. Test minor circuit
+additions iteratively against the smoke test, not in one big batch.
+
+---
+
+### 7.20 Speed up panic tests via `cyclic_base_proof` short-circuit — **codified**
+
+**Discovered:** stage-5d-next-3 added panic tests like
+`stage_5d_next_3_prove_account_update_panics_on_wrong_in_slot_count`
+to cover the `assert_eq!`-message lines in
+`prove_account_update_with_in_and_out_coins`. The first draft
+called `prove_initial(...)` to construct a real prev proof before
+invoking the function — paying **~13 min wall clock** per "panic"
+test at `MAX_IN_COINS = MAX_OUT_COINS = 8`. Multiply by N panic
+tests and the test sweep balloons.
+
+**The trick:** the slot-count `assert_eq!`s fire at the **top** of
+the function, before any witness setting, before `prove`. The
+`prev: &ProofWithPublicInputs` parameter is never consumed
+in the panic path. Substitute a `cyclic_base_proof(common_data,
+verifier_only, empty_pis)` dummy — type-equivalent, ~10 ms to
+construct, panic short-circuits before it's touched.
+
+```rust
+let dummy_inner_pis = std::iter::empty::<(usize, F)>().collect();
+let dummy_prev = cyclic_base_proof(
+ &circuit.common_data,
+ &circuit.data.verifier_only,
+ dummy_inner_pis,
+);
+let _ = prove_account_update_with_in_and_out_coins(
+ &circuit, &account_state, ZERO_HASH, &dummy_prev, &dummy_cmp(),
+ &[], // wrong slot count — assert_eq! fires here
+ &out_coins, &account_state.public_key,
+);
+```
+
+Net savings on stage 5d-next-3: ~25 min wall per full test sweep
+(2 account-update panic tests × ~13 min each). Pattern generalises
+to any `should_panic` test whose target's expensive arguments are
+only consumed *after* the panic point.
+
+**Rule of thumb:** when writing a `should_panic` test for a
+function with expensive arguments, look at where the panic fires
+in the function body — if the arguments aren't accessed before
+that point, substitute dummies.
+
+---
+
+### 7.22 Stage 5d-next-5 source-side verification via aggregator pattern — **codified (resolves §7.21)**
+
+**Discovered:** §7.21 deferred source-side verification because both
+attempted paths failed at Plonky2 1.1.0's recursion seams. The
+resolution combined two empirical fixes — `ConstantGate::new(2)`
+injection in the helper, and the `helper_degree = pad_bits + 1`
+relation — with an aggregator-pattern restructure that bundles all
+`MAX_IN_COINS` source verifies into a single non-cyclic aggregator
+proof. The outer then performs exactly **one** additional verify (the
+aggregator), staying under the "one `_or_dummy` per outer" budget
+that broke approach A in §7.21.
+
+#### Final architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ SourceAggregatorCircuit (NON-CYCLIC) [PHASE 1] │
+│ │
+│ For each slot i in 0..MAX_IN_COINS: │
+│ active[i]: BoolTarget │
+│ real_proof[i]: ProofWithPublicInputsTarget │
+│ dummy_proof[i]: ProofWithPublicInputsTarget │
+│ conditionally_verify_proof::( │
+│ active[i], │
+│ real_proof[i], st_verifier_data, ← shared │
+│ dummy_proof[i], dummy_vd_target, ← constant │
+│ st_common, │
+│ ) │
+│ │
+│ PIs: │
+│ [i*17 .. i*17 + 16]: source ProofData │
+│ [i*17 + 16]: active bit │
+│ [MAX_IN_COINS*17 .. + 4]: st verifier_data digest │
+│ [MAX_IN_COINS*17 + 4 ..]: st verifier_data sigmas_cap │
+└─────────────────────────────────────────────────────────────┘
+ │
+ │ aggregator_proof
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ Outer StateTransitionCircuit (CYCLIC) [PHASE 2a+2b] │
+│ │
+│ verify_proof::( ← hoisted above in-coin loop │
+│ aggregator_proof, │
+│ aggregator_verifier_data, ← constant_verifier_data │
+│ aggregator_common, │
+│ ) │
+│ │
+│ connect_hashes(claimed_st_digest, outer_vd.digest) │
+│ connect_hashes(claimed_st_cap, outer_vd.cap) │
+│ │
+│ Per in-coin slot i (Phase 2b): │
+│ connect(slot.active, aggregator.slot[i].active_pi) │
+│ SMT inclusion of coin_identifier in │
+│ source.output_coins_root (masked by .active) │
+│ Coupling: source.output_coins_root == │
+│ source_cmp.commitment_out_coins_root │
+│ SPEC §8 (c)(d)(e) chain for source.commitment in │
+│ outer's history_root │
+│ │
+│ conditionally_verify_cyclic_proof_or_dummy( │
+│ condition, prev_account_proof, common_data, │
+│ ) │
+│ │
+│ builder.add_gate(ConstantGate::new(2), [0, 0]) ← shape │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+#### Two empirical insights pinned by `recursion_shape_probe`
+
+**Insight 1 — `ConstantGate::new(2)` injection (probe-verified).**
+`common_data_for_recursion_c_inner` calls two `verify_proof`s in pass
+2 and 3 (one cyclic, one against the aggregator). Pass-3's
+`ArithmeticGate` instances absorb every routed constant — no
+standalone `ConstantGate` ever gets allocated by `builder.build::()`.
+But `dummy_circuit`'s rebuild always emits one (its hard-coded `- 2`
+NoopGate reservation reserves a row for `PublicInputGate +
+ConstantGate`). The `assert_eq!(&circuit.common, common_data)` at
+`plonky2-1.1.0/src/recursion/dummy_circuit.rs:116` then panics.
+
+Probe data (`recursion_shape_probe::dump_pass_3_gates_lists_for_inspection`):
+
+| Helper variant | `gates.len()` | `ConstantGate`? | `dummy_circuit` |
+|---|---:|---|---|
+| Stage 5d-next-3 baseline (1 verify, pad 14) | 13 | ✓ | **OK** |
+| 2 verify, pad 14, no injection | 12 | ✗ | **PANIC** |
+| 2 verify + 1/4/16/64/256 forced constants via `mul(c, zero)` | 12 | ✗ | **PANIC** |
+| **2 verify + explicit `ConstantGate::new(2)` injection, pad 14** | **13** | **✓** | **OK** |
+
+Fix lives in `common_data_for_recursion_c_inner`'s pass 3 — see the
+function's in-source comment for the injection rationale.
+
+**Insight 2 — `INNER_PAD_BITS_STAGE_5D_NEXT_5 = 15` (sweep-verified).**
+Once `dummy_circuit` accepts the gate-set, the cyclic fixed-point
+check at `plonk/circuit_builder.rs:1067` (`goal_data != common`) is
+still strict: it requires `outer.common == helper-pass-3 common`
+field-by-field. The `build_minimal_outer_for_diagnostic` plus
+field-diff exercise isolated the only diverging axis to
+`fri_params.degree_bits`, exposing the empirical relation:
+
+> `helper_degree = pad_bits + 1`
+
+The helper's pad-bits must therefore equal `outer_degree - 1` to
+converge:
+
+| Stage | outer gate count (approx) | outer_degree | required `pad_bits` |
+|---|---:|---:|---:|
+| 5d-next-3 (1 verify, no source-side) | ~10 k | 14 | 13 |
+| 5d-next-5 Phase 2a (2 verify, no source-side gates) | ~30 k | 15 | **14** |
+| 5d-next-5 Phase 2b (2 verify + 8 source slots × {SMT + CMP}) | ~50 k | 16 | **15** |
+| Hypothetical future stage crossing 2^16 | > 65 k | 17 | 16 |
+
+`INNER_PAD_BITS_STAGE_5D_NEXT_5 = 15` makes `helper_degree = 16` match
+the full outer's `degree_bits = 16`.
+
+If any future change crosses a power-of-two gate-count threshold,
+rerun the sweep and bump `pad_bits`:
+
+```bash
+cd program-plonky2
+cargo test --release --lib \
+ circuit::recursion_shape_probe::dump_phase_2a_pad_bits_sweep \
+ -- --ignored --nocapture
+```
+
+The sweep uses a MINIMAL outer (no real Stage 5d-next-3 / 5d-next-5
+constraints); it establishes the `helper_degree = pad_bits + 1`
+relation. The full outer's degree must then be measured directly via
+`circuit.data.common.fri_params.degree_bits` and compared.
+
+#### Phase 2b per-slot constraints
+
+For slot `i ∈ 0..MAX_IN_COINS`, in `build_circuit`'s in-coin loop:
+
+1. Extract source `ProofData` from aggregator PIs at offset
+ `i * PER_SLOT_PIS` — `account_state_hash`, `output_coins_root`,
+ `commitment_history_root` (`coin_history_root` is unused for
+ SPEC §8 step 2).
+2. **Active-bit binding** — `builder.connect(slot.active.target,
+ aggregator.slot[i].active_pi)`. Strict equality: there is no way
+ to consume an in-coin without a verified source proof.
+3. **SMT inclusion** of `coin.identifier` in `source.output_coins_root`.
+ Leaf value = `h(coin.identifier || coin.identifier)` (set-membership
+ convention, matching the source's own out-coin SMT insertion at
+ `hash_up_full_path(new_leaf = h(id || id), id_bits, nip_path)`).
+ Uses `hash_up_full_path` directly — NOT `smt_inclusion_root`, which
+ would add an extra `smt_leaf_hash` step and break the binding.
+4. **Coupling** — `source.output_coins_root ==
+ source_cmp.commitment_out_coins_root`, masked element-wise
+ (`mul(active, diff) → assert_zero`).
+5. **SPEC §8 (c)** — `source.account_state_hash ==
+ source_cmp.commitment_account_state_hash`, masked.
+6. **SPEC §8 (d), first half** — SMT inclusion of `commitment =
+ h(commitment_account_state_hash || commitment_out_coins_root)` at
+ `source_cmp.smt_key` in `source_cmp.commitment_root`, masked.
+7. **SPEC §8 (d), second half** — MMR inclusion of
+ `h(source_cmp.commitment_root || source_cmp.commitment_root_mmr_sibling)`
+ at `source_cmp.mmr_a_index` in the outer's `history_root`, masked.
+8. **SPEC §8 (e)** — MMR inclusion of `h(source_cmp.prev_smt_in_mmr_leaf
+ || source.commitment_history_root)` at `source_cmp.mmr_b_index` in
+ the outer's `history_root`, masked.
+
+#### Public API extensions
+
+```rust
+pub struct InCoinSourceWitness<'a> {
+ pub source_proof: &'a ProofWithPublicInputs,
+ pub source_inclusion: &'a InclusionProof,
+ pub source_cmp: &'a CommitmentMerkleProofs,
+}
+
+pub fn prove_initial_with_in_and_out_coins_and_sources(
+ circuit, account_state, history_root,
+ in_coins, out_coins, next_public_key,
+ sources: &[Option], // MAX_IN_COINS entries
+) -> Result>;
+
+pub fn prove_account_update_with_in_and_out_coins_and_sources(
+ circuit, account_state, history_root, prev, cmp,
+ in_coins, out_coins, next_public_key,
+ sources: &[Option],
+) -> Result>;
+```
+
+The legacy all-inactive `prove_*_with_in_and_out_coins` entry points
+delegate with `&[None; MAX_IN_COINS]`. Callers with active in-coin
+slots **must** use the `_and_sources` variants — the active-bit
+binding constraint enforces this at prove time.
+
+#### Multi-leaf MMR test fixture insight
+
+`build_test_source_witness` (1-leaf MMR, Phase 2b Initial smoke) and
+`build_test_source_and_prev_witnesses` (2-leaf MMR, Phase 2b
+AccountUpdate smoke) both ship with the implementation. The 2-leaf
+fixture is nontrivial: with BOTH the consumer-prev proof AND the
+source proof having `commitment_history_root = ZERO_HASH` (bootstrap),
+only ONE of them can use the bootstrap-shaped (e) leaf
+`h(? || ZERO_HASH)` at its own MMR index. The fixture resolves this
+by folding consumer-prev FIRST (so consumer's leaf is the unique
+`h(? || ZERO_HASH)`-shaped leaf at index 0) and source SECOND at
+index 1, then having source's (e) "borrow" consumer's bootstrap leaf
+at index 0 via `source_cmp.prev_smt_in_mmr_leaf = consumer_smt_root`
+and `source_cmp.previous_root_history_proof.1 = consumer_mmr_proof`.
+This is a TEST-FIXTURE peculiarity; production producers proving
+against a non-empty history don't hit it because they have richer
+non-bootstrap MMR shapes available.
+
+#### Test coverage matrix
+
+Positives (5 integration tests, all green):
+
+| Case | Test |
+|---|---|
+| Init, all-inactive in-coins | `stage_5c_plus_initial_non_mint_zero_balance_accepted` |
+| Init, 1 active in-coin + real source proof | `stage_5d_next_5_phase_2b_initial_with_one_active_in_coin_and_source` |
+| Init, in-coin + out-coin + source | `stage_5d_next_5_phase_2b_initial_combined_in_and_out_coin_with_source` |
+| Update, all-inactive in-coins | `stage_5c_plus_initial_then_account_update_with_commitment_proofs` |
+| Update, 1 active in-coin + real source proof | `stage_5d_next_5_phase_2b_account_update_combined_in_and_out_coin_with_source` |
+
+SPEC §13 source-side negatives (3 cases, all green):
+
+| Attack | Constraint that catches it | Test |
+|---|---|---|
+| Source's commitment not in `history_root` (tamper MMR-(e) path) | masked `connect_hashes(mmr_b_computed, history_root)` | `stage_5d_next_5_phase_3_source_not_in_history_rejected` |
+| Coin identifier not in source's `output_coins_root` (tamper SMT path) | masked `connect_hashes(source_inclusion_computed, source_output_coins_root)` | `stage_5d_next_5_phase_3_coin_not_in_source_ocr_rejected` |
+| Wrong `st_verifier_data` witnessed in aggregator | `connect_hashes(claimed_st_digest, outer_vd.circuit_digest)` | `stage_5d_next_5_phase_3_wrong_st_vk_on_aggregator_rejected` |
+
+The wrong-vk negative is non-trivial to construct because the
+aggregator's `conditionally_verify_proof` would normally reject a
+wrong-vk source proof at aggregator prove-time. The test exploits the
+all-inactive case: with no slot active, the aggregator never actually
+uses the witnessed `st_verifier_data` for verification (only the
+constant-baked `dummy_vd_target` for the dummy branch), so the
+aggregator can be proved with a LYING `st_verifier_data`. The lie
+then surfaces at the outer's `connect_hashes`.
+
+#### Benchmark (M3, 24 GB, single-threaded `cargo test --release --lib …`)
+
+- `stage_5c_plus_initial_non_mint_zero_balance_accepted` (all-inactive
+ Phase 2b smoke): **~40 s** wall.
+- `stage_5c_plus_initial_then_account_update_with_commitment_proofs`
+ (init → update chain, all-inactive in-coins): **~53 s** wall.
+- `stage_5d_next_5_phase_2b_initial_with_one_active_in_coin_and_source`
+ (Init + 1 active in-coin from source): **~99 s** wall (Init for the
+ source ~40 s + consumer Init ~50 s).
+- `stage_5d_next_5_phase_2b_account_update_combined_in_and_out_coin_with_source`
+ (Update + in-coin + out-coin + source, 2-leaf MMR): **~154 s** wall
+ (source Init + consumer prev Init + consumer Update).
+- Phase 3 negatives: each ~50–55 s wall (one source Init + one
+ consumer prove, except the wrong-vk negative which skips the source
+ build entirely via the all-inactive shortcut).
+- `dump_phase_2a_pad_bits_sweep` (`#[ignore]`d diagnostic, 4 rebuilds
+ of aggregator + minimal outer): **~138 s** wall.
+
+#### Verification runbook
+
+```bash
+cd program-plonky2
+
+# 1. Phase 2a probe (no Phase 2b dependencies).
+cargo test --release --lib \
+ circuit::recursion_shape_probe::dump_pass_3_gates_lists_for_inspection \
+ -- --nocapture
+# Expect: baseline_ok=true, 2v_14=false, 2v_14_with_constant_gate=true
+
+cargo test --release --lib \
+ circuit::recursion_shape_probe::dump_phase_2a_pad_bits_sweep \
+ -- --ignored --nocapture
+# Expect: pad_bits=N → helper_degree=N+1 for N in {14, 15, 16, 17}
+
+# 2. Phase 2a smokes (all-inactive in-coins; Stage 5d-next-3 regression).
+cargo test --release --lib \
+ stage_5c_plus_initial_non_mint_zero_balance_accepted \
+ -- --nocapture
+cargo test --release --lib \
+ stage_5c_plus_initial_then_account_update_with_commitment_proofs \
+ -- --nocapture
+
+# 3. Phase 2b positives (active in-coin slots + real source proofs).
+cargo test --release --lib stage_5d_next_5_phase_2b -- --nocapture --test-threads=2
+
+# 4. Phase 3 negatives.
+cargo test --release --lib stage_5d_next_5_phase_3 -- --nocapture --test-threads=2
+
+# 5. Aggregator regression (Phase 1).
+cargo test --release --lib circuit::source_aggregator::tests::
+```
+
+**Rule of thumb:** when a Plonky2 1.1.0 outer circuit needs more than
+one `verify_proof`, factor the additional verifies into a non-cyclic
+aggregator and verify the aggregator (a single proof) from the outer.
+Per outer build, exactly one `_or_dummy` plus one or more
+non-`_or_dummy` `verify_proof`s. The aggregator must be built before
+the outer (its `verifier_data` is a circuit constant in the outer);
+the fixed-point iteration in `common_data_for_recursion_c_inner` then
+needs `ConstantGate::new(2)` injection in pass 3 and
+`pad_bits = outer_degree - 1` to converge.
+
+### 7.23 `MINTING_ADDRESS` panic in `tokio::spawn`-ed task swallows server bootstrap — **MEDIUM, codified**
+
+**Discovered:** first auto-deploy of `zkcoins/node:beta` on the DEV
+host post-PR [#17](https://github.com/zk-coins/node/pull/17). The
+container started, the REST server bound `0.0.0.0:4242`, but
+`https://dev-api.zkcoins.app/health` returned Cloudflare 502 for hours.
+`docker compose ps` showed the container as `Up (unhealthy)` — the
+tokio worker that owned the HTTP listener panicked on every cold boot
+after the Plonky2 migration, while the block-scanner worker kept
+processing blocks. No restart, no monitor, no visible failure in
+`docker logs`.
+
+**Root cause:** the Plonky2 migration moved `MINTING_ADDRESS` to a
+well-known constant (`hash_bytes(b"zkcoins:minting-address:placeholder:v1")`
+in `program-plonky2/src/types.rs`). The SP1-era `ClientAccount::new`
+in `server` still derived `address` from the privkey's first child
+pubkey; the `assert_eq!` in `start_rest_node` between the two could
+never hold again. **And** a panic inside a `tokio::spawn`-ed task by
+default only kills the task — the process happily continued in zombie
+state for 8 h with the listener dead and the scanner alive.
+
+**Fix (PR [#36](https://github.com/zk-coins/node/pull/36)):**
+
+1. **Explicit `MINTING_ADDRESS` override** applied in
+ `runtime.rs::start_rest_node`: after constructing the
+ minting `ClientAccount` from `minting_secret.bin`, the code
+ overwrites `minting_client.address = *MINTING_ADDRESS` so the
+ on-chain identity matches the well-known constant that the Plonky2
+ circuit uses, replacing the failing `assert_eq!`. Matches the
+ pattern already used in `router_tests.rs::TestAccountData::new_minting_account`.
+2. **Global panic hook** installed at the top of `main.rs::main` that
+ runs the default reporter and then `exit(1)`. Any future tokio
+ worker panic now crash-loops the container via `restart:
+ unless-stopped` instead of becoming a silent zombie.
+3. **Integration smoke test** (`start_rest_node_binds_and_serves_health`)
+ that spawns `start_rest_node` against an ephemeral port and probes
+ `/health` over real TCP. `runtime.rs` was excluded from the
+ coverage scope, so the bootstrap path that exploded had no test at
+ all. ~22 s warm; runs in the standard test sweep.
+4. **deploy-dev post-curl-retry** in `.github/workflows/deploy-dev.yaml`:
+ up to 30 × 10 s polls of `https://dev-api.zkcoins.app/api/info` after
+ the ssh deploy. A green "Build and deploy to DEV" with a broken
+ upstream is no longer possible — the workflow fails, the auto-release
+ PR loses its green check, and the regression surfaces immediately
+ instead of hours later. Mirrored to deploy-prd in PR [#51](https://github.com/zk-coins/node/pull/51).
+
+**Lesson:** in async server code, NEVER let a spawned task panic
+silently. Either install a global panic hook (the cheap fix taken
+here) or wrap every spawned future in a `Result`-returning closure
+that explicitly propagates the panic to the main task via a watcher
+channel. The deploy workflow must also probe the public health
+endpoint before declaring success — `docker compose up -d` exiting 0
+is a build-time signal, not a runtime-readiness signal.
+
+**Regression guard:** the smoke test fires on every test sweep; the
+deploy-dev post-curl-retry fires on every DEV deploy. A regression
+that brings back the silent-panic shape fails one or both gates.
+
+---
+
+## 8. Local Artifacts
+
+- BitVM/zkCoins reference (cloned): `~/Documents/GitHub/zkcoins/BitVM-zkCoins-reference/`
+- Shielded CSV reference implementation files (downloaded by the research agent): `/tmp/shielded_csv_lib.rs`, `/tmp/shielded_csv_primitives.rs`, `/tmp/shielded_csv_node.rs`. **TODO:** clone the full `ShieldedCSV/ShieldedCSV` repo to `~/Documents/GitHub/zkcoins/ShieldedCSV-reference/` if we decide to make it the normative reference (see §5.1).
+
+---
+
+## 9. References
+
+- Shielded CSV paper: https://eprint.iacr.org/2025/068
+- Shielded CSV reference implementation: https://github.com/ShieldedCSV/ShieldedCSV
+- BitVM/zkCoins Plonky2 prototype: https://github.com/BitVM/zkCoins
+- Blockstream blog: https://blog.blockstream.com/bitcoins-shielded-csv-protocol-explained/
+- Bitcoin Magazine: https://bitcoinmagazine.com/technical/shielded-csv-protocol
+- Plonky2: https://github.com/0xPolygonZero/plonky2
diff --git a/MULTI_ASSET.md b/MULTI_ASSET.md
new file mode 100644
index 00000000..cbacba7f
--- /dev/null
+++ b/MULTI_ASSET.md
@@ -0,0 +1,1198 @@
+# Multi-Asset zkCoins Design
+
+**Status:** Design draft. No code yet. Companion to
+[`SPEC.md`](./SPEC.md), [`MIGRATION_RESEARCH.md`](./MIGRATION_RESEARCH.md),
+and [`ROADMAP.md`](./ROADMAP.md). Sibling design docs:
+[`BRIDGE_MVP.md`](./BRIDGE_MVP.md),
+[`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md),
+[`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md).
+
+**Authoritative source for:** the multi-asset protocol extension —
+scope, locked decisions, circuit and state-layer changes, API shape,
+phased rollout, non-goals.
+
+**Audience:** Engineers implementing the multi-asset upgrade.
+Presupposes `SPEC.md` (single-asset protocol), the project
+invariants in [`CONTRIBUTING.md`](./CONTRIBUTING.md) § "Working on
+the Plonky2 Migration", and the `MAX_IN_COINS`/`MAX_OUT_COINS`
+fixed-shape fanout of the current circuit.
+
+---
+
+## 0. Status
+
+Design draft only. The current protocol is single-asset: `Invoice {
+amount, recipient }`, `Account { balance: u64, … }`, no `asset_id`
+anywhere. This document specifies the extension to a permissionless
+multi-asset system — anyone mints a token by name, the creator keeps
+ongoing mint authority, transactions stay single-asset, asset
+metadata is name + decimals. Implementation tracking lands in
+[`ROADMAP.md`](./ROADMAP.md) once the maintainer approves this draft.
+
+---
+
+## 1. Motivation
+
+zkCoins today serves one asset: the faucet-minted unit returned by
+`/api/mint`. The minting account is hard-coded (`MINTING_ADDRESS`,
+see [`SPEC.md`](./SPEC.md) §8 "Note on the minting account"), the
+`Invoice` and `Coin` types carry only `amount + recipient`, and the
+account-server's `balance: u64` is a single scalar.
+
+Multi-asset opens this to any user: anyone mints a new token under a
+chosen name, distributes it, and retains the right to issue more.
+The shielded-CSV mechanics (per-account history SMT, global
+commitment MMR, BIP-340 Schnorr inscription on Bitcoin) carry over
+unchanged; the asset identity rides as an extra field on coins, on
+invoices, and on the SMT-leaf pre-image.
+
+Two design pressures pull in opposite directions:
+
+- **Privacy** — separate per-asset anonymity pools maximise
+ unlinkability across assets but multiply state and circuit cost.
+- **Simplicity** — a single SMT with `asset_id` as a public field on
+ each commitment keeps the circuit shape unchanged (the only new
+ in-circuit constraint is "all coins in this transition share the
+ same `asset_id`") and the prover cost roughly flat.
+
+This document picks **simplicity**. The privacy trade-off is
+explicit: an outside observer learns which asset moved per
+transaction; the sender, recipient, and amount stay private as
+before. Per-asset privacy pools are deferred (see §12.10).
+
+The decision space matches `MIGRATION_RESEARCH.md` §5's pattern:
+each constraint below is locked for v1 and reversible only at the
+cost of a circuit redesign.
+
+---
+
+## 2. Decisions (locked)
+
+The six decisions below are fixed for v1. Reversing any of them
+means a non-trivial protocol-level change.
+
+| # | Decision | Consequence |
+| - | -------- | ----------- |
+| **M1** | **Token creation is permissionless.** Any account can call `/api/asset/create` and mint a new asset. No whitelist, no admin gate, no fee gate. | The server is a pass-through registrar. Spam pressure is handled by the on-chain inscription fee on the genesis transaction's `Commitment`, not by the server. |
+| **M2** | **Creator retains ongoing mint authority.** The asset's genesis transaction pins a `mint_authority_pubkey` (the creator's compressed secp256k1 pubkey). Subsequent `/api/mint` calls require a fresh Schnorr signature verifiable against that pubkey. No fixed-supply rule. | No "burn the key after genesis" mode. Total supply is open-ended; trust in the asset is trust in the creator not to over-issue. Key rotation is out of scope (see §11, §12.7). |
+| **M3** | **Asset namespace is first-come-first-served on `name`.** The first genesis transaction binding a given `name` wins; later attempts return `409 Conflict`. Normalisation is `name.to_lowercase()` to remove the cheapest look-alike attacks; the trade-off is documented in §10. | `assets.name UNIQUE` at the SQL layer is the enforcement point. No retroactive renaming, no namespace governance. |
+| **M4** | **Privacy pool is a single shared SMT.** `asset_id` is a public field on each coin commitment and a public input on each state-transition proof. Anonymity-set is per-asset (all `asset_id = X` traffic mixes; `asset_id = Y` is a separate pool). | Circuit complexity unchanged modulo one extra public input + one cross-coin equality constraint. Per-asset trees and per-asset MMRs are deferred. |
+| **M5** | **Cross-asset transfers are out of protocol.** Every state transition moves exactly one `asset_id`; no atomic A↔B swap inside zkCoins. A↔B trading is a separate DEX layer (out of scope: BitVM2 bridge, Lightning atomic swap, off-protocol order-book). | The in-circuit invariant is simple: all input coins and all output coins in a transition carry the same `asset_id`. Multi-leg trades are wallet-side UX over multiple proofs, or an external swap protocol. |
+| **M6** | **On-chain asset metadata is `name + decimals` only.** `name` is UTF-8, ≤ 32 bytes after normalisation; `decimals` is `u8` (0-18). No logo, URI, description, supply cap, or other fields. | Richer metadata (logo, links, social) lives off-chain — a separate registry the wallet may consult by `asset_id`. The on-chain genesis stays small and immutable; see §6.2. `decimals` is pure UX (no on-chain math change). |
+
+These mirror the lockedness of `MIGRATION_RESEARCH.md` §5 (Plonky2
+locked-in decisions) and `BRIDGE_MVP.md` §3 (Bridge locked technical
+decisions). Each is testable at 100% coverage per invariant 4 of
+[`CONTRIBUTING.md`](./CONTRIBUTING.md).
+
+---
+
+## 3. Glossary additions
+
+Extends `SPEC.md` § Glossary. Terms below are referenced throughout
+this document.
+
+| Term | Expansion | Meaning |
+| ---- | --------- | ------- |
+| **AssetId** | — | `HashDigest`. Deterministic Poseidon digest derived from the genesis pre-image (see §4.2). Public field on every coin commitment and every state-transition proof under the multi-asset extension. |
+| **AssetGenesis** | — | The genesis transaction that creates a new asset. Carries `name`, `decimals`, `mint_authority_pubkey`, `initial_supply`, `creator_signature`. Persisted in the `assets` table; published on-chain via the same Schnorr-inscription path as a regular send. |
+| **AssetMeta** | — | Off-circuit record holding `(asset_id, name, decimals, mint_authority_pubkey, creator_address, created_at)`. One row per asset in the `assets` table; never mutated after insert (immutable post-genesis). |
+| **MintAuthorityKey** | — | The compressed secp256k1 pubkey pinned at genesis. Every subsequent `/api/mint` call for this asset must carry a fresh BIP-340 Schnorr signature verifiable against it. |
+| **M1 – M6** | — | Locked design decisions for multi-asset (this document, §2). Mirrors `MIGRATION_RESEARCH.md`'s `D1–D11` numbering scheme. |
+
+---
+
+## 4. Protocol changes
+
+### 4.1 Data structures
+
+The new shape of the core types. Field additions are highlighted in
+the diffs below; existing fields keep their semantics from
+`SPEC.md`.
+
+```rust
+// shared/src/lib.rs
+
+pub struct Invoice {
+ pub amount: Amount,
+ pub recipient: Address,
+ pub asset_id: AssetId, // NEW
+}
+
+// program-plonky2/src/types.rs
+
+pub struct Coin {
+ pub identifier: HashDigest,
+ pub recipient: Address,
+ pub amount: Amount,
+ pub asset_id: AssetId, // NEW
+}
+
+pub struct CoinTemplate {
+ pub recipient: Address,
+ pub amount: Amount,
+ pub asset_id: AssetId, // NEW
+}
+```
+
+`Account` (in `node/src/account_node.rs`) gains a per-asset
+balance map; the old `balance: u64` collapses to "balance of the
+default asset" only for the migration window (see §6.3 — there is
+no migration window because state is wiped at cutover, so the field
+is replaced outright).
+
+```rust
+// node/src/account_node.rs
+
+pub struct Account {
+ pub proof: Option,
+ pub coin_queue: Vec,
+ pub coin_history: SparseMerkleTree,
+ pub balances: BTreeMap, // REPLACES `balance: u64`
+}
+```
+
+New record type for the asset registry:
+
+```rust
+// shared/src/lib.rs
+
+pub struct AssetMeta {
+ pub asset_id: AssetId,
+ pub name: String, // normalised, ≤ 32 bytes UTF-8
+ pub decimals: u8, // 0-18
+ pub mint_authority_pubkey: bitcoin::PublicKey,
+ pub creator_address: Address,
+ pub created_at: u64, // unix seconds
+ pub initial_supply: u64,
+}
+```
+
+The Plonky2 `AccountState` carried inside the circuit — see
+`program-plonky2/src/types.rs::AccountState` — stays single-balance
+per-proof: each state-transition proof concerns exactly one
+`asset_id` (decision **M5**), so `AccountState.balance` is the
+balance of *that* asset for the duration of *this* proof. The
+per-asset book-keeping for an account lives off-circuit in
+`Account.balances`; the prover witnesses only the balance for the
+asset being moved.
+
+This keeps the in-circuit `AccountState` layout (`[owner_limbs(4),
+balance_lo, balance_hi, pubkey_x_limbs(4), pubkey_parity]` — see
+`SPEC.md` §12.3) almost unchanged. The minimal addition is one new
+public input: `asset_id` (4 field elements).
+
+### 4.2 Asset genesis (creation)
+
+An asset genesis is a state-transition proof of a new variant —
+call it `AssetGenesisProof` — that mints `initial_supply` units to
+the creator's account, binds the asset's `name`, `decimals`, and
+`mint_authority_pubkey` into the asset registry, and publishes the
+same Schnorr-signed `Commitment` as a regular send.
+
+`AssetId` derivation:
+
+```
+asset_id := Poseidon(
+ DOMAIN_TAG_ASSET_GENESIS,
+ creator_pubkey_limbs(5),
+ name_limbs(N),
+ decimals,
+ timestamp,
+)
+```
+
+`DOMAIN_TAG_ASSET_GENESIS` is a fixed Goldilocks field element
+constant (e.g. `hash_bytes(b"zkcoins:asset-genesis:v1")` taken as a
+field element). `timestamp` is the genesis request's unix-seconds
+value, included so the AssetId is content-addressed: two creators
+who pick the same `(creator_pubkey, name, decimals)` (e.g. on a
+state-wiped DEV that allows name reuse, or after a future asset
+deletion mechanism) still get distinct `asset_id`s. Note that
+`assets.name UNIQUE` already prevents production name collisions
+on a single instance — the timestamp is belt-and-braces, plus a
+provenance marker for off-chain registries. See §12.1 for the
+open question on whether to drop it.
+
+The genesis carries five things into the world:
+
+1. **`name`** — normalised (`to_lowercase()`, validated UTF-8, ≤ 32
+ bytes after normalisation). Uniqueness is enforced at the SQL
+ layer via the `assets.name UNIQUE` constraint (§6.2). The first
+ genesis to commit wins; concurrent attempts return `409
+ Conflict` (§10).
+2. **`decimals`** — `u8`, 0-18. UX-only; no on-chain math depends on
+ it.
+3. **`mint_authority_pubkey`** — compressed secp256k1, pinned for
+ the life of the asset.
+4. **`initial_supply`** — `u64`, minted to the creator's address at
+ genesis. May be 0 (the creator can choose to mint later via
+ `/api/mint`).
+5. **`creator_signature`** — BIP-340 Schnorr over
+ `H("zkcoins:asset-genesis" || asset_id || initial_supply_le ||
+ timestamp_le)`, verifiable against `mint_authority_pubkey`. This
+ binds the genesis transaction to the same key that will sign
+ future mints, preventing a separate party from claiming the
+ asset's name.
+
+### 4.3 Mint (subsequent issuance)
+
+After genesis, the asset creator may issue further units by calling
+`/api/mint { asset_id, recipient, amount, signature, timestamp }`.
+The server:
+
+1. Looks up `AssetMeta` by `asset_id`. Rejects if unknown.
+2. Verifies the BIP-340 Schnorr signature over
+ `H("zkcoins:mint" || asset_id || recipient || amount_le ||
+ timestamp_le)` against the asset's stored
+ `mint_authority_pubkey`.
+3. Rejects if the timestamp is older than 300 s or in the future —
+ matches the existing replay window in
+ `verify_send_signature` (`node/src/server.rs`).
+4. Runs the prover to produce a state-transition proof that moves
+ `amount` units of `asset_id` from the asset's mint-authority
+ account into a fresh coin for `recipient`. The same circuit
+ shape as a normal send; the only branch difference is that the
+ in-circuit signature gate fires against `mint_authority_pubkey`
+ instead of the sender's commitment pubkey (see §5).
+
+The current `/api/mint` is permissioned only by the server's
+faucet config (`feature = "faucet"`, `MINTING_ADDRESS` hard-coded);
+under multi-asset it becomes a signed request from any creator for
+their own asset.
+
+### 4.4 Send
+
+`/api/send` keeps its current shape, with `asset_id` added to the
+`Invoice` and the existing Schnorr signature widened to cover it
+under a new domain-prefix tag:
+
+```
+H("zkcoins:send"
+ || account_address
+ || recipient
+ || amount_le
+ || asset_id
+ || timestamp_le)
+```
+
+Existing wallets sign over `SHA256(account_address || recipient
+|| amount_le || timestamp_le)` with **no** domain prefix — see
+`verify_send_signature` in `node/src/server.rs`. The multi-asset
+upgrade does two things to this hash:
+
+1. **Adds `asset_id`** between `amount_le` and `timestamp_le`.
+ This is the necessary part — the signature must commit to
+ which asset is moving.
+2. **Prepends `"zkcoins:send"`** as a domain-separation tag.
+ This is a deliberate defense-in-depth addition, not a passive
+ widening: it future-proofs against a `/api/mint` or
+ `/api/asset/create` message hash being reused as a send
+ signature once those endpoints share the same secp256k1 key
+ material (the wallet's account key signs both). The mint and
+ genesis hashes already carry their own `"zkcoins:mint"` and
+ `"zkcoins:asset-genesis"` prefixes (§4.2, §4.3); adding
+ `"zkcoins:send"` here normalises the convention across all
+ three message types. See §12.5 for the open question on
+ whether the prefix is strictly required given invariant 2.
+
+Both changes are breaking for the wallet signature shape; bump
+`Capabilities.multi_asset` (§7) so wallets know to include them.
+
+**Single-asset invariant.** In a single transition, all input coins
+and all output coins share the same `asset_id`. This is enforced
+twice — defense in depth, matching the pattern in
+`node/src/account_node.rs::send_coins` (off-circuit pre-check)
+and `program-plonky2/src/circuit/main.rs` (in-circuit constraint):
+
+- **Off-circuit (server pre-check):** before paying prove cost,
+ iterate `account.coin_queue` and `invoices`, assert every
+ `asset_id` equals the transition's claimed `asset_id`. Reject
+ with `400 Mixed assets in single transition` on mismatch.
+- **In-circuit (ZK constraint):** see §5.2.
+
+### 4.5 Balance
+
+`/api/balance` returns a map of `{ asset_id_hex: amount }` instead
+of a single `balance: u64`. Single-asset clients see a one-entry
+map under the well-known "default" asset id; multi-asset clients
+iterate.
+
+```json
+{
+ "address": "ab12…",
+ "balances": [
+ { "asset_id": "00112233…", "amount": 42 },
+ { "asset_id": "deadbeef…", "amount": 1000 }
+ ]
+}
+```
+
+Because the response shape changes, bump
+`Capabilities.multi_asset = true` so single-asset clients can fall
+back gracefully. See §7 for the full API delta.
+
+---
+
+## 5. ZK-circuit changes (Plonky2)
+
+The state-transition circuit lives in
+`program-plonky2/src/circuit/main.rs`. The multi-asset extension is
+additive: one new public input, one new cross-coin equality
+constraint per active in-coin and out-coin slot, no shape change to
+the cyclic-recursion plumbing.
+
+### 5.1 New public input
+
+`ProofData` gains an `asset_id` field. Public-input layout becomes:
+
+| slot range | meaning |
+| ---------- | ------------------------ |
+| 0..4 | account_state_hash |
+| 4..8 | output_coins_root |
+| 8..12 | commitment_history_root |
+| 12..16 | coin_history_root |
+| **16..20** | **asset_id (new)** |
+
+`N_PROOF_DATA_PUBLIC_INPUTS` increases from 16 to 20. Knock-on
+effects:
+
+- `ProofData::to_field_elements` (`program-plonky2/src/types.rs`)
+ and `ProofData::from_field_elements` extend by one
+ `HashDigest`.
+- `state_transition_num_pis()` in `circuit/main.rs` recomputes
+ to `20 + 4 + 4 * cap_elements`.
+- The cyclic-recursion `common_data_for_recursion_c_inner` rebuild
+ picks up the new PI count automatically once
+ `N_PROOF_DATA_PUBLIC_INPUTS` is bumped; no manual padding tweak
+ required, but the `INNER_PAD_BITS_STAGE_5D_NEXT_5` constant
+ should be re-verified by `recursion_shape_probe::dump_*` per the
+ procedure in `MIGRATION_RESEARCH.md` §7.22 to confirm the
+ helper-degree → outer-degree match still holds at the new PI
+ count.
+
+### 5.2 New in-circuit constraints
+
+The single-asset invariant (M5) is enforced as a fan-in equality
+gate: every active in-coin slot's `coin.asset_id` and every active
+out-coin slot's `out_coin.asset_id` is connected to the
+transition's `asset_id` public input. Inactive slots are masked by
+their `active` bit, identical to the existing balance / recipient
+gates in `program-plonky2/src/circuit/main.rs`.
+
+```rust
+// Pseudo-code, fits next to the existing per-slot recipient + amount checks
+// in the in-coin and out-coin loops in circuit/main.rs.
+
+for slot in in_coin_slots {
+ // Existing: `slot.active * (slot.recipient - account.owner) == 0`
+ // New:
+ // `slot.active * (slot.asset_id - transition_asset_id) == 0`
+ connect_hashes_masked(&mut builder, slot.active, slot.asset_id, transition_asset_id);
+}
+
+for slot in out_coin_slots {
+ connect_hashes_masked(&mut builder, slot.active, slot.asset_id, transition_asset_id);
+}
+```
+
+Coin identifier derivation (`calculate_coin_identifier` in
+`program-plonky2/src/types.rs`) extends to include `asset_id` so
+that the same recipient/amount pair on two different assets
+produces distinct identifiers:
+
+```
+identifier := Poseidon(account_state_hash, asset_id, u32(coin_index))
+```
+
+The SMT leaf pre-image for the coin-history SMT
+(`SparseMerkleTree::insert(key, value)` keyed by
+`coin.identifier`) automatically inherits the new identifier
+shape; no SMT-layer change is required.
+
+### 5.3 Mint-branch signature constraint
+
+The current circuit handles the faucet mint via the
+`MINTING_ADDRESS` exception (`SPEC.md` §8 "Note on the minting
+account"). Under multi-asset this generalises: the genesis and the
+ongoing mint paths take the `AssetGenesisProof` /
+`AssetMintProof` branches in `ProofType`, and the in-circuit
+constraint becomes "the request is signed by the asset's
+`mint_authority_pubkey`".
+
+Two viable architectures, mirroring the recurring trade-off in
+`SPEC.md` §12.6:
+
+1. **Off-circuit Schnorr verify (preferred for v1).** The server
+ verifies the BIP-340 Schnorr signature with the existing
+ `secp.verify_schnorr` call (the same path used by
+ `verify_send_signature` in `node/src/server.rs`), and the
+ in-circuit branch only enforces that the proof's
+ `mint_authority_pubkey` public input matches the
+ asset-registry-stored value. The asset registry is server state,
+ not on-chain state — the mainnet hardening track decides whether
+ this is acceptable (it is for the closed test environment per
+ invariant 2 of [`CONTRIBUTING.md`](./CONTRIBUTING.md)).
+2. **In-circuit Schnorr verify.** Add a BIP-340 Schnorr gadget to
+ the circuit, witness the signature, and verify in-circuit. More
+ expensive (Schnorr-on-secp256k1 inside Plonky2 is non-trivial
+ — see `MIGRATION_RESEARCH.md` §5.4) and not required for the
+ trust model decided in M1 + M2.
+
+→ **v1: option 1.** The mint-authority pubkey is a regular
+ public-input on the genesis/mint branches; the signature check is
+ off-circuit. The architectural call is open at §12.6 — flip to
+ in-circuit if a future deployment requires the stronger trust
+ model.
+
+### 5.4 Prover cost delta
+
+The per-tx cost delta is **minor**:
+
+- +4 public inputs (one new `HashDigest` worth) per proof.
+- +4 × (`MAX_IN_COINS` + `MAX_OUT_COINS`) = +64 masked-equality
+ field-element constraints per proof. Each `connect_hashes_masked`
+ on a `HashOut` (4 elements per Plonky2
+ `NUM_HASH_OUT_ELTS`) lands four masked-equality gates; with
+ `MAX_IN_COINS = MAX_OUT_COINS = 8` per
+ `program-plonky2/src/circuit/main.rs`, that is 16 slots × 4 =
+ 64 gates total — negligible against the ~50 k-gate outer
+ circuit (`INNER_PAD_BITS_STAGE_5D_NEXT_5 = 15`).
+- One extra `HashOut` (4 field elements) added to the
+ coin-identifier pre-image (was `(asth_4, coin_index_1)` = 5
+ elements; now `(asth_4, asset_id_4, coin_index_1)` = 9
+ elements). Plonky2 Goldilocks Poseidon has `SPONGE_RATE = 8`
+ (`plonky2::hash::poseidon::SPONGE_RATE`), so 5 elements
+ absorbed in one permutation; 9 elements now absorb in two. The
+ per-coin Poseidon cost roughly doubles for the identifier
+ derivation, but this is one extra permutation per slot —
+ negligible against the per-slot work elsewhere in the circuit.
+
+The R2 performance budget from `CONTRIBUTING.md` invariant 3 (warm
+≤ 5 s, ≤ 64 GB peak) is not threatened by multi-asset alone.
+
+### 5.5 Cite-points
+
+For implementers, the relevant code sites in the current circuit:
+
+- Public-input count: `program-plonky2/src/circuit/main.rs::N_PROOF_DATA_PUBLIC_INPUTS`.
+- Per-slot in-coin processing (where the new `asset_id` equality
+ gate lands): the in-coin loop in `build_circuit`.
+- Per-slot out-coin processing: the out-coin loop in
+ `build_circuit`, alongside the existing identifier-check.
+- Coin-identifier derivation: `program-plonky2/src/types.rs::calculate_coin_identifier`.
+- Padding constants: `INNER_PAD_BITS_STAGE_5D_NEXT_5`,
+ re-verified via `recursion_shape_probe::dump_phase_2a_pad_bits_sweep`.
+
+---
+
+## 6. State layer
+
+### 6.1 SMT changes
+
+Coin commitments include `asset_id` in the pre-image via the new
+`calculate_coin_identifier` formula (§5.2). The SMT structure stays
+single-tree per **M4**; `asset_id` is just one more field in the
+leaf pre-image, so the existing `program-plonky2/src/merkle/sparse_merkle_tree.rs`
+needs no structural change. The global commitment-history SMT and
+MMR (see `SPEC.md` §5) keep their current shape — they are keyed by
+the commitment pubkey, not by `asset_id`, so cross-asset proofs
+share the same history root and the same anonymity-set at the
+commitment layer.
+
+### 6.2 Postgres schema deltas
+
+New table `assets` — one row per registered asset, immutable
+post-insert:
+
+```sql
+CREATE TABLE assets (
+ asset_id BYTEA PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE,
+ decimals SMALLINT NOT NULL,
+ mint_authority_pubkey BYTEA NOT NULL,
+ creator_address BYTEA NOT NULL,
+ initial_supply BIGINT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX assets_name_idx ON assets (name);
+```
+
+The `name UNIQUE` constraint is the first-come-first-served
+enforcement point (decision M3 / §10).
+
+The `accounts` row needs to hold a per-asset balance. Two options
+match the trade-off space of `SPEC.md` §12.8 and `MIGRATION_RESEARCH.md`
+§5: simpler vs. more queryable.
+
+**Option (a) — JSONB column on `accounts`:**
+
+```sql
+ALTER TABLE accounts ADD COLUMN balances JSONB NOT NULL DEFAULT '{}';
+-- Shape: { "": , ... }
+```
+
+**Option (b) — separate `account_balances` table:**
+
+```sql
+CREATE TABLE account_balances (
+ address BYTEA NOT NULL REFERENCES accounts(address) ON DELETE CASCADE,
+ asset_id BYTEA NOT NULL REFERENCES assets(asset_id),
+ amount BIGINT NOT NULL,
+ PRIMARY KEY (address, asset_id)
+);
+```
+
+→ **v1: option (a).** The bincode-`Account`-in-`BYTEA` pattern
+already used for the `accounts` table (see
+`CONTRIBUTING.md` § "Persistent State") composes naturally with a
+`BTreeMap` field on `Account`; the JSONB column is a
+side index for ad-hoc queries (`SELECT … WHERE balances ?
+''` works in Postgres). If the operational team later
+needs richer balance queries (top-holders, distribution histograms),
+add option (b) as a derived table populated by a trigger; not
+needed for the MVP.
+
+The `minting_meta.num_pubkeys` counter that the faucet uses
+(`CONTRIBUTING.md` § "Persistent State") becomes per-asset.
+Simplest shape: fold it into `assets` as a `num_pubkeys BIGINT NOT
+NULL DEFAULT 0` column, advanced atomically per mint.
+
+```sql
+ALTER TABLE assets ADD COLUMN num_pubkeys BIGINT NOT NULL DEFAULT 0;
+```
+
+The standalone `minting_meta` row is dropped at cutover (no
+migration window, see §6.3).
+
+### 6.3 Migration notes
+
+Per [`CONTRIBUTING.md`](./CONTRIBUTING.md) invariant 2 ("Closed
+test environment — DEV *and* PRD"), the cutover wipes server state
+and starts fresh. No live-migration logic.
+
+The recovery procedure from `CONTRIBUTING.md` § "DEV state
+recovery" applies as written: stop the server, truncate every
+state-layer table (now including `assets`), drop the proofs
+directory, restart. The pre-multi-asset coins are abandoned on-chain
+(they're random test data); the new server starts at genesis with
+an empty `assets` table.
+
+PR-A1/A2/A3 already left DEV and PRD with empty Postgres state
+after the Plonky2 cutover (`SPEC.md` invariant 2; PR
+[#73](https://github.com/zk-coins/node/pull/73) finalised the
+state-wipe pattern). Multi-asset reuses the same operational
+procedure; no new wipe tooling required.
+
+---
+
+## 7. API changes
+
+For each endpoint, the new shape and back-compat note.
+
+### 7.1 `POST /api/asset/create` (new)
+
+Genesis a new asset.
+
+```
+Body:
+{
+ "name": "FOO",
+ "decimals": 8,
+ "initial_supply": 1000000,
+ "mint_authority_pubkey": "<33-byte hex>",
+ "signature": "<64-byte BIP-340 Schnorr hex>",
+ "timestamp": 1716393600
+}
+
+Response (201 Created):
+{
+ "asset_id": "<32-byte hex>",
+ "name": "foo"
+}
+
+Response (409 Conflict):
+{ "error": "asset name already taken" }
+```
+
+The handler:
+
+1. Normalises `name` (`to_lowercase()`, UTF-8-validate, byte-length
+ check ≤ 32).
+2. Validates `decimals ∈ [0, 18]`.
+3. Verifies the BIP-340 Schnorr signature against
+ `mint_authority_pubkey` over
+ `H("zkcoins:asset-genesis" || name_normalised || decimals ||
+ initial_supply_le || timestamp_le)`.
+4. Computes `asset_id` per §4.2.
+5. Begins a transaction: `INSERT INTO assets … ON CONFLICT (name)
+ DO NOTHING`. If the insert affected zero rows, the name was
+ already taken — return 409. Otherwise, run the prover to
+ produce the `AssetGenesisProof`, persist the proof file, and
+ advance the SMT. This matches the existing
+ `UsernameStore::claim` pattern in `node/src/username.rs`
+ (`ON CONFLICT (username) DO NOTHING` + post-check on the
+ returned row count).
+6. Returns `{ asset_id, name }`.
+
+Suggested handler name: `asset_create_handler`. Suggested request
+type: `AssetCreateRequest`.
+
+### 7.2 `GET /api/asset/list` (new)
+
+List every known asset.
+
+```
+Response:
+{
+ "assets": [
+ {
+ "asset_id": "",
+ "name": "foo",
+ "decimals": 8,
+ "mint_authority_pubkey": "<33-byte hex>",
+ "creator_address": "<32-byte hex>",
+ "initial_supply": 1000000,
+ "num_pubkeys": 42,
+ "created_at": "2026-05-22T12:00:00Z"
+ },
+ …
+ ]
+}
+```
+
+Suggested handler name: `asset_list_handler`. Read-only; serves
+straight from the `assets` table; cache headers per the existing
+`/api/info` pattern.
+
+### 7.3 `GET /api/asset/info/:id_or_name` (new)
+
+Single-asset lookup. Path parameter is either the lowercased name
+or the hex-encoded `asset_id`. Returns one of the records from
+`/api/asset/list`'s `assets` array, or `404 Not Found`.
+
+Suggested handler name: `asset_info_handler`.
+
+### 7.4 `POST /api/mint` (modified)
+
+The current faucet semantics
+(`feature = "faucet"`, no signature required because the server is
+the minter) are removed. The new shape:
+
+```
+Body:
+{
+ "asset_id": "",
+ "recipient": "",
+ "amount": 100,
+ "signature": "",
+ "timestamp": 1716393600
+}
+```
+
+Handler verifies the signature against the asset's stored
+`mint_authority_pubkey` (§4.3). The faucet shortcut survives only
+as the "creator never signed away the key, so they can call this"
+case — it is no longer privileged.
+
+`feature = "faucet"` is collapsed into the always-on path; the
+`Capabilities.faucet` flag stays for back-compat but is wired to
+`multi_asset` truthiness (see §7.8).
+
+### 7.5 `POST /api/send` (modified)
+
+Adds `asset_id` to the request body:
+
+```
+Body:
+{
+ "account_address": "",
+ "recipient": "",
+ "amount": 100,
+ "asset_id": "", // NEW
+ "public_key": "<33-byte hex>",
+ "signature": "",
+ "timestamp": 1716393600
+}
+```
+
+The Schnorr-signed message extends to cover `asset_id` (see §4.4).
+Existing single-asset wallets break here unless they update to the
+new signature shape — gated by `Capabilities.multi_asset`.
+
+### 7.6 `GET /api/balance` (modified — breaking)
+
+Was:
+
+```json
+{ "balance": 1234, "username": "alice" }
+```
+
+Becomes:
+
+```json
+{
+ "balances": [
+ { "asset_id": "", "amount": 1234 }
+ ],
+ "username": "alice"
+}
+```
+
+This is a breaking change for single-asset wallets. They MUST gate
+on `Capabilities.multi_asset` and switch parser. There is no
+back-compat shim — the migration is at cutover, the closed
+environment makes it safe (invariant 2).
+
+### 7.7 `POST /api/commit` (unchanged)
+
+Shape unchanged. The underlying proof carries `asset_id` because
+it is now part of `ProofData`, but the commit endpoint's wire
+shape (proof_id + Schnorr commitment) does not.
+
+### 7.8 `GET /api/info` (modified)
+
+`Capabilities` gains `multi_asset`:
+
+```rust
+pub struct Capabilities {
+ pub address_list: bool,
+ pub faucet: bool,
+ pub usernames: bool,
+ pub lnurl: bool,
+ pub multi_asset: bool, // NEW
+}
+```
+
+The `faucet` flag stays for wallet-side back-compat (it has been
+`false` since PR [#73](https://github.com/zk-coins/node/pull/73)
+on both DEV and PRD anyway) but is functionally subsumed by
+`multi_asset = true` once the upgrade lands.
+
+---
+
+## 8. Wallet (client) impact
+
+This document is server-centric. The wallet (`zk-coins/app`)
+adapts in four places; full design is out of scope here.
+
+- **Per-asset balance display.** The wallet's home screen renders a
+ list of `(asset_meta, amount)` rather than a single balance.
+ Drives a `/api/asset/list` fetch on first open and on background
+ refresh; `asset_id → AssetMeta` lookup is cached.
+- **Asset selection in the send flow.** The send screen gains an
+ asset picker. The wallet's existing single-asset send becomes
+ "send the default asset"; the new send-flow is "pick asset,
+ enter amount, recipient".
+- **Create-asset UX.** New screen: name, decimals, initial supply.
+ Signs the genesis request with the wallet's existing key
+ derivation tree — `mint_authority_pubkey` is the wallet's
+ account pubkey, no new key material required.
+- **Schnorr signature scope.** The same BIP-340 key signs over the
+ extended message (now including `asset_id`); no key-management
+ changes.
+
+The current Schnorr-derivation pattern (BIP-32 child key per
+commitment, derivation index = `num_pubkeys - 1`) carries over
+without modification. `asset_id` is an extra field hashed into the
+signed message, not a separate keyspace.
+
+---
+
+## 9. Privacy properties
+
+The trade-off picked by M4 is explicit: per-transaction privacy
+narrows from "anyone on the protocol" to "anyone on this asset".
+
+| Observer learns | From | When |
+| --------------- | ---- | ---- |
+| Transaction exists | On-chain `4242`-prefix inscription | Real-time |
+| `asset_id` of the transaction | Public input of the proof, included in `ProofData` and the inscription's commitment message | Real-time |
+| Transaction count per asset | Aggregate scanner data | Real-time |
+| Total on-chain throughput per asset | Aggregate scanner data | Real-time |
+
+| Observer does **not** learn | Why |
+| --------------------------- | --- |
+| Sender address | Shielded by the SMT/MMR structure (`SPEC.md` §5) |
+| Recipient address | Same |
+| Amount | Same |
+| Cross-asset linkage | Each transition concerns exactly one asset (M5); the wallet does not bundle transactions across assets |
+
+**Anonymity set:** per asset. All transfers of asset X mix
+together; transfers of asset Y are a separate pool because
+`asset_id` is public on the commitment. A new asset with low
+volume has a small anonymity set on day one and grows with
+adoption; this is the privacy/simplicity trade-off the design
+accepts under M4.
+
+**Mitigation paths (out of scope for v1):**
+
+- Per-asset privacy pools with a per-asset SMT and a per-asset
+ MMR. Multiplies state cost by `n_assets`; deferred (§12.10).
+- Hide `asset_id` behind a commitment (Pedersen `Commitment::commit(asset_id, rand)`)
+ in the on-chain inscription. Closes the "asset_id is public"
+ leak at the cost of a `Commitment::commit` opening in every
+ recipient's proof — same shape as the D2/D10 hiding-recipient
+ fix in `SPEC.md` §15. Tracked in §12.11.
+
+The two mitigations compose; they are tracked together in §12.10
+and §12.11.
+
+---
+
+## 10. First-come-first-served namespace enforcement
+
+The mechanics behind decision M3.
+
+- **SQL enforcement.** `assets.name UNIQUE` + `INSERT … ON CONFLICT
+ (name) DO NOTHING` — the same pattern as the username store
+ (see `CONTRIBUTING.md` § "Persistent State" `usernames` row).
+ Whichever genesis transaction commits first wins. Concurrent
+ attempts on the same name receive `409 Conflict`.
+- **No retroactive renaming.** Once `assets.name` is set, it is
+ immutable. The `assets` row is never `UPDATE`d after insert;
+ there is no admin endpoint to rename.
+- **Case-insensitive normalisation.** `name.to_lowercase()` (Rust
+ default, locale-independent Unicode lowercasing) is applied at
+ validation time and at lookup time. This removes the cheapest
+ homograph class (`USDT` vs `usdt` vs `Usdt`) at the cost of
+ ruling out distinct names that differ only in case.
+- **Trade-off acknowledged.** Full homograph defence
+ (`u` vs Cyrillic `u`, zero-width-joiner attacks) is out of scope
+ for v1. The same trade-off applies as in `feedback_dns_migration`
+ — every name shown in the wallet UI MUST be displayed with both
+ `name` and `asset_id` (the asset_id is the trust anchor; the
+ name is UX). Wallets that show only `name` carry the homograph
+ risk.
+
+Race-handling at the database layer is the canonical solution; do
+not rely on application-side locking. Postgres' MVCC guarantees
+that exactly one writer wins the unique-key race; the others'
+`INSERT ... ON CONFLICT (name) DO NOTHING` returns zero affected
+rows, which the handler translates to HTTP 409. This avoids the
+need to catch and re-classify a `23505 unique_violation` —
+matches `db::claim_username` in `node/src/db.rs`.
+
+---
+
+## 11. Mint authority
+
+The mechanics behind decision M2.
+
+- **Genesis pins `mint_authority_pubkey`.** Compressed secp256k1,
+ written into the `assets` row at creation, immutable thereafter.
+- **Subsequent mint signature.** Every `/api/mint` request carries
+ a BIP-340 Schnorr signature over
+ `SHA256("zkcoins:mint" || asset_id || recipient || amount_le ||
+ timestamp_le)`, verified against the asset's
+ `mint_authority_pubkey`. Same secp256k1 primitive as the send
+ signature (`verify_send_signature` in `node/src/server.rs`); no
+ new crypto primitive.
+- **Replay protection.** 5-minute timestamp window
+ (`now.abs_diff(timestamp) > 300 → reject`), matching the
+ existing pattern.
+- **Per-asset request counter.** The `assets.num_pubkeys` column
+ advances per mint (§6.2). The minting account's
+ `prev_commitment_pubkey` is derived from this counter exactly as
+ the existing faucet's `minting_meta.num_pubkeys` does today.
+- **No fixed supply.** The protocol does not enforce a hard cap.
+ Total supply is `initial_supply + Σ(mint amounts)`. Off-chain
+ registries may publish supply caps as a social convention; the
+ protocol does not.
+- **Key rotation is out of scope.** A creator who loses their
+ mint-authority key loses the ability to mint more units. There
+ is no admin override, no rotation endpoint, no escape hatch.
+ Future work — see §12.7.
+
+---
+
+## 12. Open questions / future work
+
+Three groups: open architectural questions the maintainer needs
+to rule on before P2 starts (§12.1 – §12.6), deferred features
+the design explicitly punts on (§12.7 – §12.12), and one
+semantic clarification (§12.13). Bullets follow the shape of
+`BRIDGE_MVP.md` §13.
+
+### 12.1 AssetId pre-image: keep `timestamp` or drop it?
+
+§4.2 includes `timestamp` in the Poseidon pre-image alongside
+`creator_pubkey`, `name`, and `decimals`. The `assets.name UNIQUE`
+constraint (M3 / §10) already enforces first-come-first-served
+name uniqueness at the SQL layer, so `timestamp` is not load-
+bearing for collision resistance on a single instance.
+
+- **Choice in doc:** include `timestamp`. Acts as a provenance
+ marker (off-chain registries learn when the asset was created
+ by inspecting the AssetId) and lets the same `(pubkey, name,
+ decimals)` tuple produce distinct AssetIds across state-wiped
+ test environments.
+- **Alternative:** drop `timestamp`. AssetId becomes a pure
+ function of `(creator_pubkey, name, decimals)`; reproducible
+ across environments; smaller pre-image.
+- **Trade-off:** keeping it costs nothing on-chain (one extra
+ field element in a Poseidon pre-image, already covered by §5.4)
+ and gives a free provenance hint. Dropping it makes AssetIds
+ reproducible across DEV/PRD, which simplifies cross-environment
+ testing but means a wiped DEV that re-creates `("FOO", 8)` from
+ the same creator collides with the old AssetId — fine in
+ practice (state is wiped together) but worth a maintainer call.
+
+### 12.2 Postgres balance shape: JSONB column vs separate table?
+
+§6.2 picks **option (a) — JSONB column on `accounts`**. The
+trade-off is real and the maintainer may prefer (b).
+
+- **Choice in doc:** JSONB column. Composes naturally with the
+ existing `bincode-Account-in-BYTEA` pattern; the JSONB is a
+ side index for `WHERE balances ? ''` queries.
+- **Alternative:** separate `account_balances` table keyed by
+ `(address, asset_id)` with a `BIGINT amount` column. Cleaner
+ for Postgres-side queries (top-holders, distribution
+ histograms, `SUM(amount) WHERE asset_id = X` for total
+ supply audits).
+- **Trade-off:** JSONB minimises moving parts but pushes
+ query complexity into application code. The separate table
+ multiplies writes per state transition (one row per affected
+ asset per account) but makes operational queries trivial. If
+ the maintainer expects significant on-Postgres analytics
+ tooling, switch to (b) before P3 lands.
+
+### 12.3 Wallet rollout coordination for the breaking `/api/balance` shape
+
+§7.6 changes `/api/balance` from `{ balance: u64 }` to `{
+balances: [{ asset_id, amount }] }`. This is the single
+client-visible breaking change in the upgrade.
+
+- **Choice in doc:** gate purely on `Capabilities.multi_asset =
+ true` from `/api/info`. Wallets check the capability flag on
+ every boot and switch their parser accordingly.
+- **Alternative:** add a `version: u32` field to
+ `/api/balance`'s response (and to `/api/info`'s `Capabilities`)
+ so wallets can detect the schema bump even if they fail to
+ re-fetch `/api/info` first. Or: ship both shapes for a
+ cutover window (`balances` and `balance` both populated for
+ N days).
+- **Trade-off:** invariant 2 (closed test environment, DEV and
+ PRD) makes the capability-flag approach safe — there are no
+ external wallets to worry about, and the wallet
+ (zk-coins/app) and server roll out together in lockstep.
+ Adding a version field is belt-and-braces that costs nothing
+ but pollutes the JSON. Recommend keeping capability-flag only
+ unless the maintainer wants the safety net.
+
+### 12.4 Unicode homograph defence beyond `to_lowercase()`?
+
+§10 picks case-insensitive normalisation via `name.to_lowercase()`.
+This defends `USDT` / `Usdt` / `usdt` but not Cyrillic-А (U+0410)
+vs Latin-A (U+0041), zero-width-joiner attacks, or other Unicode
+confusables.
+
+- **Choice in doc:** Rust's locale-independent `to_lowercase()`
+ only. Wallet UI is expected to display both `name` and
+ `asset_id` so the AssetId is the trust anchor.
+- **Alternative:** NFKC normalisation + a Unicode confusables
+ filter (e.g. `unicode-security` crate's `mixed_script_confusable`
+ detection) at the validation stage. Rejects names whose
+ script mix is suspicious; closes the most common phishing
+ vectors at registry-write time.
+- **Trade-off:** `to_lowercase()` alone is cheap and reversible
+ but trusts the wallet UX to enforce the rest. NFKC +
+ confusables is the right long-term answer but adds a
+ dependency and rejects some legitimate names (mixed-script
+ brand names). The current design takes the cheap path and
+ treats the AssetId as the trust anchor; if mainnet hardening
+ ever lands, revisit at the namespace-governance step.
+
+### 12.5 `"zkcoins:send"` domain-tag: keep, drop, or version?
+
+§4.4 introduces a `"zkcoins:send"` domain-separation prefix on
+the send-signature hash. Current `verify_send_signature` signs
+without a prefix.
+
+- **Choice in doc:** add the prefix as defense-in-depth, mirroring
+ the `"zkcoins:mint"` and `"zkcoins:asset-genesis"` prefixes
+ on the other two message types.
+- **Alternative:** keep the unprefixed shape and only add
+ `asset_id` to the existing fields. Simpler diff against the
+ current `verify_send_signature`; one fewer thing for the
+ wallet to update.
+- **Trade-off:** the prefix prevents future cross-message
+ signature reuse (e.g. a malicious peer convincing a wallet to
+ sign what looks like a send but is actually a mint over the
+ same key material). Under invariant 2 (closed environment),
+ the attack surface is low — but the prefix is free at
+ signing time and the wallet update is a single hashing tweak
+ bundled with the `asset_id` widening. Recommend keeping
+ unless the maintainer objects to the broader signature
+ shape change.
+
+### 12.6 Off-circuit vs in-circuit Schnorr for the mint branch
+
+§5.3 picks off-circuit Schnorr verify for the mint and genesis
+branches. The asset registry is server state, not on-chain state.
+
+- **Choice in doc:** off-circuit verify via existing
+ `secp.verify_schnorr`. The in-circuit branch only enforces
+ that the proof's `mint_authority_pubkey` matches the
+ registry value.
+- **Alternative:** in-circuit BIP-340 Schnorr-on-secp256k1
+ gadget. Verifies the mint signature inside the proof itself;
+ removes the server-state trust assumption.
+- **Trade-off:** in-circuit Schnorr-on-secp256k1 is non-trivial
+ in Plonky2 (`MIGRATION_RESEARCH.md` §5.4 has the analysis).
+ For the closed test environment (invariant 2), off-circuit
+ is sufficient. If a future deployment treats minting as a
+ bridge primitive or moves to a trust-minimised setting, this
+ decision flips and the gadget cost lands in the prover
+ budget.
+
+### 12.7 Key rotation for mint authority (deferred feature)
+
+If a creator loses their signing key (or wants to migrate to a
+new one), the asset is effectively frozen at its current supply.
+A rotation mechanism — signed by the old key, written as an
+`assets.rotation_pubkey` column — is the obvious extension. Out
+of scope for v1 to keep the genesis path immutable; revisit
+once a real key-loss event lands.
+
+### 12.8 Richer on-chain metadata (deferred feature)
+
+Logos, URIs, descriptions, social links. M6 explicitly excludes
+these — they live in an off-chain registry the wallet consults
+by `asset_id`. The on-chain genesis stays small.
+
+### 12.9 Cross-asset atomic swap inside zkCoins (deferred feature)
+
+M5 defers this. Trading happens on a separate DEX layer; the
+BitVM2 bridge (`BRIDGE_MVP.md`) and the Lightning atomic swap
+layer (`LIGHTNING_ATOMIC_SWAP.md`) are the canonical
+out-of-protocol paths.
+
+### 12.10 Per-asset privacy pools (deferred feature)
+
+M4 picks the shared-pool design for simplicity. A per-asset
+SMT + per-asset MMR raises anonymity-set per asset to "the
+asset's own traffic, hidden from other assets' traffic" — same
+as Tornado-style pool separation. Cost: multiplies state and
+Bitcoin-side commitment traffic by `n_assets`. Deferred.
+
+### 12.11 Hiding `asset_id` on-chain (deferred feature)
+
+Combines with the D2/D10 hiding-recipient fix in `SPEC.md` §15.
+Out of scope for v1; tracked alongside the mainnet-blocker
+privacy fixes. Closes the "asset_id is public on every
+commitment" leak at the cost of a `Commitment::commit` opening
+in every recipient's proof.
+
+### 12.12 Burn (asset deflation) (deferred feature)
+
+Not in MVP. If a future creator wants explicit burn, the
+cleanest design is a sentinel recipient address (`BURN_ADDRESS
+= HashDigest::ZERO` or a domain-separated constant) that the
+circuit treats as a coin sink with no corresponding
+`apply_coin`. Adds one branch in
+`account_node::receive_coin`. Defer until a real use case
+arrives.
+
+### 12.13 Decimals semantics (clarification)
+
+Purely UX-display. The on-chain `amount` is a `u64`; the
+wallet formats with `decimals` for display only. No on-chain
+math change. The protocol does not enforce that `amount %
+10**decimals` makes sense.
+
+---
+
+## 13. Implementation order
+
+Phased rollout, mapped to PR boundaries. Effort estimates are
+qualitative (S = small, M = medium, L = large, XL = extra large)
+per the convention in `BRIDGE_MVP.md` §12.1.
+
+| Phase | Scope | Effort | Risk |
+| ----- | ----- | ------ | ---- |
+| **P1 — Shared types + AssetId plumbing** | `shared/src/lib.rs` gains `AssetId`, `AssetMeta`; `Invoice` gains `asset_id`; `program-plonky2/src/types.rs::Coin`/`CoinTemplate` gain `asset_id`. No behaviour change yet — the field is propagated but the server defaults it to a placeholder `DEFAULT_ASSET_ID` so existing tests pass unchanged. Drop in a `MULTI_ASSET_FIXME` comment at every site that will need real handling in P5. | **S** | Low — mechanical |
+| **P2 — Circuit extension** | `program-plonky2/src/circuit/main.rs`: bump `N_PROOF_DATA_PUBLIC_INPUTS` to 20, add `asset_id` public input, add per-slot masked-equality gates, extend `calculate_coin_identifier`. Re-run `recursion_shape_probe::dump_phase_2a_pad_bits_sweep` to confirm padding still fits. Coverage gate stays at 100%. The single heaviest lift. | **L** | Medium — cyclic-recursion padding may shift |
+| **P3 — Asset registry endpoints** | `POST /api/asset/create`, `GET /api/asset/list`, `GET /api/asset/info/:id_or_name`. New `assets` table migration. SQL `name UNIQUE` enforcement. Handler tests for the 409-on-conflict race. | **M** | Low — standard HTTP API extension |
+| **P4 — Mint signature verification** | `POST /api/mint` switches from faucet to signed creator-mint. Per-asset `num_pubkeys` counter. The faucet shortcut is removed; the always-on `Capabilities.faucet` is rewired to `multi_asset`. | **M** | Medium — replaces a known-good code path; tests must cover the per-asset replay protection |
+| **P5 — Send + balance + commit shape** | `POST /api/send` extends signed message, `GET /api/balance` becomes per-asset map, single-asset off-circuit pre-check enforces M5, `Capabilities.multi_asset = true`. Backfill the `MULTI_ASSET_FIXME` sites from P1. | **L** | Medium — multiple coupled changes, all wallet-visible |
+| **P6 — Wallet adaptation** | `zk-coins/app`: balance display, send-flow asset picker, create-asset UX. Separate PR(s) in the app repo, gated on `Capabilities.multi_asset` from the server's `/api/info`. | **L** | Medium — UX-heavy, parallel to server work |
+
+**Aggregate effort: M + L + M + M + L + L ≈ 4 person-months at
+full focus.** Phase 1 can begin immediately; Phase 2 is the heavy
+lift and gates Phases 3 onward.
+
+Per [`CONTRIBUTING.md`](./CONTRIBUTING.md) invariant 4, every
+phase ships with 100% test coverage on the activated surface
+(`cargo llvm-cov --fail-under-lines 100 -- --test-threads=1` from
+inside the affected crate). Negative tests — proof rejection when
+in-coin `asset_id` differs from out-coin `asset_id`, signature
+verification failure on a forged mint, 409 on duplicate name — are
+mandatory.
+
+---
+
+## 14. Non-Goals (Restated)
+
+So nobody scope-creeps:
+
+- Migrating existing single-asset state — **not in v1** (closed
+ test environment, state-wipe at cutover per invariant 2).
+- Per-asset privacy pools — **deferred** (§12.10, decision M4).
+- Cross-asset atomic swaps inside zkCoins — **out of protocol**
+ (decision M5, §12.9; lives in the BitVM bridge / Lightning
+ swap docs).
+- Rich on-chain metadata (logo, URI, description) — **excluded**
+ (decision M6, §12.8).
+- Mint-authority key rotation — **deferred** (§11, §12.7).
+- Burn / deflationary mechanics — **not in MVP** (§12.12).
+- In-circuit BIP-340 Schnorr verify for the mint branch —
+ **open architectural call** (§5.3, §12.6).
+- Homograph-attack defence beyond `to_lowercase()` normalisation —
+ **open architectural call** (§10, §12.4).
+
+---
+
+## 15. References
+
+- [`SPEC.md`](./SPEC.md) — single-asset protocol specification.
+ Multi-asset is additive to §3 (Account Model), §4 (Merkle
+ Structures), §7 (Program Inputs), §8 (Circuit Logic), §9
+ (Public Output).
+- [`MIGRATION_RESEARCH.md`](./MIGRATION_RESEARCH.md) — Plonky2
+ rationale, §5 (locked decisions), §7 (lessons learned).
+ Multi-asset extends the §5-style decisions list; the §7.22
+ cyclic-recursion padding methodology applies to verifying the
+ new public-input count against `INNER_PAD_BITS_STAGE_5D_NEXT_5`.
+- [`ROADMAP.md`](./ROADMAP.md) — status tracker. Add a row per
+ phase from §13 once implementation starts.
+- [`BRIDGE_MVP.md`](./BRIDGE_MVP.md) — structural reference for
+ this document.
+- [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) — the
+ out-of-protocol cross-asset trading layer.
+- [`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md) — the BTC-side
+ cross-asset trading layer.
+- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — project invariants,
+ decision recipe, pre-push checklist.
+- `program-plonky2/src/circuit/main.rs` — circuit entry point;
+ see `N_PROOF_DATA_PUBLIC_INPUTS`, `MAX_IN_COINS`, `MAX_OUT_COINS`,
+ `INNER_PAD_BITS_STAGE_5D_NEXT_5`.
+- `program-plonky2/src/types.rs` — `Coin`, `CoinTemplate`,
+ `AccountState`, `ProofData`, `calculate_coin_identifier`.
+- `shared/src/lib.rs` — `Invoice`, `ClientAccount::create_commitment`.
+- `node/src/account_node.rs` — `Account`, `send_coins`, the
+ off-circuit pre-check pattern that the new single-asset
+ invariant follows.
+- `node/src/server.rs` — `verify_send_signature` (mint signature
+ follows the same 5-minute replay window and message-hash
+ pattern), `Capabilities`.
+
+---
+
+## 16. Change Log
+
+| Date | Change |
+| ---- | ------ |
+| 2026-05-22 | Initial draft. |
diff --git a/README.md b/README.md
index 1b1d13bb..136b8b3c 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,57 @@
-# zkCoins Server
+# zkCoins Node
+
+[](https://hub.docker.com/r/zkcoins/node)
+[](https://hub.docker.com/r/zkcoins/node)
Rust/Axum backend for [zkcoins.app](https://zkcoins.app) — account management, ZK proof generation, Bitcoin blockchain scanning, and nullifier publishing.
+Container images: **[hub.docker.com/r/zkcoins/node](https://hub.docker.com/r/zkcoins/node)**
+
## Live
-| Environment | URL | Image |
-| ----------- | -------------------------------------------------- | ---------------------- |
-| **PRD** | [api.zkcoins.app](https://api.zkcoins.app) | `zkcoin/server:latest` |
-| **DEV** | [dev-api.zkcoins.app](https://dev-api.zkcoins.app) | `zkcoin/server:beta` |
+| Environment | URL | Image |
+| ----------- | -------------------------------------------------- | ------------------------------------------------------------------------------------ |
+| **PRD** | [api.zkcoins.app](https://api.zkcoins.app) | [`zkcoins/node:latest`](https://hub.docker.com/r/zkcoins/node/tags?name=latest) |
+| **DEV** | [dev-api.zkcoins.app](https://dev-api.zkcoins.app) | [`zkcoins/node:beta`](https://hub.docker.com/r/zkcoins/node/tags?name=beta) |
## Stack
| Layer | Technology | Why |
| --------------- | -------------------- | ---------------------------------------------------- |
-| Language | Rust 1.81 | Same as ZK circuits, memory safety, performance |
+| Language | Rust nightly | Required for Plonky2 (`feature(specialization)`) |
| Web framework | Axum | Built on Tokio, idiomatic async Rust |
-| ZK Proofs | SP1 zkVM | Write proofs in standard Rust, no DSL |
-| Data structures | SMT + MMR | Non-inclusion proofs + append-only history |
+| ZK Proofs | Plonky2 + Poseidon-Goldilocks (cyclic recursion) | Server-side, no zkVM, no external prover dependency |
+| Data structures | SMT + MMR (Poseidon) | Non-inclusion proofs + append-only history |
| Bitcoin | Taproot Inscriptions | 64-byte nullifiers, Esplora API scanning |
| Bitcoin index | electrs (Esplora) | Esplora REST API via shared Docker network `bitcoin` |
Full rationale: [docs.zkcoins.app/tech-decisions](https://docs.zkcoins.app/tech-decisions)
+## Trust Model
+
+Proof generation runs **inside this server process**. `AccountNode::send_coins` (`node/src/account_node.rs`) calls `self.prover.prove_account_update_with_in_and_out_coins_and_sources(...)` (and the `prove_initial_*` variant for first-time accounts) on every send / receive / mint. ZK proving requires the full private witness, so the server sees, in cleartext:
+
+- Sender, recipient, and amount of every coin movement
+- The complete in-coin / out-coin / source-aggregator slot layout per account
+- Account history roots, Merkle proofs, and inclusion-proof witnesses
+- Usernames and their bound coin sets (`UsernameStore`)
+- Postgres rows persisting all of the above (`node/migrations/000{1,2}_*.sql`)
+
+The **on-chain footprint stays private** — Plonky2 ensures that the public outputs (nullifiers, history roots, Taproot inscriptions) carry no readable transaction data. Block explorers and chain analytics see only opaque 64-byte commitments. The trust boundary is therefore the **server operator**, not the chain.
+
+| | Hosted (`api.zkcoins.app`) | Self-hosted |
+| --- | --- | --- |
+| On-chain privacy (vs. block explorers) | ✅ | ✅ |
+| Operator sees plaintext transaction data | ❌ Yes — DFX runs the hosted node | ✅ No |
+| Setup effort | ✅ None | ⚠️ Postgres + electrs + Bitcoin node |
+
+**If you need full transaction privacy, run your own server.** Every release is shipped as `zkcoins/node:latest` (see [Live](#live)), the build recipe is [`Dockerfile`](./Dockerfile), and runtime knobs are documented in [Configuration](#configuration). Point the [zkcoins.app](https://zkcoins.app) client at your self-hosted instance for end-to-end self-custody of transaction data.
+
## Contributing
-**New PRs may only merge into `develop` if test coverage is 100% on the activated surface.** Code behind a Cargo feature (`address-list`, `faucet`, `usernames`, `lnurl`) is excluded from the MVP measurement — feature-gated routes do not need to be tested as long as the feature stays off in the PRD build. Concretely:
+**New PRs may only merge into `develop` if test coverage is 100% on the activated surface.** Code behind a Cargo feature (`address-list`, `lnurl`) is excluded from the MVP measurement — feature-gated routes do not need to be tested because both DEV and PRD ship the MVP-only binary with every Cargo feature off. (Mint and usernames are part of the MVP and are permanently compiled in — no Cargo feature gate.) Concretely:
-- `cargo llvm-cov -p server` (no `--all-features`) must report 100% lines, statements, branches, and functions on the MVP build. CI enforces this with `--fail-under-lines 100`. The current baseline is below 100% — the regression-block threshold is set to the current measured value and the goal is to lift it to 100% via follow-up PRs.
+- `cargo llvm-cov -p node` (no `--all-features`) must report 100% lines + 100% functions on the activated MVP surface. CI enforces this with `--fail-under-lines 100 --fail-under-functions 100` in the `Coverage Gate (100% lines + functions)` job. The current `develop` baseline is at the gate.
- Defensive code that genuinely cannot be reached in unit tests (e.g. the publisher's Bitcoin-broadcast path that requires a signet/regtest node, the `main.rs` runtime bootstrap) is excluded from the measured scope at the file level rather than tested.
- The branch is protected on GitHub: a PR cannot be merged while CI is red.
@@ -40,47 +65,45 @@ API endpoints, background services, their activation status, and the tests that
**Triage legend** (MVP testing decision): `mvp` = in MVP scope, must reach full test coverage before launch · `gate` = not in MVP scope; hidden behind a Cargo feature, default off, no test coverage required · `planned` = not in scope for MVP.
-**Coverage legend:** unit % refers to `cargo-llvm-cov` line coverage of the module that implements the function (latest run, `SP1_PROVER=mock` with `--all-features`). `—` means no test exists.
+**Coverage legend:** unit % refers to `cargo-llvm-cov` line coverage of the module that implements the function. The MVP-scope per-module summary is in § "Test stack" below; the authoritative live numbers are in the `Coverage Gate` CI job. `—` means no test exists.
| Function | Trigger | Status | Triage | Tests |
| ------------------------------------ | ------------------------------------- | ------------------------ | ------- | ----------------------------- |
-| Health check | `GET /health` | always | mvp | 75% (server) |
-| Network info | `GET /api/info` | env¹ | mvp | 75% (server) |
-| Get balance | `GET /api/balance?address=` | always | mvp | 75% (server) |
-| List all addresses | `GET /api/address` | feature (`address-list`) | gate | 75% (server) |
-| Mint coins (faucet, single-phase) | `POST /api/mint` | feature (`faucet`)² | gate | 91% (account) |
-| Send — phase 1 (generate proof) | `POST /api/send` | env² | mvp | 75% (server) |
-| Send — phase 2 (commit + broadcast) | `POST /api/commit` | env³ | mvp | 75% (server) · 0% (publisher) |
-| Receive coin | `POST /api/receive` | always | mvp | 91% (account) |
-| Download coin proof | `GET /api/proof/:id` | always | mvp | 75% (server) |
-| Claim username | `POST /api/username/claim` | feature (`usernames`) | gate | 98% (username) |
-| Resolve username | `GET /api/username/resolve/:username` | feature (`usernames`) | gate | 98% (username) |
-| LNURL-Pay metadata | `GET /.well-known/lnurlp/:username` | feature (`lnurl`) | gate | 75% (server) |
-| LNURL-Pay callback | `GET /lnurl/pay/:username` | feature (`lnurl`) | gate | 75% (server) |
-| Bitcoin block scanner (background) | Loop in `main.rs`, 30 s poll | env⁴ | mvp | 51% (scanner) · 4% (main) |
-| State persistence (SMT/MMR write) | Scanner callback on commitment match | always | mvp | 97% (state) |
+| Health check | `GET /health` | always | mvp | 100% (router) |
+| Network info | `GET /api/info` | env¹ | mvp | 100% (router) |
+| Get balance | `GET /api/balance?address=` | always | mvp | 100% (router) |
+| List all addresses | `GET /api/address` | feature (`address-list`) | gate | 100% (router) |
+| Mint coins (single-phase) | `POST /api/mint` | always² | mvp | 100% (account_node) |
+| Send — phase 1 (generate proof) | `POST /api/send` | env² | mvp | 100% (router) |
+| Send — phase 2 (commit + broadcast) | `POST /api/commit` | env³ | mvp | 100% (router) · 0% (publisher) |
+| Receive coin | `POST /api/receive` | always | mvp | 100% (account_node) |
+| Download coin proof | `GET /api/proof/:id` | always | mvp | 100% (router) |
+| Claim username | `POST /api/username/claim` | always | mvp | 100% (username) |
+| Resolve username | `GET /api/username/resolve/:username` | always | mvp | 100% (username) |
+| LNURL-Pay metadata | `GET /.well-known/lnurlp/:username` | feature (`lnurl`) | gate | 100% (router) |
+| LNURL-Pay callback | `GET /lnurl/pay/:username` | feature (`lnurl`) | gate | 100% (router) |
+| Bitcoin block scanner (background) | WS subscription in `scanner_ws.rs` | env⁴ | mvp | 100% (scanner) · — (main, excluded) |
+| State persistence (SMT/MMR write) | Scanner callback on commitment match | always | mvp | 100% (state) |
| Taproot inscription broadcast | Called by `/api/commit` | env³ | mvp | 0% (publisher) |
| Publisher UTXO lookup | Internal, before broadcast | env³ | mvp | 0% (publisher) |
| Explorer endpoints (`/api/stats`, …) | n/a | planned | planned | — |
| Light client support | n/a | planned | planned | — |
¹ `NETWORK_NAME` env var controls the string returned. `IS_MAINNET=true` flips the default to `"Mainnet"`.
-² Proof generation routes through SP1. `SP1_PROVER=mock` skips real proving; `cpu`/`cuda`/`network` perform actual proving (latency and resource cost vary by stage — see [Proving Strategy](#proving-strategy)).
-³ Requires `PUBLISHER_KEY` set to a real funded key and `ESPLORA_URL` reachable. With the default test key the server panics on `IS_MAINNET=true` startup; on testnet it accepts the call but broadcast will fail without funded UTXOs.
-⁴ Scanner depends on `ESPLORA_URL` being reachable; on connection failure it backs off and retries.
+² Proof generation routes through the Plonky2 cyclic-recursion circuit. Single host, single Rust process — no zkVM, no external prover service. Mac Studio M3 Ultra is the production hardware target (96 GB unified memory, no external GPU). See [Proving Strategy](#proving-strategy).
+³ Requires `PUBLISHER_KEY` set to a real funded key and `ESPLORA_URL` reachable. With the default test key the server panics on `IS_MAINNET=true` startup; on testnet it accepts the call but broadcast will fail without funded UTXOs — DEV and PRD both return `503 SERVICE_UNAVAILABLE` to the client on broadcast failure (the historic `DEV_SKIP_BROADCAST_FAILURE` env-gate that silently swallowed these failures was removed once DEV and PRD were unified on the MVP-only binary; the DEV publisher wallet therefore has to be funded for E2E paths).
+⁴ Scanner depends on `ESPLORA_URL` (REST, used for the per-block `get_block_txids` / `get_tx` lookups and for the post-reconnect tip anchor) AND `ESPLORA_WS_URL` (WebSocket, used by `scanner_ws` to receive new-tip events — issue #84). Both default to mutinynet endpoints; on connection failure the WS subscriber reconnects with exponential backoff capped at 30 s.
### Cargo features
-All non-MVP routes are gated by Cargo features so the disabled handler functions, helper structs, and `AppState` fields are excluded from the binary at compile time. With a feature off, the route is never registered and the fallback responds with `404`. There is no runtime path that can reach a disabled handler. Defaults are empty (fail-closed): the PRD image build passes no features, the DEV image build passes all four.
+All non-MVP routes are gated by Cargo features so the disabled handler functions, helper structs, and `AppState` fields are excluded from the binary at compile time. With a feature off, the route is never registered and the fallback responds with `404`. There is no runtime path that can reach a disabled handler. Defaults are empty (fail-closed): **both the DEV and the PRD image builds pass no features**, so the two environments run the identical MVP-only binary. The Cargo flags exist for self-hosters who want to compile a binary with a specific non-MVP subset enabled, and for future per-feature rollouts when an individual feature is deemed ready for production.
| Feature | Gates |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address-list` | `GET /api/address` |
-| `faucet` | `POST /api/mint`, `MintRequest`, `AppState::minting_account` |
-| `usernames` | `POST /api/username/claim`, `GET /api/username/resolve/:u`, `ClaimUsernameRequest`, `UsernameStore::{claim,save_to_file}`, `AppState::usernames_path` |
-| `lnurl` | `GET /.well-known/lnurlp/:u`, `GET /lnurl/pay/:u` (depends on `usernames`) |
+| `lnurl` | `GET /.well-known/lnurlp/:u`, `GET /lnurl/pay/:u` |
-Build the MVP-only binary (PRD): `cargo build --release -p server`. Build with everything enabled (DEV / tests): `cargo build --release -p server --all-features`. The Docker `FEATURES` build arg accepts a comma-separated list and is forwarded to `cargo build --features`.
+Build the MVP-only binary (DEV + PRD ship this): `cargo build --release -p node`. Build with every feature enabled (CI clippy + tests + self-host opt-in): `cargo build --release -p node --all-features`. The Docker `FEATURES` build arg accepts a comma-separated list and is forwarded to `cargo build --features`; both `deploy-dev.yaml` and `deploy-prd.yaml` leave it empty.
### Triage gaps
@@ -96,87 +119,87 @@ Features tagged `mvp` whose current test coverage is insufficient — these bloc
#### Health check
-- **Module:** `server.rs::main_app` route handler
+- **Module:** `router.rs::main_app` route handler
- **Behaviour:** returns the literal string `"ok"` with HTTP 200
-- **Tests:** `server.rs::tests::health_returns_ok`
+- **Tests:** `router.rs::tests::health_returns_ok`
#### Network info
-- **Module:** `server.rs::info_handler`
-- **Behaviour:** returns `{ "network": NETWORK_NAME }`. `NETWORK_NAME` defaults to `Mutinynet` when `IS_MAINNET=false`, `Mainnet` when `true`
-- **Tests:** `server.rs::tests::info_returns_network_name`
+- **Module:** `router.rs::info_handler`
+- **Behaviour:** returns `{ network, capabilities: { address_list, faucet, usernames, lnurl }, username_domain }`. `network` defaults to `Mutinynet` when `IS_MAINNET=false`, `Mainnet` when `true`. `capabilities.{address_list,lnurl}` each reflect whether the corresponding Cargo feature was compiled into this binary, letting clients gate UI on a single server-side source of truth instead of parallel build-time env flags. `capabilities.{faucet,usernames}` are hardcoded `true` — mint and usernames are permanent MVP — and are retained only for back-compat with wallet clients that deserialise the shape. `username_domain` is the external hostname this server serves; **required env var** (server panics on startup if unset). PRD sets `USERNAME_DOMAIN=zkcoins.app`, DEV sets `USERNAME_DOMAIN=dev.zkcoins.app` — distinct from `network` because the same chain can be served from two isolated external hostnames, and the client renders `@` from this field
+- **Tests:** `router.rs::tests::info_returns_network_name_capabilities_and_username_domain`, `router.rs::tests::info_serialization_format_is_stable`
#### Get balance
-- **Module:** `server.rs::get_balance_handler` → `account_server.rs::AccountServer::get_account_balance`
-- **Behaviour:** address parsed as hex pubkey, looks up the account. Returns `{ balance, username? }`. The minting address returns `u64::MAX`
-- **Tests:** `server.rs::tests::balance_*` (5 tests covering happy path, unknown address, invalid hex, missing param, wrong length)
+- **Module:** `router.rs::get_balance_handler` → `account_node.rs::AccountNode::get_account_balance`
+- **Behaviour:** address parsed as hex pubkey, looks up the account. Returns `{ balance, username? }`. A well-formed address with no on-chain activity yields `200 OK` with `balance: 0` (canonical zero state, not 404). The minting address returns `u64::MAX`. Malformed input — invalid hex, wrong length, or a missing `address` query parameter — returns `422`
+- **Tests:** `router.rs::tests::balance_*` (6 tests covering happy path, unknown address with and without a claimed username, invalid hex, missing param, wrong length)
#### List all addresses
-- **Module:** `server.rs::get_address_handler` → `account_server.rs::AccountServer::get_addresses`
+- **Module:** `router.rs::get_address_handler` → `account_node.rs::AccountNode::get_addresses`
- **Behaviour:** returns all known addresses as hex strings. Intended for explorer/debug use, not user-facing
-- **Tests:** `server.rs::tests::address_returns_list`
+- **Tests:** `router.rs::tests::address_returns_list`
-#### Mint coins (faucet, single-phase)
+#### Mint coins (single-phase)
-- **Module:** `server.rs::mint_handler` → `account_server.rs::send_coins` with the server-held minting account
+- **Module:** `router.rs::mint_handler` → `account_node.rs::send_coins` with the server-held minting account
- **Behaviour:** server signs commitment itself (no client roundtrip) using the minting key
-- **Proof generation:** `zkcoins_prover::Prover::create_account` (or `update_account` for the receiver) under SP1
-- **Tests:** `account_server.rs::tests::test_create_minting_account`, `test_mint_single_invoice`, `test_mint_repro_live_setup`
+- **Proof generation:** `zkcoins_prover::Prover` (the Plonky2 wrapper in [`script-plonky2/`](./script-plonky2/)) — `prove_initial` for new accounts, `prove_account_update` for receivers
+- **Tests:** `account_node.rs::tests::test_create_minting_account`, `test_mint_single_invoice`, `test_mint_repro_live_setup`
#### Send — phase 1 (generate proof)
-- **Module:** `server.rs::send_coin_handler` → `verify_send_signature` (Schnorr over `SHA256(account_address || recipient || amount || timestamp)`, ±5 min skew) → `account_server.rs::send_coins`
+- **Module:** `router.rs::send_coin_handler` → `verify_send_signature` (Schnorr over `SHA256(account_address || recipient || amount || timestamp)`, ±5 min skew) → `account_node.rs::send_coins`
- **Behaviour:** returns `{ proof_id, account_state_hash, output_coins_root }`. Proof is persisted under `data/proofs/.bin` for later commit
-- **Tests:** request-layer tests in `server.rs::tests::send_*` and `send_signature_*` (12 tests covering parser, signature verification, replay). Proof generation itself is not exercised — tests run with `SP1_PROVER=mock`
+- **Tests:** request-layer tests in `router.rs::tests::send_*` and `send_signature_*` (12 tests covering parser, signature verification, replay). Proof generation itself is not exercised — the Plonky2 cyclic-recursion build is too slow for unit tests (~3–15 min per prove at production parameters); positive proofs are exercised in `program-plonky2/` directly
#### Send — phase 2 (commit + broadcast)
-- **Module:** `server.rs::commit_handler` → `publisher.rs::create_and_broadcast_inscription`
-- **Behaviour:** verifies the client's Schnorr commitment, builds a Taproot commit+reveal tx pair, mines a txid prefix `4242` (max 400 000 attempts in `publisher.rs::inscription_txs`), broadcasts both txs, then calls `account_server.rs::receive_coin` to deliver the coin to the recipient
-- **Tests:** `server.rs::tests::commit_missing_body_returns_error`, `commit_nonexistent_proof_id_returns_404`. **No happy-path broadcast test** — would require a live Bitcoin signet/regtest
+- **Module:** `router.rs::commit_handler` → `publisher.rs::create_and_broadcast_inscription`
+- **Behaviour:** verifies the client's Schnorr commitment, builds a Taproot commit+reveal tx pair, mines a txid prefix `4242` (max 400 000 attempts in `publisher.rs::inscription_txs`), broadcasts both txs, then calls `account_node.rs::receive_coin` to deliver the coin to the recipient
+- **Tests:** `router.rs::tests::commit_missing_body_returns_error`, `commit_nonexistent_proof_id_returns_404`. **No happy-path broadcast test** — would require a live Bitcoin signet/regtest
#### Receive coin
-- **Module:** `server.rs::receive_coin_handler` → `account_server.rs::receive_coin`
+- **Module:** `router.rs::receive_coin_handler` → `account_node.rs::receive_coin`
- **Behaviour:** replay-protected via per-account `coin_history` SMT
-- **Tests:** `account_server.rs::tests::test_receive_duplicate_coin_rejected`, `test_receive_updates_balance`
+- **Tests:** `account_node.rs::tests::test_receive_duplicate_coin_rejected`, `test_receive_updates_balance`
#### Download coin proof
-- **Module:** `server.rs::get_proof_handler` → `ProofStore::get_proof`
+- **Module:** `router.rs::get_proof_handler` → `ProofStore::get_proof`
- **Behaviour:** streams the binary serialised `CoinProof` (`Vec` from bincode) with content-type `application/octet-stream`
-- **Tests:** `server.rs::tests::proof_not_found_returns_404`
+- **Tests:** `router.rs::tests::proof_not_found_returns_404`
#### Claim username
-- **Module:** `server.rs::claim_username_handler` → `username.rs::UsernameStore::claim`
-- **Behaviour:** verifies Schnorr signature over `SHA256(username || pubkey || timestamp)` (5 min skew); writes to `usernames.bin` (atomic)
-- **Tests:** `server.rs::tests::claim_username_*` (3 tests) + `username.rs::tests::*` (8 tests covering valid charset, duplicates, persistence)
+- **Module:** `router.rs::claim_username_handler` → `username.rs::UsernameStore::claim`
+- **Behaviour:** verifies Schnorr signature over `SHA256(username || pubkey || timestamp)` (5 min skew); persists to the Postgres `usernames` table via `db::claim_username` (`INSERT … ON CONFLICT DO NOTHING`)
+- **Tests:** `router.rs::tests::claim_username_*` (3 tests) + `username.rs::tests::*` (8 tests covering valid charset, duplicates, persistence)
#### Resolve username
-- **Module:** `server.rs::resolve_username_handler` → `username.rs::UsernameStore::resolve`
+- **Module:** `router.rs::resolve_username_handler` → `username.rs::UsernameStore::resolve`
- **Behaviour:** if exact username unknown, falls back to hex prefix matching against known addresses. Case-insensitive
-- **Tests:** `server.rs::tests::resolve_unknown_username_returns_404`, `resolve_minting_address_by_hex_prefix`, `username.rs::tests::resolve_is_case_insensitive`
+- **Tests:** `router.rs::tests::resolve_unknown_username_returns_404`, `resolve_minting_address_by_hex_prefix`, `username.rs::tests::resolve_is_case_insensitive`
#### LNURL-Pay metadata and callback
-- **Module:** `server.rs::lnurlp_handler`, `server.rs::lnurl_callback_handler`
+- **Module:** `router.rs::lnurlp_handler`, `router.rs::lnurl_callback_handler`
- **Behaviour:** thin stub implementation of [LNURL-pay](https://github.com/lnurl/luds/blob/luds/06.md). Metadata returned for known usernames; callback returns a phase-2 error (not wired to a real BOLT-11 invoice generator yet)
-- **Tests:** `server.rs::tests::lnurlp_known_address_returns_pay_request`, `lnurlp_unknown_user_returns_404`, `lnurl_pay_callback_returns_phase2_error`
+- **Tests:** `router.rs::tests::lnurlp_known_address_returns_pay_request`, `lnurlp_unknown_user_returns_404`, `lnurl_pay_callback_returns_phase2_error`
#### Bitcoin block scanner
- **Module:** `scanner.rs::scan_for_inscriptions` / `InscriptionScanner::scan_from_block`. Loop spawned from `main.rs::main`. State saved between runs in `data/latest_block.bin`
-- **Behaviour:** polls Esplora; filters txs by txid prefix `4242`; extracts Taproot inscription content via `extract_inscription_content`; deserialises as `Commitment`; calls callback in `main.rs` which verifies the signature and updates state
+- **Behaviour:** subscribes to the Esplora WebSocket (`scanner_ws.rs`, `ESPLORA_WS_URL`) for new tip events; drains the resulting mpsc channel in `scanner_runtime.rs`, walking forward through `block_status.next_best`; filters txs by txid prefix `4242`; extracts Taproot inscription content via `extract_inscription_content`; deserialises as `Commitment`; calls callback in `main.rs` which verifies the signature and updates state. Polling was removed in [issue #84](https://github.com/zk-coins/node/issues/84); see [CONTRIBUTING.md § "No polling — events only"](./CONTRIBUTING.md#no-polling--events-only) for the CI lint that enforces this
- **Tests:** `scanner.rs::tests::parse_valid_inscription_into_commitment`, `reject_invalid_inscription_data`, `verify_commitment_signature_after_deserialization`, `parse_multi_chunk_inscription`. **No integration test** with a real Bitcoin block
#### State persistence (SMT/MMR write)
-- **Module:** `state.rs::State::update` (atomic writes via `atomic_write` helper)
-- **Behaviour:** on each verified commitment: append SMT root to MMR, persist `smt.bin`, `mmr.bin`, `latest_block.bin`
+- **Module:** `state.rs::State::update` + scanner callback in `main.rs` → `db::persist_state_tx`
+- **Behaviour:** on each verified commitment: append SMT root to MMR, then atomically upsert the SMT bytes, MMR bytes, and last-processed block hash inside a single `BEGIN; UPSERT; UPSERT; UPSERT; COMMIT` against Postgres (issue #11 fix). Replaces the pre-migration `smt.bin` / `mmr.bin` / `latest_block.bin` sibling files
- **Tests:** `state.rs::tests::*` (9 tests covering single + multiple updates, persistence roundtrip, proof generation/verification, empty MMR edge cases)
#### Taproot inscription broadcast and Publisher UTXO lookup
@@ -187,17 +210,18 @@ Features tagged `mvp` whose current test coverage is insufficient — these bloc
#### Planned
-- **Explorer endpoints (`/api/stats`, `/api/nullifiers`)** — to power an `explorer.zkcoins.app` companion app
+- **Explorer endpoints (`/api/stats`, `/api/nullifiers`)** — to power the `zkcoins.space` companion app
- **Light client support** — let wallets verify nullifier set membership without scanning the chain themselves
### Configuration
| Variable | Default | Effect |
| --------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `SP1_PROVER` | `cpu` | `mock` (no real proofs, instant), `cpu`, `cuda`, `network`. Tests run with `mock`. |
-| `ESPLORA_URL` | `https://mutinynet.com/api` | Esplora API endpoint (electrs or public) |
+| `ESPLORA_URL` | `https://mutinynet.com/api` | Esplora REST API endpoint (electrs or public) |
+| `ESPLORA_WS_URL` | `wss://mutinynet.com/api/v1/ws` | Esplora WebSocket endpoint consumed by `scanner_ws` (issue #84). Override only when the upstream WS path changes |
| `IS_MAINNET` | `false` | `true` for Bitcoin Mainnet, `false` for Mutinynet/Signet |
| `NETWORK_NAME` | `Mutinynet` / `Mainnet` | Human-readable name returned by `/api/info`. Default depends on `IS_MAINNET` |
+| `USERNAME_DOMAIN` | _(required, no default)_ | External hostname returned by `/api/info`. The client renders `@` from this. **Server panics on startup if unset.** PRD sets `zkcoins.app`, DEV sets `dev.zkcoins.app` — silent fallback would let a misconfigured stage reproduce the cross-network routing bug (#95) |
| `PUBLISHER_KEY` | test key | 32-byte hex private key for inscription publishing. **Required on mainnet** — server panics on startup if default test key is detected with `IS_MAINNET=true` |
| `RUST_LOG` | `info` | Log level |
@@ -207,37 +231,39 @@ Runtime config above shapes _behaviour_ of compiled-in routes. _Which_ routes ar
Spawned from `main.rs::main`:
-1. **REST server** (`tokio::spawn` of `start_rest_server`) — Axum app bound to `0.0.0.0:4242`
-2. **Block scanner** (driven directly in main, not spawned) — `scan_for_inscriptions` runs an infinite loop polling Esplora every 30 s and writing state on each verified commitment
+1. **REST server** (`tokio::spawn` of `start_rest_node`) — Axum app bound to `0.0.0.0:4242`
+2. **Block scanner** (driven directly in main, not spawned) — `scan_for_inscriptions` consumes new tips from the WS-fed `mpsc` channel produced by `scanner_ws::run_scanner_ws` (spawned as a tokio task at startup) and writes state on each verified commitment. No fixed-interval polling — see [issue #84](https://github.com/zk-coins/node/issues/84)
### Tests
-| Stack | Command | What it covers |
-| ---------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
-| `cargo test` | `SP1_PROVER=mock cargo test -p server` | 45 tests covering only MVP code paths — what the PRD binary actually contains |
-| `cargo test` | `SP1_PROVER=mock cargo test -p server --all-features` | 58 tests including the gated `address-list`, `faucet`, `usernames`, and `lnurl` routes |
-| `cargo-llvm-cov` | `SP1_PROVER=mock cargo llvm-cov -p server --all-features` | Line coverage (latest run: **69.0% lines · 55.0% regions · 76.4% functions**) — measured with all gates on |
+| Stack | Command | What it covers |
+| ---------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
+| `cargo test` | `cargo test -p node` | MVP code paths — what the DEV + PRD binary actually contains |
+| `cargo test` | `cargo test -p node --all-features` | Including the gated `address-list` and `lnurl` routes |
+| `cargo-llvm-cov` | `cargo llvm-cov -p node` | Coverage gate enforced by CI: 100% lines + functions on the activated MVP surface |
-Per-module line coverage (latest run, all features):
+Per-module coverage (CI-gated):
-| Module | Tests | Line % |
-| ------------------- | ----- | ------ |
-| `server.rs` | 37 | 74.55% |
-| `account_server.rs` | 6 | 91.12% |
-| `state.rs` | 9 | 97.01% |
-| `username.rs` | 8 | 98.29% |
-| `scanner.rs` | 4 | 50.99% |
-| `publisher.rs` | 0 | 0.00% |
-| `main.rs` | 0 | 4.33% |
+| Module | Line + function % | Notes |
+| ------------------- | ----------------- | ---------------------------------------------------------------------------------- |
+| `account_node.rs` | 100% | send-coins flow, account ledger, scanner integration |
+| `scanner.rs` | 100% | Bitcoin block / inscription scanner |
+| `router.rs` | 100% | REST handlers + request validation |
+| `state.rs` | 100% | Poseidon-based SMT + MMR |
+| `username.rs` | 100% | Username claim / resolve / LNURL |
+| `publisher.rs` | excluded | Bitcoin commit/reveal broadcasting — needs live signet/regtest node |
+| `main.rs` | excluded | Runtime bootstrap |
+| `*_runtime.rs` | excluded | Background-loop wrappers; covered indirectly via integration tests against handlers |
+| `scanner_ws.rs` | excluded | WS subscriber + reconnect loop; pure helpers (`parse_ws_frame`, `frame_signals_tx_seen`) are unit-tested, the I/O loop is covered indirectly via the publisher's `track-tx` round-trip |
-`publisher.rs` and `main.rs` are untested by design — they require a live Bitcoin node and a funded publisher key. CI runs both the MVP build (`cargo build/clippy`) and the all-features build, plus `cargo test --all-features`. Coverage is collected ad-hoc, not in CI.
+`publisher.rs`, `main.rs`, the `*_runtime.rs` wrappers, and `scanner_ws.rs` are excluded by design — they require a live Bitcoin node, a funded publisher key, a bound TCP socket, or an upstream WebSocket peer, none of which fit in a unit test. The exclusion list is encoded in the CI gate's `--ignore-filename-regex`; everything else is held at 100% lines + 100% functions. CI runs the MVP build, the all-features build, `cargo nextest run -p node -p shared --release --all-features --test-threads 1 -E 'not binary(api_remote)'` on the self-hosted M3 Ultra runner pool, and the `Coverage Gate (100% lines + functions)` job.
## Running
Requires access to a Bitcoin node. See [Backend docs](https://docs.zkcoins.app/infrastructure/backend).
```bash
-SP1_PROVER=mock cargo run -p server
+cargo run -p node
# Server starts on http://0.0.0.0:4242
```
@@ -254,59 +280,64 @@ Mint uses a single-phase flow (server holds the minting account key).
## Project Structure
```
-server/ # Axum REST API
+node/ # Axum REST API
+├── src/
+│ ├── main.rs # Entry point, chain scanner, bind 0.0.0.0:4242
+│ ├── router.rs # REST endpoints + /health
+│ ├── runtime.rs # Bootstrap: lazy_statics, Postgres pool, REST listener
+│ ├── account_node.rs # Account logic, coin proofs, prover calls
+│ ├── state.rs # Sparse Merkle Tree + Merkle Mountain Range
+│ ├── scanner.rs # Bitcoin block scanner (event-driven via scanner_ws, prefix 4242)
+│ ├── scanner_ws.rs # Esplora WebSocket subscriber (issue #84, replaces 30 s polling)
+│ └── publisher.rs # Taproot Inscription broadcaster (commit/reveal)
+shared/ # Shared types (Commitment, Invoice, ClientAccount)
+program-plonky2/ # Cyclic-recursion state-transition circuit (Plonky2 + Poseidon)
├── src/
-│ ├── main.rs # Entry point, chain scanner, bind 0.0.0.0:4242
-│ ├── server.rs # REST endpoints + /health
-│ ├── account_server.rs # Account logic, coin proofs, prover calls
-│ ├── state.rs # Sparse Merkle Tree + Merkle Mountain Range
-│ ├── scanner.rs # Bitcoin block scanner (30s polling, prefix 4242)
-│ └── publisher.rs # Taproot Inscription broadcaster (commit/reveal)
-shared/ # Shared types (Commitment, Invoice, ClientAccount)
-program/ # SP1 zkVM circuit types (AccountState, Coin, ProofData)
-├── src/merkle/ # SMT + MMR implementations
-script/ # Prover (real SP1 zkVM — create_account, update_account)
+│ ├── circuit/ # `build_circuit` + per-stage gadgets
+│ ├── hash.rs # Poseidon-Goldilocks helpers (HashDigest, digest_to_bytes…)
+│ ├── merkle/ # Poseidon-based SMT + MMR
+│ ├── types.rs # AccountState, Coin, ProofData
+│ └── inputs.rs # CommitmentMerkleProofs, ProofType
+script-plonky2/ # Host-side prover wrapper (Prover struct)
```
+The last SP1 zkVM / SHA256 state is preserved at tag `v0.last-sp1` for historical reference. Recover with `git checkout v0.last-sp1 -- program/ script/`.
+
## Docker
```bash
-docker build -t zkcoin/server .
+docker build -t zkcoins/node .
docker run -p 4242:4242 \
--network bitcoin \
- -e SP1_PROVER=mock \
-e ESPLORA_URL=http://electrs-mainnet:3000 \
- zkcoin/server
+ zkcoins/node
```
-The pre-built ELF (`elf/zkcoins-program`) is committed to the repo, so Docker builds do not require the Succinct toolchain — only standard Rust.
+Docker builds use nightly Rust auto-installed via `rust-toolchain` (no external toolchain needed). The Dockerfile lives at the repo root; `.github/workflows/deploy-dev.yaml` builds `zkcoins/node:beta` for `linux/arm64` and deploys to the DEV host on every push to `develop`.
## CI/CD
| Workflow | Trigger | Action |
| ---------------------- | ------------ | ---------------------------------------------------- |
-| `deploy-dev.yaml` | Push develop | Docker (ARM64) → `zkcoin/server:beta` → DEV server |
-| `deploy-prd.yaml` | Push main | Docker (ARM64) → `zkcoin/server:latest` → PRD server |
+| `deploy-dev.yaml` | Push develop | Docker (ARM64) → `zkcoins/node:beta` → DEV server |
+| `deploy-prd.yaml` | Push main | Docker (ARM64) → `zkcoins/node:latest` → PRD server |
| `auto-release-pr.yaml` | Push develop | Creates Release PR (develop → main) |
Build time: ~5 minutes (Rust compilation on ARM64).
## Proving Strategy
-Staged scaling for the SP1 prover:
+zkCoins is **server-heavy**: a single trusted server generates all proofs, the wallet holds only the private key and signs BIP-340 Schnorr over `SHA256(serialize(asth) ‖ serialize(ocr))`. There is no in-browser Poseidon, no wasm-Plonky2 verifier, no in-app ZK gadget. See [`SPEC.md`](./SPEC.md) §13 + the memory `feedback_zkcoins_server_side_compute` for the full rationale.
-| Stage | When to move | Configuration |
-| ------------------------------ | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **0. Mock (DEV)** | Development & testing | `SP1_PROVER=mock` — no real proofs, instant responses. Required on DEV because CPU prover causes OOM (SP1 `update_account` exceeds available memory). |
-| **1. CPU (PRD)** | Production baseline | `SP1_PROVER=cpu` running on Mac Studio M3 Ultra, 96 GB unified memory. `create_account` works, `update_account` needs memory tuning. |
-| **2. Succinct Prover Network** | CPU latency becomes a bottleneck | `SP1_PROVER=network` — no hardware commitment, requires PROVE token deposit and accepts token-price exposure. See [docs.succinct.xyz](https://docs.succinct.xyz/docs/sp1/prover-network/quickstart). |
-| **3. Self-hosted CUDA** | Network volume too costly or PROVE exposure undesirable | `SP1_PROVER=cuda` on x86 Linux with NVIDIA GPU (Compute Capability ≥ 8.6, ≥ 24 GB VRAM — RTX 4090 / 5090 / RTX 6000 Ada). Apple Silicon is not supported. |
+**Hardware target: Mac Studio M3 Ultra** (96 GB unified RAM, single host). All on-box compute is available: Performance + Efficiency cores, the integrated Apple Silicon GPU (via Metal — currently unused because Plonky2 ships CPU + CUDA backends only), Neural Engine, AMX. **Not available**: external GPU accelerators (no NVIDIA, no CUDA), no cloud prover services (no Succinct Prover Network, no AWS GPU). Performance budget is what the M3 Ultra delivers; if a design overshoots, the design changes — we do not add external hardware.
-Skip stages only with concrete latency or cost data, not assumptions.
+Current cyclic-recursion proof times at production parameters (`MAX_IN_COINS = MAX_OUT_COINS = 8`, `INNER_PAD_BITS = 14`): 3–15 min wall per `prove_*` call. See [`program-plonky2/SESSION_STATE.md`](./program-plonky2/SESSION_STATE.md) for the detailed test-time table.
## Open Tasks
-- [ ] GPU acceleration (`SP1_PROVER=cuda`) or Succinct Prover Network
+- [ ] Step 9: signet end-to-end roundtrip against `dev.zkcoins.app` (create account → mint → send → receive)
+- [ ] Step 9: R2 performance measurement on the M3 Ultra (warm proof ≤ 5 s target ≤ 1 s; cold ≤ 30 s; peak mem < 64 GB)
+- [ ] Pre-mainnet hardening: D2/D10 (hiding recipient), D7 (reorg safety), D8 (per-coin nullifier-accum) — see `SPEC.md` §15
- [ ] Explorer endpoints (`/api/stats`, `/api/nullifiers`)
- [ ] Light client support
@@ -318,6 +349,19 @@ Skip stages only with concrete latency or cost data, not assumptions.
| [zk-coins/docs](https://github.com/zk-coins/docs) | Documentation ([docs.zkcoins.app](https://docs.zkcoins.app)) |
| [zk-coins/research](https://github.com/zk-coins/research) | Protocol research, upstream repos, paper PDF |
+## Design Documents
+
+| Document | Scope | Status |
+| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------ |
+| [`LIGHTNING_ATOMIC_SWAP.md`](./LIGHTNING_ATOMIC_SWAP.md) | Trustless LN ↔ zkCoins atomic swap design (HTLC on inscription funding tx) | Draft |
+| [`BITVM_BRIDGE.md`](./BITVM_BRIDGE.md) | BTC ↔ zkCoins trustless mint/burn bridge — landscape, BitVM2 / Glock / Mosaic comparison, N=100 federation target | Draft |
+| [`BRIDGE_MVP.md`](./BRIDGE_MVP.md) | Engineering spec for the bridge MVP — 8 phases, file-by-file, 5–7 months effort estimate | Draft |
+
+These documents describe the bridge and swap roadmap. They build on
+the Plonky2 migration that landed via PR [#17](https://github.com/zk-coins/node/pull/17)
+on 2026-05-18 and cross-reference `SPEC.md`, `MIGRATION_RESEARCH.md`,
+and `ROADMAP.md`.
+
## Protocol
Based on [Shielded CSV](https://eprint.iacr.org/2025/068) by Jonas Nick (Blockstream), Liam Eagen (Alpen Labs), Robin Linus (ZeroSync). Server code derived from [ZeroSync/ZKCoins](https://github.com/ZeroSync/ZKCoins).
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 00000000..657e1779
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,512 @@
+# Plonky2 Migration Roadmap
+
+Living tracker for the SP1 → Plonky2 + Poseidon migration. **Updated on
+every commit to `develop`** — if this file is stale relative to recent
+commits, that is a bug. The migration PR ([#17](https://github.com/zk-coins/node/pull/17))
+merged 2026-05-18; Steps 1–8 are done and Step 9 is partially done
+(DEV live, signet e2e roundtrip + R2 performance measurement remain).
+
+Source documents:
+
+- [`CONTRIBUTING.md`](./CONTRIBUTING.md) § "Working on the Plonky2 Migration" — **start here for fresh sessions.** Onboarding, project invariants, decision recipe, pre-push checklist, foot-gun summary, navigation aid for everything below.
+- [`SPEC.md`](./SPEC.md) — protocol specification (the *what*).
+- [`MIGRATION_RESEARCH.md`](./MIGRATION_RESEARCH.md) — analysis of the upstream references + design decisions + **§7 Lessons Learned during implementation** (the *why* + *what bit us*).
+- [`program-plonky2/CONTRIBUTING.md`](./program-plonky2/CONTRIBUTING.md) — operational handoff: toolchain, build/test/lint commands, runtime characteristics, pitfalls (the *how to actually hack on this*).
+- This file — execution plan, status, estimates (the *when and how-overview*).
+
+---
+
+## Status at a Glance
+
+Legend: ✅ done · 🟡 in progress · ⏳ todo. Effort estimates are
+person-days at full focus; multiply for part-time work.
+
+| # | Step | Status | Effort | Risk |
+| - | ---- | ------ | ------ | ---- |
+| 1 | Reconcile `SPEC.md` with paper divergences | ✅ done | — | — |
+| 2 | Scaffold `program-plonky2/` standalone crate | ✅ done | — | — |
+| 3a | Port off-circuit Poseidon hash + byte conversion | ✅ done | — | — |
+| 3b | Port off-circuit sparse Merkle tree to Poseidon | ✅ done | — | low (regression covered) |
+| 3c | Port off-circuit MMR to Poseidon | ✅ done | — | — |
+| 3d | Port off-circuit `AccountState`/`Coin`/`ProofData` | ✅ done | — | — |
+| 4a | In-circuit MMR inclusion gadget | ✅ done | — | — |
+| 4b | In-circuit SMT inclusion gadget | ✅ done | — | — |
+| 4c | In-circuit SMT non-inclusion gadget (verify only) | ✅ done | — | — |
+| 4c+ | In-circuit SMT insert gadget (new-root computation) | ✅ done | — | — |
+| 4d | Port `ProgramInputs` + `CommitmentMerkleProofs` types | ✅ done | — | — |
+| 5 | Monolithic state-transition circuit (recursion, padding, vk-pin) | ✅ done (5a/5b/5c/5c+/5d/5d-next-3/5d-next-5). Stage 5d-next-5 source-side cyclic verify landed via PR [#23](https://github.com/zk-coins/node/pull/23) — aggregator pattern + Phase 2b per-slot SMT inclusion + SPEC §8 (c)(d)(e) chain + 3 §13 negatives. See [`MIGRATION_RESEARCH.md` §7.22](./MIGRATION_RESEARCH.md#722-stage-5d-next-5-source-side-verification-via-aggregator-pattern--codified-resolves-721) for the empirical insights (`ConstantGate::new(2)` injection + `helper_degree = pad_bits + 1` sweep). | — | — |
+| 6 | `script-plonky2/` host-side prover wrapper | ✅ done (`d96bb62`) | — | — |
+| 7 | Server: **replace** SP1 path with Plonky2 (no feature flag, no dual backend) | ✅ done — `send_coins` performs **in-circuit source-side validation via Stage 5d-next-5 Phase 2 aggregator** (PR [#23](https://github.com/zk-coins/node/pull/23)); off-circuit pre-checks retained as defense-in-depth (microsecond-level fast-fail before the minute-scale prove). Initial server cut (`c71c9fc`) ran off-circuit-only because Phase 2 was deferred; the in-circuit wiring landed via the Step-7 follow-up. Dockerfile re-introduced (`dac0179`). 106 server tests pass on the MVP build, 119 with `--all-features` (32 baseline + 10 inline error-path in `d6a3cb9` + 64 ported SP1-era fixtures re-enabled in `account_node_tests.rs` / `router_tests.rs` + 13 feature-gated). Smoke-test verified end-to-end (`cargo run` + `/health` + `/api/info`, block scanner connects). | — | — |
+| 8 | App / wallet: Schnorr-signing boundary, server-API integration | ✅ done — `zk-coins/app` ships `wasm.createCommitment(xpriv, num_pubkeys, asth_hex, ocr_hex)` (in `app/rust/client/src/lib.rs`) signing `SHA256(asth ‖ ocr)` via BIP-340 Schnorr (D11). Two-phase send: `/api/send` (Phase 1, proof) → `/api/commit` (Phase 2, signature). API client in `app/src/lib/api/client.ts` covers `info` / `balance` / `send` / `commit` / `mint` / `username/*` endpoints exactly matching server routes registered at `node/src/server.rs:1261–1289`. WASM mock + Vitest coverage gate already enforced in app repo. | — | — |
+| 9 | DEV deployment + end-to-end roundtrip on signet | 🟡 DEV live — PR [#17](https://github.com/zk-coins/node/pull/17) merged 2026-05-18 21:50 UTC; auto-deploy via `.github/workflows/deploy-dev.yaml` landed `zkcoins/node:beta` on `dev-api.zkcoins.app`. `/health` → 200 `ok`; `/api/info` → 200 with `{network:"Mutinynet", capabilities:{address_list, faucet, usernames, lnurl: true}, username_domain:"dev.zkcoins.app"}` (post-[#73](https://github.com/zk-coins/node/pull/73) `address_list` and `lnurl` are `false` because DEV ships the MVP-only binary identical to PRD; `faucet` and `usernames` are hardcoded `true` — mint and usernames are permanent MVP, not feature-gated; the `usernames` Cargo feature was later removed outright — see PR [#76](https://github.com/zk-coins/node/pull/76)). Bootstrap-unblock fix in PR [#36](https://github.com/zk-coins/node/pull/36) (explicit `MINTING_ADDRESS` override + global panic hook + smoke test + deploy-dev post-curl-retry; see [`MIGRATION_RESEARCH.md` §7.23](./MIGRATION_RESEARCH.md#723-minting_address-panic-in-tokiospawn-ed-task-swallows-server-bootstrap--medium-codified)). Deploy concurrency guards + PRD smoke test in PR [#51](https://github.com/zk-coins/node/pull/51). DEV/PRD parity (drop DEV-only Cargo features + remove `DEV_SKIP_BROADCAST_FAILURE` env-gate) in PR [#73](https://github.com/zk-coins/node/pull/73). **Remaining:** ① e2e roundtrip (create account → mint → send → receive) on signet from `dev.zkcoins.app`; ② R2 measurement on M3 Ultra (warm ≤ 5 s, ideal ≤ 1 s; cold ≤ 30 s; peak mem < 64 GB); ③ reactive: redesign per R2 if the budget is missed. | 2–4 d | medium |
+| — | Pre-mainnet blockers: D2/D10 (recipient hiding), D7 (reorg safety), D8 (per-coin nullifier-accum) | ⏳ todo | **+2–3 weeks** | high (real protocol redesign) |
+
+**MVP status:** Steps 1–8 ✅ done. Step 9 partially done — DEV is live and serving traffic; signet e2e roundtrip and the R2 performance measurement on M3 Ultra remain. **Remaining engineering effort: 0 d** for the migration itself; **remaining ops effort: ~2–4 d** for the e2e probe campaign + R2 budget check. If the R2 budget holds on first measurement, the migration is complete and the project moves to the pre-mainnet hardening track.
+
+### Definition of "MVP"
+
+For this project, an "MVP" is **minimum viable** in two simultaneous senses, both non-negotiable:
+
+1. **Minimal feature surface.** Only what's needed for one complete user loop (create account → mint → send → receive → balance updates). No feature-bloat. If a capability is not on the critical path for that loop, it does not enter the MVP — see SPEC.md §15's deferred items.
+2. **100% test coverage on the activated surface.** Same standard as the SP1/SHA256 codebase (see README.md "Contributing"). Code that is gated OFF in the MVP build (Cargo features `address-list`, `lnurl` — disabled in both DEV and PRD images since PR [#73](https://github.com/zk-coins/node/pull/73)) is excluded; everything else MUST be tested. Mint and usernames are part of the MVP and are permanently compiled in (no `faucet` or `usernames` Cargo feature), so they count toward the activated surface. `cargo llvm-cov --fail-under-lines 100 -- --test-threads=1` is the gate (run from inside the affected crate; `--test-threads=1` keeps circuit-test memory peaks predictable on the M3 Ultra).
+
+These two requirements are not in tension — the first reduces the surface, the second keeps what remains clean. "MVP" is never an excuse to skip tests; it's an excuse to skip *features*. Negative tests (asserting that invalid witnesses are rejected) are mandatory for every gadget and every state-transition path.
+
+### Architecture summary
+
+The architecture is **server-side compute**: the server generates all ZK proofs; the wallet holds only the private key and signs BIP-340 Schnorr over `SHA256(serialize(asth) ‖ serialize(ocr))`. There is no in-browser Poseidon, no wasm-Plonky2 verifier, no in-app ZK gadget.
+
+**Hardware target: Mac Studio M3 Ultra, 96 GB unified RAM, single host.** All on-box compute is available: Performance and Efficiency cores, the integrated Apple Silicon GPU (via Metal), Neural Engine, AMX. What is **not** available: external hardware accelerators (no NVIDIA, CUDA, GPU farms) and external cloud proving services (no Succinct Prover Network, no AWS GPU, no Lambda Labs). Performance budget is what the M3 Ultra delivers; if a design overshoots, the design changes — we do not add external hardware. Note: Plonky2 currently has no Metal / Apple-Silicon-GPU backend, so the integrated GPU is effectively idle for proving. That is a library property (Plonky2 ships CPU + CUDA only), not a constraint we imposed; if a Metal backend becomes available it's fair game.
+
+zkCoins is in a **closed test environment** (DEV *and* PRD). No external users, no real money, no existing user-base to migrate. Step 7 therefore **replaces** the SP1 path outright rather than running a dual backend: SP1 modules are deleted, server starts with a clean Poseidon SMT/MMR state, no Cargo feature flag, no migration helpers. This is reflected in the lower effort estimates for step 7 (2–3 d instead of 3–5 d) and the dropped risk for R5.
+
+Pre-mainnet hardening adds another 2–3 weeks on top.
+
+---
+
+## Done
+
+Commit refs (newest first). Doc-only commits to ROADMAP / SPEC /
+MIGRATION_RESEARCH / CONTRIBUTING are not individually listed once
+they merely correct or extend this file — see `git log` for the
+exhaustive history.
+
+- [`d6a3cb9`](./../../commit/d6a3cb9) — test(account_node): 10 inline error-path tests (Account::new, get_minting_account_address Ok+Err, get_account_balance Ok+Err, load_from_file Err+missing-path, save+load roundtrip, send_coins Unknown account + Insufficient funds). Total test count 32 → 42. account_node.rs body still excluded from CI coverage gate (full SP1-era test-fixture port is a separate follow-up). state_tests.rs clippy auto-fixed in the same commit.
+- [`dac0179`](./../../commit/dac0179) — feat(docker): Dockerfile for the Plonky2 node (Step 9 prep). `rust:bookworm` base + rustup auto-installs nightly via `rust-toolchain`. Multi-stage build, FEATURES build-arg, debian-bookworm-slim runtime, EXPOSE 4242. Local release build verified clean (1m 26s on M3 Ultra). Smoke run end-to-end: `cargo run --release -p node` + `curl /health` → `ok`, `curl /api/info` → `{"network":"Mutinynet"}`, block scanner connects + processes Mutinynet tip.
+- [`c71c9fc`](./../../commit/c71c9fc) — feat(step-7): `send_coins` wired to the Plonky2 `Prover` wrapper. Off-circuit source-side validation (in-coin in source's output_coins_root + source commitment in history MMR) replaces Stage 5d-next-5 Phase 2 (deferred post-MVP, blocked on Plonky2 1.1.0 ConstantGate shape mismatch — see `MIGRATION_RESEARCH.md` §7.22 for the eventual resolution). MMR proof paths in `get_merkle_proofs` now extended to `MMR_PROOF_PATH_LEN`; history_root passed to prover is `state.mmr.root_extended(MMR_PROOF_PATH_LEN)`. Init vs AccountUpdate branch on `account.proof` + `DEV_SKIP_BROADCAST_FAILURE` env-var bypass preserved. The env-var bypass was later removed in PR [#73](https://github.com/zk-coins/node/pull/73) once DEV and PRD were unified on the MVP-only binary. Test re-enable (account_node_tests + router_tests modules disabled at include-point) is a separate follow-up.
+- [`19dcecf`](./../../commit/19dcecf) — fix(ci): relax coverage scope to skip account_node.rs + server.rs during Step-7 migration (their test modules are gated off pending Stage 5d-next-5 merge); new `test_get_mmr_inclusion_proof_known_root_returns_ok` to keep state.rs at 100% line / function coverage.
+- [`ee0ef4b`](./../../commit/ee0ef4b) — fix(ci+server): CI workflow rewritten for nightly toolchain + Plonky2 crate names; server clippy `-D warnings` cleanup (feature-gated structs `#[cfg(...)]`, deprecated `to_inner` → `to_keypair`, `unimplemented!` block replaced with explicit `Err` to avoid `diverging_sub_expression`); coverage timeout 30m → 60m.
+- [`00adbb4`](./../../commit/00adbb4) — feat(step-7): workspace toolchain unification (stable → nightly, root absorbs `program-plonky2/` + `script-plonky2/`) + server-side import migration. `program/` + `script/` SP1 crates deleted. shared/server use the Plonky2-era modules (`hash`, `types`, `inputs`); `[u8;32]` → `HashOut` boundary conversions via `digest_from_bytes` / `digest_to_bytes`; MMR leaf hash switched from SHA256 to Poseidon `hash_concat`. `account_node::send_coins` body wrapped in `unimplemented!` pending Prover-API integration after Stage 5d-next-5 merge. 31 server tests passing (scanner, state, username, etc.); `account_node_tests` + `router_tests` modules disabled at include point.
+- [`b76bd39`](./../../commit/b76bd39) — feat(program-plonky2): step 7 prep — serde derives + persistence helpers (SMT/MMR/types/inputs all get `Serialize`/`Deserialize`; `save_merkle_tree` / `load_merkle_tree` / `save_mmr` / `load_mmr` ported from SP1-era helpers; 4 new tests for round-trip + missing-path I/O errors; `[u8; 33]` pubkey worked around with inline `BigArray33` helper to dodge serde's N≤32 derive limit)
+- [`d96bb62`](./../../commit/d96bb62) — feat(script-plonky2): step 6 — host-side prover wrapper around `StateTransitionCircuit` (new crate `script-plonky2/` with `Prover` struct + `prove_initial` / `prove_account_update` / `verify` thin wrappers; mirrors the SP1-era `script/` crate shape; nightly toolchain via rust-toolchain.toml symlink to program-plonky2)
+- [`c1df545`](./../../commit/c1df545) — docs: defer Stage 5d-next-4 source-side cyclic verify to 5d-next-5 (post-MVP) — Plonky2 1.1.0's `dummy_circuit` can't reproduce `ConstantGate`-containing common_data shapes (Approach A) AND the in-circuit data-only fallback hit `goal_data != common` mismatch at build (Approach B); the trusted server folding only validly-proved commitments into history MMR makes Stage 5d-next-3 + prev_account CMP sufficient for server-heavy MVP. See MIGRATION_RESEARCH §7.21.
+- [`6ea965a`](./../../commit/6ea965a) — docs: finalise session pickup — §7.20 + test-confirmation + verification checklist
+- [`7db536d`](./../../commit/7db536d) — docs: session-state pickup notes for next agent
+- [`50a1bd9`](./../../commit/50a1bd9) — test: speed up account_update panic-tests via cyclic_base_proof (~25 min wall saved per full sweep)
+- [`8fab78a`](./../../commit/8fab78a) — test: combined in-and-out integration test on AccountUpdate (mirror of `d292855` on the cyclic-recursion + CommitmentMerkleProofs path)
+- [`05c17f8`](./../../commit/05c17f8) — docs(SPEC): note MAX_OUT_COINS in the constants table
+- [`a502b8f`](./../../commit/a502b8f) — test: cover assert_eq panics on the *_in_and_out_coins wrappers (3 new should_panic tests for `prove_*_with_in_and_out_coins`)
+- [`508ec9c`](./../../commit/508ec9c) — docs(ROADMAP): refresh commit list + test count after MAX_OUT_COINS=8 bump
+- [`d292855`](./../../commit/d292855) — test: combined in-and-out integration test (one Initial proof exercising both in-coins and out-coins loops in a single transition; validates running-balance mutations and interim/final account_state_hash distinction compose correctly)
+- [`56f3a05`](./../../commit/56f3a05) — feat: stage 5d-next-3-bump — MAX_OUT_COINS to 8 (mirrors MAX_IN_COINS at SPEC §13's production target; INNER_PAD_BITS bumped 13 → 14)
+- [`1943316`](./../../commit/1943316) — docs: stage 5d-next-4 design doc for source verification
+- [`6b5a885`](./../../commit/6b5a885) — feat: stage 5d-next-3 — out-coins processing
+- [`b2b82e7`](./../../commit/b2b82e7) — feat: stage 5d-next-2 — bump MAX_IN_COINS to 8
+- [`0195f71`](./../../commit/0195f71) — feat: stage 5d-next — apply_coin (recipient + balance + overflow). Per-slot witnesses extended with `coin_recipient`, `coin_amount_lo`, `coin_amount_hi`. Active slots assert `coin_recipient == account.owner` and `balance += coin_amount` with overflow check via `split_le(sum, 33)`. Running balance threaded through `MAX_IN_COINS` slots; final balance fed to a second Poseidon hash for the public `ProofData.account_state_hash`. New tests: positive (1 active in-coin, balance increases by 42, final hash matches off-circuit `apply_coin`); negatives (wrong recipient rejected, overflow rejected).
+- [`7db3c29`](./../../commit/7db3c29) — feat: stage 5d (minimal) + 5e (partial) — in-coin slot processing for coin_history + four SPEC §13 negative tests. 5d adds `MAX_IN_COINS = 1` const, `InCoinSlotTargets` per slot (`active`, `coin_identifier`, 256-sibling `nip_path`), per-slot SMT non-inclusion + insert into `coin_history_root` masked by `active`, new `prove_initial_with_in_coins` / `prove_account_update_with_in_coins` wrappers, and 5 tests (1 positive + 1 negative + 3 panic guards). 5e adds 4 negative tests against the existing 5c+ predicates.
+- [`2ce36ce`](./../../commit/2ce36ce) — test: cover assert_eq panic messages in set_cmp_witness (3 should_panic tests restoring 100% line coverage after 5c+)
+- [`4bc5f2f`](./../../commit/4bc5f2f) — feat: stage 5c+ — `CommitmentMerkleProofs` in-circuit (SPEC §8 (c)(d)(e); fixed-shape SMT inclusion at `TREE_DEPTH = 256` + 2× MMR inclusion at `MMR_PROOF_PATH_LEN = 31`; new `MMR_MAX_DEPTH = 32` const + `MMRProof::extend_to(depth)` + `MerkleMountainRange::root_extended(depth)` off-circuit helpers; new `select_hash` masking pattern so every constraint fires only when `condition = true`; `dummy_cmp()` placeholder used by `prove_initial` to populate the unused fields; tests: positive bootstrap chain (Init→Update with full CommitmentMerkleProofs verify) plus negatives for (b), (c), (d).)
+- [`4f317fe`](./../../commit/4f317fe) — refactor: SMT redesign to uncompressed fixed-256 paths (off-circuit `InclusionProof` / `NonInclusionProof` always carry exactly `TREE_DEPTH = 256` siblings; path compression removed from `insert` and proof generation; `NonInclusionProof.leaf` field dropped — non-inclusion now witnesses the empty-leaf default at the depth-256 slot; in-circuit `verify_smt_inclusion` / `verify_smt_non_inclusion` / `verify_smt_insert` reduced to a single `hash_up_full_path` engine; case A/B branch and `extension` parameter gone.)
+- [`bba6470`](./../../commit/bba6470) — feat: stage 5c — AccountUpdate branch (condition now a free witness; cyclic verify binds SPEC §8 (a); state continuity (b) via `condition * (account_state_hash - prev.account_state_hash) == 0`; coin_history carry-over via `select(condition, prev.coin_history_root, DEFAULT_HASHES[0])`; mint exception masked with `!condition`; 5 tests incl. Initial→AccountUpdate chain and state-discontinuity rejection; SPEC §8 (c)(d)(e) MMR/SMT history checks DEFERRED to stage 5c+)
+- [`d167237`](./../../commit/d167237) — feat: stage 5b — Initial-branch state-transition predicate (`circuit/main.rs` rewritten: counter payload replaced by 16-element `ProofData`, mint exception + empty-SMT roots + in-circuit Poseidon `AccountState::hash`, condition pinned `false`; 3 tests: mint accepted, non-mint zero-balance accepted, non-mint nonzero-balance rejected)
+- [`83fa0c1`](./../../commit/83fa0c1) — feat: stage 5a — cyclic recursion plumbing PoC (`circuit/main.rs`, 2 tests: base + 1 recursive cycle; superseded by stage 5b)
+- [`6cf949c`](./../../commit/6cf949c) — feat: SMT insert verify gadget (8 tests: 3 positive incl. deep-divergence Case B, 3 negative incl. case-A invariant, 2 build-time assertion panics)
+- [`79bd39e`](./../../commit/79bd39e) — docs: hardware target — M3 Ultra single host, no external hardware, no cloud prover (later corrected to note the integrated Apple GPU IS available, just unused by Plonky2 today)
+- [`e14d9df`](./../../commit/e14d9df) — feat: 100% test coverage on program-plonky2 (16 new tests + MMR refactor + coverage(off) annotations)
+- [`2b6f2cb`](./../../commit/2b6f2cb) — docs: consistency review pass — fix stale counts, add glossary, reconcile §6
+- [`401f813`](./../../commit/401f813) — docs(ROADMAP): closed test env — replace SP1, don't migrate
+- [`cd94f85`](./../../commit/cd94f85) — docs: CONTRIBUTING + §7 Lessons Learned (8 entries)
+- [`4cf98ac`](./../../commit/4cf98ac) — docs(ROADMAP): Plonky3 as post-MVP path; document rejected alternative
+- [`1967087`](./../../commit/1967087) — docs(ROADMAP): server-side compute, drop wasm Poseidon
+- [`2fed8f0`](./../../commit/2fed8f0) — feat: port `ProgramInputs` + `CommitmentMerkleProofs` (4 tests)
+- [`9ba03bc`](./../../commit/9ba03bc) — feat: SMT non-inclusion verify gadget (3 tests + 1 negative)
+- [`8002ce3`](./../../commit/8002ce3) — feat: SMT inclusion gadget + `circuit/util` (4 tests)
+- [`5c92a62`](./../../commit/5c92a62) — docs: initial ROADMAP
+- [`15d45c9`](./../../commit/15d45c9) — feat: MMR inclusion gadget (4 tests)
+- [`e1af850`](./../../commit/e1af850) — feat: AccountState/Coin/ProofData (8 tests)
+- [`c28e279`](./../../commit/c28e279) — feat: MMR to Poseidon (8 tests)
+- [`6215009`](./../../commit/6215009) — feat: SMT to Poseidon + zero-state collision fix (12 tests)
+- [`984580f`](./../../commit/984580f) — feat: Poseidon hash module (5 tests)
+- [`8fa6a92`](./../../commit/8fa6a92) — chore: toolchain pin + lock §5 decisions
+- [`72c3b78`](./../../commit/72c3b78) — feat: scaffold `program-plonky2/` standalone crate
+- [`049ec3e`](./../../commit/049ec3e) — docs: SPEC reconciled with paper, §15 divergences
+- [`57cdce4`](./../../commit/57cdce4) — docs: migration research
+- [`496c652`](./../../commit/496c652) — docs: circuit specification
+
+**Test count on this branch:** 103 (all green on nightly-2025-04-15).
+Breakdown: `prelude` 1 · `hash` 5 · `merkle::smt` 19 · `merkle::mmr` 14 ·
+`types` 10 · `inputs` 5 · `circuit::mmr` 5 · `circuit::smt` 12 ·
+`circuit::main` 32.
+
+**Coverage:** **100% lines, 100% functions, 100% regions** on `program-plonky2/`
+as measured by `cargo llvm-cov --fail-under-lines 100`. Test modules
+are annotated with `#[cfg_attr(coverage_nightly, coverage(off))]` so
+assertion-message-string regions inside tests don't pollute the
+production-surface measurement. Defensive `else ZERO_HASH` branches
+in the MMR were collapsed into `.get().copied().unwrap_or(...)` so the
+unreachable bounds-check shares one region with the success path
+rather than carrying its own perpetually-uncovered branch.
+
+---
+
+## In Progress
+
+**Step 5 — Monolithic state-transition circuit** (✅ done, broken into
+stages, each landed as its own reviewable commit; preserved below as
+the historical record):
+
+- **5a — recursion plumbing PoC** ✅ done in [`83fa0c1`](./../../commit/83fa0c1),
+ superseded by 5b. `circuit/main.rs` skeleton with
+ `conditionally_verify_cyclic_proof_or_dummy`,
+ `add_verifier_data_public_inputs`, three-pass
+ `common_data_for_recursion`, and a counter payload (`counter = if
+ condition { inner.counter + 1 } else { 0 }`). The R1 evidence that
+ cyclic recursion + `circuit_digest` pinning work in our Plonky2
+ 1.1.0 setup. Tests and payload replaced in 5b.
+- **5b — Initial branch with real predicate** ✅ done in
+ [`d167237`](./../../commit/d167237). Counter payload replaced by
+ 16-element `ProofData` public output. In-circuit Poseidon
+ `AccountState::hash` (with 32-bit balance limbs and 56-bit pubkey
+ limbs, both range-checked), `is_minting` predicate via element-wise
+ `is_equal` AND, mint exception enforced as `(1 - is_minting) *
+ balance_limb == 0`, `output_coins_root` and `coin_history_root`
+ constants from `DEFAULT_HASHES[0]`. `condition` constrained to
+ `false`. Three tests in `circuit::main`.
+- **5c — AccountUpdate branch** ✅ done in this revision. `condition`
+ is now a free witness. `conditionally_verify_cyclic_proof_or_dummy`
+ binds SPEC §8 (a) (same circuit via `circuit_digest`). State
+ continuity (b) enforced as `condition * (account_state_hash[i] -
+ prev.account_state_hash[i]) == 0` for each of the 4 hash elements.
+ `coin_history_root` carry-over via `select(condition,
+ prev.coin_history_root, DEFAULT_HASHES[0])`. Mint exception masked
+ with `(1 - condition) * (1 - is_minting)` so it only applies to
+ Initial. 5 tests in `circuit::main`: 3 Initial-side from 5b plus a
+ full Initial→AccountUpdate chain (recursive verify works
+ end-to-end) and an AccountUpdate state-discontinuity rejection.
+ **SPEC §8 (c)(d)(e) — `CommitmentMerkleProofs` predicate proving
+ prev was published in the global history MMR — is NOT YET WIRED.
+ Stage 5c+ closes that gap.**
+- **5c+ — CommitmentMerkleProofs in-circuit** ✅ done in commit
+ [`4bc5f2f`](./../../commit/4bc5f2f). SPEC §8 (c)(d)(e) all wired via
+ in-circuit SMT inclusion (`TREE_DEPTH = 256`) + 2× MMR inclusion
+ (`MMR_PROOF_PATH_LEN = 31`). Coverage-fix in
+ [`2ce36ce`](./../../commit/2ce36ce).
+- **5d — in-coin slots (minimal)** ✅ done in this revision.
+ `MAX_IN_COINS = 1` (production target is 8 per SPEC §13; bumping
+ the constant is mechanical). Per slot the circuit reserves an
+ `active` bit, a `coin_identifier`, and a 256-sibling
+ `nip_path`. Active slots prove SMT non-inclusion of
+ `coin_identifier` at the running `coin_history_root` and compute
+ the new root after inserting `coin_identifier` (used both as key
+ and as leaf value, making `coin_history` a set-membership SMT).
+ Inactive slots are masked no-ops. The `coin_history_root` running
+ value is chained through all slots and emitted as
+ `ProofData.coin_history_root`. **NOT YET WIRED (defer to 5d+):**
+ recursive verification of each in-coin's source proof, SMT
+ inclusion of `coin.identifier` in `source.output_coins_root`, the
+ source's own CommitmentMerkleProofs, and the apply_coin balance /
+ recipient update on `AccountState`. Without these, in-coins are
+ unsound (a prover can claim any `coin_identifier` was sent to
+ them); 5d+ closes the gap. New tests in `circuit::main`: positive
+ Init-with-1-active-in-coin into empty coin_history; tampered nip
+ path rejected; 3 panic guards (`nip_path` length, slot count for
+ `prove_initial_with_in_coins`, slot count for
+ `prove_account_update_with_in_coins`).
+- **5d-next — apply_coin semantics** ✅ done in this revision.
+ Per-slot witnesses extended: `coin_recipient: HashOutTarget`,
+ `coin_amount_lo: Target`, `coin_amount_hi: Target` (both
+ range-checked to 32 bits). Per slot, masked by `active`:
+ - Recipient check `active * (coin_recipient[i] - owner[i]) == 0`
+ for each of 4 hash elements.
+ - Balance add with overflow check via `split_le(sum, 33)`: bits
+ auto-witnessed by Plonky2's `BaseSumGate` generator; bit 32 is
+ the carry / overflow. `new_lo = sum_lo - 2^32 * carry`,
+ `sum_hi = balance_hi + active * coin_amount_hi + carry`,
+ `new_hi = sum_hi - 2^32 * overflow`, `assert overflow == 0`.
+ - Running balance threaded through slots; final balance feeds a
+ second `Poseidon(owner || final_balance_lo || final_balance_hi ||
+ pubkey_limbs)` for the FINAL `account_state_hash` in `ProofData`.
+ The earlier `account_state_hash` (from initial balance) keeps
+ serving SPEC §8 (b) state-continuity and (c) commitment-witness
+ checks. Tests: positive 1-active-in-coin with `coin.amount = 42`
+ increments balance and matches off-circuit `apply_coin` hash;
+ `recipient != owner` rejected; `amount` causing balance overflow
+ rejected.
+
+- **5d-next-2 — bump `MAX_IN_COINS` to 8** ✅ done in this revision.
+ `MAX_IN_COINS` const is now 8. `common_data_for_recursion_c`
+ padding bumped to `INNER_PAD_BITS = 13` (`1 << 13 = 8192` gates)
+ to accommodate the larger outer circuit. Test helper
+ `slots_first_active(&coin, &nip, &dummy_coin, &dummy_nip)` builds
+ a `MAX_IN_COINS`-length slot array with the first slot active.
+ All 4 `prove_*_with_in_coins` tests refactored to use it; build
+ and prove confirmed for `stage_5d_initial_with_one_active_in_coin`
+ (188s wall).
+
+- **5d-next-3 — out-coins processing** ✅ done in this revision.
+ `MAX_OUT_COINS = 1` slot reserved (mechanical bump to 8 later).
+ Per slot witnesses: `active`, `out_coin_identifier`,
+ `out_coin_amount_lo/hi`, `nip_path`. Per slot constraints (masked
+ by `active`):
+ - SMT non-inclusion + insert into `running_output_coins_root`
+ (mirroring the in-coins coin_history pattern, but for the new
+ `output_coins_root`).
+ - Balance subtraction with **underflow check** via
+ `split_le(diff, 64)` (vs. overflow check `split_le(sum, 33)` for
+ in-coins addition).
+ - `out_coin_identifier == Poseidon(interim_account_state_hash ||
+ u32(slot_index))` — mirrors off-circuit
+ [`crate::types::calculate_coin_identifier`].
+
+ Pubkey rotation: new `next_public_key_limbs` witness. The FINAL
+ `account_state_hash` (committed as `ProofData.account_state_hash`)
+ uses the NEW pubkey; the interim hash (used for identifier
+ derivation) uses the INITIAL pubkey, per SPEC §8 step 3 ordering.
+
+ API: new `prove_initial_with_in_and_out_coins` /
+ `prove_account_update_with_in_and_out_coins` for full caller
+ control. The existing `prove_initial` / `prove_account_update`
+ wrappers default `next_public_key = account_state.public_key`
+ (no rotation) and all-inactive out-coin slots.
+
+ Tests: positive `stage_5d_next_3_initial_with_one_active_out_coin`
+ (one out-coin emits, balance decreases by amount, pubkey rotates,
+ output_coins_root matches off-circuit insert); two negatives
+ (wrong identifier, underflow); two panic guards (nip-path length,
+ out-slot count).
+
+- **5d-next-5 — source-side verification via aggregator pattern** ✅
+ done via PR [#23](https://github.com/zk-coins/node/pull/23).
+ Architecture: non-cyclic [`SourceAggregatorCircuit`](program-plonky2/src/circuit/source_aggregator.rs)
+ bundles up to `MAX_IN_COINS` source proofs via per-slot
+ `conditionally_verify_proof`; the outer state-transition circuit
+ verifies the aggregator proof once via `verify_proof` and binds its
+ claimed state-transition `verifier_data` to its own via
+ `connect_hashes`. Per-slot SPEC §8 step 2 gates fire inside the
+ in-coin loop: SMT inclusion of `coin.identifier` in
+ `source.output_coins_root`, OCR coupling, SPEC §8 (c)(d)(e) chain
+ for source's commitment in `history_root`, strict
+ `connect(slot.active, aggregator.slot[i].active_pi)` so no in-coin
+ can be consumed without a verified source. Two Plonky2 1.1.0
+ shape-mismatch blockers were resolved empirically: explicit
+ `ConstantGate::new(2)` injection in the helper's pass-3, and
+ `INNER_PAD_BITS_STAGE_5D_NEXT_5 = 15` (`helper_degree = pad_bits +
+ 1`). Probes characterising both insights live in
+ [`src/circuit/recursion_shape_probe.rs`](program-plonky2/src/circuit/recursion_shape_probe.rs).
+ Full end-state in
+ [`MIGRATION_RESEARCH.md` §7.22](./MIGRATION_RESEARCH.md#722-stage-5d-next-5-source-side-verification-via-aggregator-pattern--codified-resolves-721).
+- **5e — negative tests from SPEC §13** ✅ done — all 11 negatives
+ covered (the previously-deferred 3 source-side negatives landed
+ with Stage 5d-next-5 Phase 3). Covered:
+ - Initial non-mint balance ≠ 0 → rejected (`stage_5c_plus_initial_non_mint_nonzero_balance_rejected`).
+ - Initial mint accepted (`stage_5c_plus_initial_mint_with_balance_accepted`, returns coin_history_root = DEFAULT_HASHES[0]).
+ - Account update mismatched state hash → rejected (`stage_5c_plus_account_update_state_discontinuity_rejected`).
+ - Prev's commitment_history_root not in current MMR → 4 tests:
+ `stage_5e_account_update_tampered_mmr_a_path_rejected`,
+ `stage_5e_account_update_tampered_mmr_b_path_rejected`,
+ `stage_5e_account_update_wrong_mmr_sibling_rejected`,
+ `stage_5e_account_update_wrong_history_root_rejected`.
+ - Double-spend (same in-coin twice in coin_history) → rejected
+ (`stage_5e_double_spend_same_coin_twice_rejected`).
+ - Out-coin identifier mismatch → rejected
+ (`stage_5d_next_3_initial_out_coin_wrong_identifier_rejected`).
+ - Sum of outputs > balance (underflow) → rejected
+ (`stage_5d_next_3_initial_out_coin_underflow_rejected`).
+ - Sum of input amounts overflow → rejected
+ (`stage_5d_initial_in_coin_overflow_rejected`).
+ - Wrong recipient on in-coin → rejected
+ (`stage_5d_initial_in_coin_wrong_recipient_rejected`).
+
+ Newly covered by Stage 5d-next-5 Phase 3 (PR #23):
+ - Input coin whose source-proof is not in commitment history →
+ `stage_5d_next_5_phase_3_source_not_in_history_rejected`.
+ - Input coin whose identifier is not in source's `output_coins_root`
+ → `stage_5d_next_5_phase_3_coin_not_in_source_ocr_rejected`.
+ - Wrong `vk` on recursive source proof →
+ `stage_5d_next_5_phase_3_wrong_st_vk_on_aggregator_rejected`.
+
+ Original (pre-stage-5b) wording: Overflow, underflow,
+ wrong vk, double-spend, wrong identifier, mismatched
+ account_state_hash, etc.
+
+Each stage carries the 100 % line coverage gate before commit.
+
+---
+
+## Next (in order)
+
+### Step 5 — Monolithic state-transition circuit — ✅ done (see *In Progress* above for the historical breakdown)
+**Effort:** 3–5 days (actual).
+**Files:** `program-plonky2/src/circuit/main.rs` (new) — the equivalent of `program/src/main.rs`.
+**Scope:** assemble all gadgets into the full circuit; implement Initial vs. AccountUpdate branch via `conditionally_verify_cyclic_proof_or_dummy`; fix `MAX_IN_COINS = 8`; pin `vk` via `add_verifier_data_public_inputs`; commit `ProofData` as 16-element public output.
+**Test plan (100% coverage gate applies):**
+ - Single send (1 in-coin → 1 out-coin) — initial proof path.
+ - Two sequential sends — update-proof recursion.
+ - All 11 negative cases from SPEC §13 (overflow, underflow, wrong vk, double-spend, wrong identifier, mismatched account_state_hash, etc.). Each is a separate `assert!(data.prove(pw).is_err())` test.
+ - `cargo llvm-cov` on the new circuit module must be 100% lines + branches.
+**Risk:** **High.** First real test of Plonky2 cyclic recursion with our public-input shape. The BitVM reference's toy IVC pattern is the only existing example; correctness depends on identical `circuit_digest` between build passes (two-pass `common_data_for_recursion` trick).
+
+### Step 6 — `script-plonky2/` prover host
+**Effort:** 1–2 days.
+**Files:** new crate `script-plonky2/`.
+**Mirror of:** `script/src/lib.rs::Prover`.
+**Test plan (100% coverage gate applies):**
+ - End-to-end through `create_account` and `update_account` paths.
+ - Error path: malformed inputs rejected.
+ - `cargo llvm-cov` on the prover wrapper must be 100%.
+**Risk:** Low. Plonky2 prover API is simpler than SP1's.
+
+### Step 7 — Server: replace SP1 with Plonky2 (no dual backend)
+**Effort:** 2–3 days.
+**Files:** `node/src/account_node.rs`, `node/src/state.rs`, `node/src/scanner.rs`, `node/src/server.rs`. Plus delete the SP1-specific imports and replace the old `program/` and `script/` references with `program-plonky2/` + `script-plonky2/`.
+**Strategy:** closed test environment means no migration. Stop the running DEV/PRD server, delete the existing SMT/MMR data files (`smt.bin`, `mmr.bin`, `accounts.bin`, `latest_block.bin`), start the new Plonky2-based server with a fresh state. No Cargo feature flag, no compatibility shim, no parallel-deploy.
+**Key challenge:** the Schnorr commitment message stays `SHA256(serialize(asth) ‖ serialize(ocr))` per §5.4 of `MIGRATION_RESEARCH.md`, so the scanner converts Poseidon outputs to bytes before SHA256 → BIP-340 verify.
+**Test plan (100% coverage gate applies):** the same `cargo llvm-cov -p node --fail-under-lines 100` gate that already enforces this on the SP1 build carries over. Every handler, every error path, every scanner state transition that lives in the PRD-feature-set must be covered. The current SP1 coverage baseline (see README.md table) is the floor to maintain.
+**Risk:** Low. Mechanical port, no compatibility surface area.
+
+### Step 8 — App / wallet — ✅ done
+**Status:** Pre-existing app-repo wiring already matches the new Plonky2 server contract — no code change required for the MVP.
+**Files in `zk-coins/app`:**
+ - `rust/client/src/lib.rs` — `create_commitment(xpriv, num_pubkeys, asth_hex, ocr_hex)` (BIP-340 Schnorr over `SHA256(asth ‖ ocr)`, returns `{public_key, signature, message}` JSON).
+ - `src/app/send/page.tsx` — Phase 1 (`/api/send`) + Phase 2 (`/api/commit`) two-step send flow with in-flight commit persistence + retry.
+ - `src/lib/api/client.ts` — typed client for every server route registered in `node/src/server.rs` (`info`, `balance`, `send`, `commit`, `mint`, `username/claim`, `username/resolve`, `address`).
+ - `src/__tests__/app/send-pipeline.test.tsx` — round-trip + retry + idempotency unit tests (mocked WASM).
+ - `src/__tests__/lib/api/contract.live.test.ts` — schema-conformance probes against a live server.
+**Why nothing changed in the wallet for the Plonky2 cutover:** the wallet operates strictly above the server-side ZK boundary. It signs `SHA256(asth ‖ ocr)` — both 32-byte hex blobs supplied by the server — with secp256k1. Whether the server computed `asth`/`ocr` via SP1+SHA256 or Plonky2+Poseidon is opaque to the wallet, and `digest_to_bytes` on the server side already serialises Poseidon `HashOut` into the same 32-byte shape (see `program-plonky2/src/hash.rs:48`).
+**Test gate:** existing Vitest coverage gate in `zk-coins/app` (per that repo's CONTRIBUTING.md). No new gate.
+**Remaining open question for Step 9 verification:** that `signature_verifies_after_app_send` lands as an e2e probe against the live DEV server. This is part of Step 9, not Step 8.
+
+### Step 9 — DEV deployment + e2e — 🟡 DEV live, e2e + R2 pending
+**Done:**
+ - PR [#17](https://github.com/zk-coins/node/pull/17) merged 2026-05-18 21:50 UTC. Auto-deploy via `.github/workflows/deploy-dev.yaml` pushed `zkcoins/node:beta` to Docker Hub and deployed to the DEV host. Bootstrap fix in PR [#36](https://github.com/zk-coins/node/pull/36) (explicit `MINTING_ADDRESS` override + global panic hook + smoke test + deploy-dev post-curl-retry — see [`MIGRATION_RESEARCH.md` §7.23](./MIGRATION_RESEARCH.md#723-minting_address-panic-in-tokiospawn-ed-task-swallows-server-bootstrap--medium-codified)).
+ - `https://dev-api.zkcoins.app/health` → 200 `ok`; `https://dev-api.zkcoins.app/api/info` → 200 with `{network:"Mutinynet", capabilities:{address_list, faucet, usernames, lnurl: true}, username_domain:"dev.zkcoins.app"}` (post-[#73](https://github.com/zk-coins/node/pull/73) `address_list` and `lnurl` are `false` because DEV ships the MVP-only binary identical to PRD; `faucet` and `usernames` are hardcoded `true` — mint and usernames are permanent MVP, not feature-gated; the `usernames` Cargo feature was later removed outright — see PR [#76](https://github.com/zk-coins/node/pull/76)).
+ - Deploy hardening: PR [#51](https://github.com/zk-coins/node/pull/51) added deploy-dev + deploy-prd concurrency guards and a PRD smoke test.
+ - DEV/PRD parity: PR [#73](https://github.com/zk-coins/node/pull/73) dropped the DEV-only Cargo features (`address-list`, `faucet`, `usernames`, `lnurl`) and removed the `DEV_SKIP_BROADCAST_FAILURE` env-gate so the two environments run the identical MVP-only binary. A follow-up refactor removed the `faucet` Cargo feature outright — mint is permanent MVP and ships unconditionally in every build — and a further refactor removed the `usernames` Cargo feature so usernames are permanent MVP and ship unconditionally too (PR [#76](https://github.com/zk-coins/node/pull/76)).
+**Remaining:**
+ 1. e2e roundtrip on signet from `dev.zkcoins.app`: create account → mint → send → recipient receives. Success criterion: one happy-path + one failure-path per route. Tracked via a follow-up GitHub issue.
+ 2. Real performance measurement on the M3 Ultra. R2 budget: warm proof ≤ 5 s, ideally ≤ 1 s; cold-start ≤ 30 s including circuit-data load; peak mem < 64 GB during proving. Plonky2 currently runs CPU-only on Apple Silicon (no Metal backend); that's the operative baseline.
+ 3. If budget is missed: redesign per R2 (reduce `MAX_IN_COINS`, drop in-coin recursion, or switch to folding). **NOT** add external hardware or move to a cloud prover — the closed-environment + single-host constraint is non-negotiable.
+**Test plan:** the authoritative coverage gate runs in CI on the self-hosted M3 Ultra runner pool (`.github/workflows/ci.yaml`, jobs `Node + Shared Tests` and `Coverage Gate`, gated behind the `ci:full` label per PR [#48](https://github.com/zk-coins/node/pull/48)); the pre-push hook only enforces fmt + clippy + `cargo check`. Step 9 verifies integration, not unit coverage. e2e success criterion: every endpoint round-trips under realistic conditions (one happy-path traversal per route plus at least one failure path per route).
+**Risk:** Medium. First real exposure of the cyclic-recursive prover to production hardware under realistic load. If the budget holds, MVP is done.
+
+---
+
+## Pre-Mainnet Hardening
+
+These are not MVP scope but block mainnet, per `SPEC.md` §15.
+
+| # | Item | Effort |
+| - | ---- | ------ |
+| D2/D10 | Hiding recipient commitments (`Commitment::commit(acct_id, rand)`) — fixes coin-linkability | 1 week |
+| D7 | Conditional-noop on reorg (gracefully degrade when claimed nullifier-accum no longer a prefix) | 4–5 days |
+| D8 | Per-coin nullifier-accum snapshot — recipients verify coin age locally | 2–3 days |
+| Tests | Paper-derived test suite from `MIGRATION_RESEARCH.md` §3 (A-SEC, ToSAcc prefix, half-aggregate Schnorr, etc.) | 1 week |
+
+**Total pre-mainnet add-on: ~2–3 weeks.**
+
+---
+
+## Long-term positioning
+
+Plonky2 is bridge technology. Post-MVP (after step 9): Plonky3 evaluation. Field/hash choice then via planned migration, not via ad-hoc drift.
+
+---
+
+## Risk Register
+
+### R1 — Plonky2 cyclic recursion correctness (high)
+**What can go wrong:** Step 5 fails because `circuit_digest` isn't stable between the two `common_data_for_recursion` passes, or the public-input layout in `add_verifier_data_public_inputs` is misaligned.
+**Mitigation:** Start step 5 with the simplest possible "I verify myself with a trivial payload" circuit before adding the real predicate. Validates the recursion plumbing in isolation.
+**Trigger to escalate:** if 1 day of debugging step 5 doesn't produce a verifying proof, escalate to the maintainers / the Plonky2 community.
+
+### R2 — 1-second proof target unreachable on M3 Ultra (medium)
+**What can go wrong:** Real circuit with 1+8 recursive verifies is too large for sub-second proving on the target hardware.
+**Hardware constraint:** Mac Studio M3 Ultra, 96 GB RAM, single host. The integrated Apple GPU is on the box and would be usable IF Plonky2 had a Metal backend — it doesn't, so de facto we're on CPU. External hardware (NVIDIA, CUDA, GPU farms) and external cloud provers (Succinct Network, AWS, etc.) are off the table. If proof time overshoots, the design changes; we do not add external hardware.
+**Mitigation knobs (all design-level):**
+ (a) reduce `MAX_IN_COINS`;
+ (b) drop recursion of in-coin proofs (replace with off-circuit nullifier-set check; this is a protocol change);
+ (c) switch to a folding scheme (Nova / HyperNova / similar) that's CPU-native;
+ (d) opportunistic: if a Plonky2 Metal backend becomes available, evaluate.
+**Explicitly OFF the table:** discrete NVIDIA / CUDA hardware (we have an Apple Silicon box, not an x86 + NVIDIA host), Succinct Prover Network (violates closed-test-env + no-external-services rule), Apple Neural Engine / AMX as custom-kernel targets (we won't author the kernels ourselves).
+**Trigger to escalate:** measured proof time > 5 s on M3 Ultra. Wallet-side performance is N/A — proving is server-side; the wallet's send-flow latency = proof time + network roundtrip.
+
+### R3 — (removed)
+Was: "Wasm Poseidon too slow." No longer applicable — the wallet performs no Poseidon hashing (server-side compute architecture). The wallet's only crypto is BIP-340 Schnorr signing of a SHA256 digest, which WebCrypto handles natively.
+
+### R4 — Pre-mainnet hardening pushes timeline (high)
+**What can go wrong:** D2/D10 hiding recipient is a real protocol change, not a patch. May require re-doing step 5 if it doesn't fit the existing circuit shape.
+**Mitigation:** Decide before mainnet whether to ship the MVP variant first (linkable recipients, documented) and harden later, or harden now. Currently planning the former (per §5.5 in MIGRATION_RESEARCH).
+**Trigger to escalate:** if regulatory or PR feedback flags linkability before MVP launch.
+
+### R5 — SP1 stays in the workspace forever (mitigated by closed-env strategy)
+**What was the worry:** dual-backend Cargo feature flag would let SP1 linger because there's no forcing event to remove it.
+**Mitigation in place:** zkCoins is in a closed test environment (DEV + PRD), so step 7 doesn't introduce a feature flag — it deletes the SP1 path outright as part of the rewire. There is no parallel-backend phase, therefore no "follow-up cleanup PR" needed. Risk reduced from medium to low.
+
+### R6 — Plonky2 itself becomes the new dead-end (medium, long horizon)
+**What can go wrong:** Plonky2 is in maintenance mode at 0xPolygonZero. Plonky3 is where active development goes (new gate sets, BabyBear field, Poseidon2 hash, GPU paths). If we ignore Plonky3 indefinitely we end up where SP1 left us — on a stack with no upstream momentum.
+**Mitigation:** Treat Plonky2 as **bridge technology**, not the final destination. See *Post-MVP path: Plonky3* below.
+**Trigger to escalate:** Plonky2 upstream goes 12 months without a release, OR Plonky3 reaches feature parity for our use-case (recursion + BIP-340-Schnorr boundary).
+
+---
+
+## Post-MVP Path: Plonky3
+
+Plonky2 is the **MVP bridge**, not the long-term substrate. After step 9
+succeeds we schedule a Plonky3 evaluation. Concretely:
+
+- **Field:** Plonky3 default is **BabyBear** (`p = 2^31 - 2^27 + 1`).
+ Smaller field, GPU-friendlier in general — but the GPU paths in
+ practice mean *CUDA*, which our M3 Ultra host can't run. Apple
+ Silicon GPU support would have to come via Metal in the prover
+ library; that's not the typical Plonky3-BabyBear GPU pitch. The
+ motivation for BabyBear here therefore reduces to "matches SP1's
+ choice / Plonky3-native"; Plonky2 we use Goldilocks because that's
+ Plonky2's mature default.
+- **Hash:** Plonky3 default is **Poseidon2** (~2× faster than the
+ original Poseidon used in Plonky2).
+- **Gadget reuse:** algorithmic structure (SMT, MMR, ProofData layout,
+ recursion contract) stays. The Plonky3 port is primarily plumbing —
+ re-typing field elements, swapping the hash function, adjusting limb
+ packing for BabyBear's smaller modulus.
+- **Estimated effort for Plonky3 cutover:** 2–4 weeks. Field and hash
+ change cost ~20% of that; the rest is Plonky3's different API
+ (recursion patterns, gate sets, witness generation).
+- **Trigger to start:** Plonky3 reaches feature parity for recursion +
+ our public-input layout. Currently (2026-05) it is close but the
+ recursion ergonomics are still under active iteration.
+
+### Considered alternative — adopt BabyBear + Poseidon2 inside Plonky2 *now*
+
+A reviewer suggested switching to BabyBear field and Poseidon2 hash
+already during this Plonky2 migration so that the Plonky3 cutover later
+becomes "pure glue code". Rejected for v1:
+
+1. **Plonky2 + BabyBear is fork-land.** `plonky2` 1.1.0 on crates.io is
+ Goldilocks-only. BabyBear support exists in community forks
+ (`plonky2-goldibear`-style) but those carry less upstream momentum
+ than the canonical Goldilocks build. We'd trade one upstream-mature
+ stack for one less-mature stack, with no MVP benefit.
+2. **Poseidon2 in Plonky2 needs custom implementation.** The crate's
+ `PoseidonHash` is Poseidon1. Poseidon2 means either hand-rolling the
+ permutation or pulling another community crate. Custom crypto code
+ in the MVP path is exactly what we want to avoid.
+3. **Migration cost now is non-trivial.** Switching to BabyBear means
+ re-doing `hash.rs`, `types.rs`, both Merkle modules (Goldilocks's
+ 2-limb u64 → BabyBear's 3-limb u64, 4-element digest → 8-element
+ digest, ProofData re-shape, etc.). Roughly 3–4 days of work that
+ produces no end-user-visible change.
+4. **Plonky3 cutover later is not "glue code" anyway.** Plonky3's API
+ (recursion ergonomics, gate sets, witness generation) is meaningfully
+ different from Plonky2's. The field/hash choice contributes maybe 20%
+ of that work; the rest happens either way. Switching field early
+ shrinks the eventual diff by maybe one day, at the cost of slower MVP
+ delivery.
+
+The decision is reversible: if the Plonky3 evaluation post-step-9 shows
+a clean enough path, we can do the field+hash switch *as part of* that
+migration with no extra structural cost.
+
+---
+
+## Update Protocol
+
+Whenever a commit lands on this branch:
+
+1. If the commit completes a step → flip its row in *Status at a Glance* to ✅ and move its entry under *Done*.
+2. If the commit partially completes a step → flip to 🟡 and note progress under *In Progress*.
+3. If new tasks emerge → add a row in *Next* or *Pre-Mainnet Hardening* with effort estimate.
+4. If the commit invalidates an estimate → revise the *Effort* column.
+5. If the commit hits or escalates a risk → update the relevant *Risk Register* entry.
+
+Stale roadmap = broken roadmap. If a commit changes scope and this file
+isn't updated, the next reviewer should reject the PR until it is.
diff --git a/SPEC.md b/SPEC.md
new file mode 100644
index 00000000..800b2f75
--- /dev/null
+++ b/SPEC.md
@@ -0,0 +1,488 @@
+# zkCoins Circuit Specification
+
+This document specifies the zkCoins state-transition circuit (currently implemented in Plonky2 + Poseidon in `program-plonky2/src/circuit/main.rs`) and the surrounding off-circuit responsibilities. It is **implementation-agnostic**: it does not mandate Plonky2, Poseidon, or any particular proof system. It is intended as a starting point for porting the circuit to other proof systems (e.g. Plonky3 with Poseidon2 / BabyBear) while preserving protocol semantics. Historical context: the original implementation used SP1 + SHA256 (recoverable at tag `v0.last-sp1`); PR [#17](https://github.com/zk-coins/node/pull/17) (merged 2026-05-18) migrated to Plonky2 + Poseidon-Goldilocks.
+
+> **Scope note.** This spec describes the **zkCoins MVP variant** of the Shielded CSV protocol, not the paper as published. It deliberately departs from [eprint 2025/068](https://eprint.iacr.org/2025/068) in 11 concrete ways — see §15 "Divergences from Shielded CSV (paper)" below, and [`MIGRATION_RESEARCH.md`](./MIGRATION_RESEARCH.md) for full analysis against the upstream reference implementation at [`ShieldedCSV/ShieldedCSV`](https://github.com/ShieldedCSV/ShieldedCSV).
+>
+> **New here?** Start with [`CONTRIBUTING.md`](./CONTRIBUTING.md) § "Working on the Plonky2 Migration" for the project invariants, decision recipe, and reading order. This spec is the *what*; CONTRIBUTING is the *how to navigate*.
+
+The reference implementation lives in:
+
+- `program-plonky2/src/types.rs` — `AccountState`, `Coin`, `ProofData` and pure helpers
+- `program-plonky2/src/circuit/main.rs` — circuit entry point (build + prove)
+- `program-plonky2/src/circuit/source_aggregator.rs` — non-cyclic per-slot source aggregator (Stage 5d-next-5)
+- `program-plonky2/src/merkle/sparse_merkle_tree.rs` — Poseidon SMT
+- `program-plonky2/src/merkle/merkle_mountain_range.rs` — Poseidon MMR
+- `script-plonky2/src/lib.rs` — host-side Plonky2 prover wrapper
+- `node/src/account_node.rs` — input preparation (host)
+- `node/src/state.rs` — global state (SMT + MMR)
+- `shared/src/commitment.rs` — Schnorr commitment used to bind a proof to an on-chain inscription
+
+---
+
+## 1. Goal
+
+A zkCoins coin transfer produces a recursive SNARK that proves:
+
+1. The sender's **account state** transition is consistent with the input coins (sum of inputs ≥ sum of outputs, no overflow).
+2. Each input coin was produced by a previous valid send proof (recursive verification).
+3. Each input coin has not been spent before in this account (non-inclusion in the account's coin history, then inserted).
+4. Each input coin's parent commitment is included in the **global commitment history** (so the chain ordering is authoritative).
+5. The output coins have deterministic, content-addressed identifiers derived from the next account state.
+6. A public `ProofData` summary is committed: the new account state hash, the new output-coins root, the global commitment-history root, and the new coin-history root.
+
+The proof is then "registered" on-chain by publishing a Schnorr commitment over `H(account_state_hash || output_coins_root)` as a Taproot inscription with txid prefix `4242`. The scanner picks up this commitment and inserts it into the global SMT, after which the global MMR root advances.
+
+---
+
+## Glossary
+
+Abbreviations and shorthand used throughout this spec and the surrounding documents (`MIGRATION_RESEARCH.md`, `ROADMAP.md`, `program-plonky2/CONTRIBUTING.md`, source comments).
+
+| Term | Expansion | Meaning |
+| ---- | --------- | ------- |
+| **asth** | account state hash | `H(AccountState)` — the digest committed by a send proof as its post-state. |
+| **ocr** | output coins root | The Merkle root of the SMT containing the send's output coin identifiers. |
+| **vk** | verifying key | The proof system's verifier key. In Plonky2 it's the `circuit_digest`; pinned via `add_verifier_data_public_inputs`. |
+| **pk** | public key | secp256k1 compressed pubkey, 33 bytes. For account commitments, rotates per send. |
+| **SMT** | Sparse Merkle Tree | Binary tree of depth 256 (one level per key bit), used for the per-account coin history, the per-send output coins tree, and the global commitment SMT. |
+| **MMR** | Merkle Mountain Range | (Misnomer in this codebase: actually a capacity-doubling padded Merkle tree.) Append-only structure holding the global commitment history. |
+| **PCD** | Proof-Carrying Data | Recursive-proof composition abstraction used by the Shielded CSV paper; in Plonky2 we instantiate this with cyclic SNARK recursion. |
+| **NIP** | NonInclusionProof | Witness that a key is *not* in an SMT. Two cases off-circuit: case A (empty subtree) and case B (path-compressed sibling leaf). |
+| **IP** | InclusionProof | Witness that a key *is* in an SMT, with its associated value. |
+| **D1–D11** | Divergences | Numbered list of differences between this implementation and Shielded CSV eprint 2025/068 (`MIGRATION_RESEARCH.md` §3, summarised in SPEC §15). |
+| **R1–R6** | Risks | Numbered entries in the ROADMAP risk register. |
+| **MAX_IN_COINS** | — | `= 8`. Fixed bound on input coins per send (Plonky2 circuit is fixed-shape; see decision §5.2 in MIGRATION_RESEARCH). |
+| **MAX_OUT_COINS** | — | `= 8`. Fixed bound on output coins per send; same fixed-shape rationale as `MAX_IN_COINS`. |
+| **TREE_DEPTH** | — | `= 256`. SMT depth (one level per key bit). |
+| **Step N** | — | Refers to the corresponding row in ROADMAP's *Status at a Glance* table. |
+| **BIP-340** | — | Bitcoin Schnorr signature scheme over secp256k1. The wallet uses BIP-340 to sign `SHA256(serialize(asth) ‖ serialize(ocr))`. |
+| **Goldilocks** | — | The 64-bit prime field used by Plonky2 (`p = 2^64 - 2^32 + 1`). |
+| **Poseidon** | — | Algebraic hash function we use for all Merkle node hashing and the field-element commitment of `AccountState`. |
+
+---
+
+## 2. Conventions and Types
+
+### 2.1 Hash function
+
+Let `H : bytes → F^n` denote the protocol-wide hash function. In the reference implementation `H` is SHA256 (`HashDigest = [u8; 32]`). In a Plonky2 port, `H` should be an algebraic hash (e.g. Poseidon over the Goldilocks field, output 4 field elements ≡ 256 bits of security with appropriate parameters). Once chosen, `H` MUST be used consistently in:
+
+- All Merkle tree node hashes (`hash_concat`)
+- The leaf-encoding rule (see §4.1)
+- `AccountState::hash` (account commitment digest)
+- `calculate_coin_identifier`
+- The "commitment message" hashed before Schnorr signing (`H(account_state_hash || output_coins_root)`)
+- The State's MMR-leaf rule (`H(smt_root || prev_mmr_root)`)
+- The SMT key-derivation for a Bitcoin pubkey: `key = H(serialize_compressed(pubkey))`
+
+There is **no domain separation between "leaf hashing" and "internal node hashing"** in the SMT today, except that the very bottom leaf is `hash_concat(value, key)` and a domain-separated `hash_leaf(0x00 || data)` is used only for the DEFAULT_HASHES seed. A clean Plonky2 port SHOULD introduce explicit domain separation tags as field-element prefixes to avoid second-preimage ambiguity. See §10 for migration guidance.
+
+### 2.2 Primitive types
+
+| Type | Meaning |
+| --------------- | ---------------------------------------------------------------------------------- |
+| `HashDigest` | Output of `H`. Fixed-size byte string (32 bytes for SHA256, 4 field elts for Poseidon). |
+| `Address` | `HashDigest` derived as `H(initial_public_key_bytes)`. |
+| `Amount` | `u64`. Coin amounts are non-negative integers; circuit MUST check `checked_add`/`checked_sub`. |
+| `PublicKey` | Compressed secp256k1 pubkey, 33 bytes. Schnorr signatures (BIP-340) use x-only. |
+| `VerifyingKey` | Identifier of the proof system's verifying key. SP1 uses `[u32; 8]`. Plonky2 would use the circuit's `VerifierOnlyCircuitData` digest. |
+
+### 2.3 Coin identifier rule
+
+```
+identifier := H(account_state_hash || u32_be(coin_index))
+```
+
+where `account_state_hash` is the **sender's next** account state hash (after balance is decremented but **before** the public key is rotated to `next_public_key`), and `coin_index` is the 0-based index of the coin in the `out_coins` vector. This makes coin identifiers deterministic and content-addressed, which is what allows the circuit to enforce uniqueness and non-malleability without needing a per-coin signature.
+
+---
+
+## 3. Account Model
+
+### 3.1 `AccountState`
+
+```
+AccountState {
+ owner: Address // = H(initial_public_key_bytes), never changes
+ balance: u64
+ public_key: PublicKey // current commitment pubkey (rotates each send)
+}
+```
+
+`AccountState::hash` MUST be a deterministic, canonical encoding hashed with `H`. The reference uses `bincode::serialize` followed by SHA256; a Plonky2 port SHOULD use a fixed field-element layout: `[owner_limbs..., balance_low, balance_high, pubkey_x_limbs..., pubkey_y_parity]` and a single Poseidon call.
+
+### 3.2 Coin
+
+```
+Coin {
+ identifier: HashDigest // = H(sender_next_account_state_hash || u32_be(index))
+ recipient: Address // recipient's account owner
+ amount: Amount
+}
+```
+
+### 3.3 Account transitions inside the circuit
+
+- **`apply_coin(coin)`** (used for input coins): assert `coin.recipient == self.owner`, `self.balance = self.balance.checked_add(coin.amount)`. Overflow MUST cause the proof to fail.
+- **`send_coins(out_coins, out_proofs, next_public_key)`** (used after applying all input coins):
+ - Build the `out_coins_root` by inserting each `out_coin.identifier` into an initially empty SMT, witnessed by a non-inclusion proof per coin. The circuit MUST assert `out_coins_root == current_root` before each insert (i.e. each proof witnesses the running root).
+ - Decrement `self.balance` by each coin's amount with `checked_sub`; underflow MUST cause the proof to fail.
+ - After all inserts: compute `account_hash := H(self)` and assert `coin.identifier == H(account_hash || u32_be(i))` for every output coin `i`.
+ - Finally rotate the account's `public_key` to `next_public_key`.
+ - Return `out_coins_root`.
+
+---
+
+## 4. Merkle Structures
+
+### 4.1 Sparse Merkle Tree (SMT)
+
+- **Depth:** `TREE_DEPTH = 256`. The Poseidon-Goldilocks port keeps this — a `HashDigest` is 4 Goldilocks elements × 64 bits = 256 bits when serialised, so 256 levels exactly cover the key's bit space. Implementations on smaller fields (e.g. BabyBear, 31 bits) would pack the key into more limbs but typically keep the depth at 256 (full-key-bit-tree); see `program-plonky2/src/merkle/sparse_merkle_tree.rs::TREE_DEPTH`.
+- **Key:** a `HashDigest`. Bit `i` is the MSB-first selector at level `i` (level 0 = root, level `TREE_DEPTH` = leaf).
+- **Leaf encoding:** `leaf_hash = H(value || key)`. The `value` is itself a `HashDigest`.
+- **Default leaf** at level `TREE_DEPTH`: `H(0x00 || ε)` (domain-separated empty leaf in the reference; Plonky2 SHOULD pick a fixed sentinel field-element constant).
+- **Default internal hashes:** `DEFAULT_HASHES[level] = H(DEFAULT_HASHES[level+1] || DEFAULT_HASHES[level+1])`.
+- **Inclusion proof** = `(key, siblings[0..TREE_DEPTH])`. Verifier reconstructs the root from `H(value, key)` upwards, using bit `i` of `key` (MSB-first) to decide ordering: bit=0 → `(current, sibling)`, bit=1 → `(sibling, current)`.
+- **Non-inclusion proof** = `(key, root, siblings, leaf=(other_key, other_value))`. Two cases:
+ 1. **Empty subtree case:** `other_key == key` AND `other_value == DEFAULT_HASHES[siblings.len()]`. Verifier hashes that default leaf upwards.
+ 2. **Occupied sibling case:** `other_key != key` (assert). Verifier hashes `H(other_value, other_key)` upwards along `other_key`'s path. By the SMT invariant this proves no leaf with `key` is present along the same prefix.
+- **Insert via non-inclusion proof:** the verifier-and-inserter recomputes the new root by extending the proof with default-hash padding down to the first differing bit between `key` and `other_key`, then hashes both leaves upward. This MUST yield the new root deterministically.
+
+### 4.2 Merkle Mountain Range (MMR)
+
+In the reference this is actually a **fixed-shape padded Merkle tree** with capacity doubling, not a classical MMR. The name is historical; the structure used is simpler.
+
+- Capacity is the next power of two ≥ leaf-count, starting at 2.
+- Missing leaves are padded with `ZERO_HASH` (= 32 zero bytes, or the zero field element).
+- Internal nodes: `node = H(left || right)`. Missing right siblings are `ZERO_HASH`.
+- The root advances when a leaf is appended; capacity doubles when the tree fills (no re-hashing, just resize).
+- **Proof** = `(index, path)` where `path[level]` is the sibling at each level from leaf to (level just below) root. Verifier: if `index` is even at this level, `H(current || sibling)`; else `H(sibling || current)`; `index /= 2`.
+
+---
+
+## 5. Global Commitment Format and History
+
+### 5.1 Off-chain "commitment" (`shared::commitment::Commitment`)
+
+A `Commitment` produced by the client is:
+
+```
+Commitment {
+ public_key: PublicKey // commitment pubkey (= account's current pk)
+ signature: Schnorr(BIP-340) // over msg_hash (see below)
+ message: bytes // the raw 32-byte H(asth || ocr) digest (no double-hashing)
+}
+```
+
+The signed message is `H(account_state_hash || output_coins_root)` where both inputs are `HashDigest`s. If a Plonky2 port keeps SHA256 _here_ for compatibility with secp256k1 Schnorr, that is fine — but the `account_state_hash` and `output_coins_root` operands themselves are produced by `H` and so MUST match the chosen circuit hash. Mismatching the two will break the scanner ↔ circuit link.
+
+### 5.2 Global state (`node::state::State`)
+
+- `smt: SparseMerkleTree` — keyed by `H(serialize_compressed(commitment_pubkey))`, value = `H(account_state_hash || output_coins_root)` (`Commitment::get_account_state_hash()` — misleading name, it's actually the message digest).
+- `mmr: MerkleMountainRange` — leaves are `H(smt_root || prev_mmr_root)`.
+- `prev_mmr_root: HashDigest` — the MMR root just before the most recent SMT update was folded in.
+- `root_indices: Map` — host-side lookup, not part of the protocol.
+
+#### `State::update(commitments)`
+
+For each `Commitment c`:
+
+1. `key := H(serialize_compressed(c.public_key))`
+2. `value := c.message` (= `H(asth || ocr)`)
+3. `smt.insert(key, value)` — fails if key already present with a different value (replay/inconsistency).
+
+After all inserts:
+
+4. `smt_root := smt.root()`
+5. `prev_mmr_root := mmr.root()` (capture, then update `self.prev_mmr_root`)
+6. `leaf := H(smt_root || prev_mmr_root)`
+7. `mmr.append(leaf)`
+8. Return `mmr.root()` (the new global commitment-history root).
+
+This is the contract that the scanner enforces, and the circuit's `verify_commitment` / `verify_previous_root` assume.
+
+---
+
+## 6. `CommitmentMerkleProofs`
+
+A bundle of Merkle witnesses linking one **proof** (account or coin) to the current global history root. Provided as a hint to the circuit; the circuit verifies them.
+
+```
+CommitmentMerkleProofs {
+ commitment_root: HashDigest // SMT root containing this commitment
+ commitment_proof: InclusionProof // proves commitment in that SMT
+ commitment_root_history_proof: MMRProof // proves SMT root is in the MMR (paired w/ prev_mmr_root)
+ commitment_root_mmr_sibling: HashDigest // = prev_mmr_root at the time this commitment was folded
+ previous_root_history_proof: (HashDigest, MMRProof) // proves the previous MMR root is also in the MMR
+ commitment_account_state_hash: HashDigest // claimed asth, opened
+ commitment_out_coins_root: HashDigest // claimed ocr, opened
+}
+```
+
+### Verifier rules
+
+- `commitment_proof.verify(H(commitment_account_state_hash || commitment_out_coins_root), commitment_root)` MUST hold.
+- `commitment_root_history_proof.verify(H(commitment_root || commitment_root_mmr_sibling), current_history_root)` MUST hold.
+- `previous_root_history_proof.1.verify(H(previous_root_history_proof.0 || prev_proof_history_root), current_history_root)` MUST hold, where `prev_proof_history_root` is the `commitment_history_root` committed by the prior proof we are verifying.
+
+This chain is what enforces **monotonicity of history**: a new proof must extend the same history its inputs came from.
+
+---
+
+## 7. Program Inputs (`ProgramInputs`)
+
+These are passed to the circuit on stdin (SP1) or as private witness (Plonky2). All fields are private witnesses except those re-derived from the public output (`ProofData`).
+
+```
+ProgramInputs {
+ proof_type: InitialProof | AccountUpdateProof
+ verification_key: VerifyingKey // self-hash for recursion (see §9)
+ account_state: AccountState // sender's state BEFORE this send
+ current_history_root: HashDigest // claimed global MMR root
+
+ // Only present for AccountUpdateProof
+ prev_proof_public_values: Option // prior account proof's public output
+ prev_proof_history_proofs: Option // witness that prior proof was committed on-chain
+
+ // Per input coin (in_coins[i])
+ in_coins: [Coin]
+ in_coin_proofs_public_values: [ProofData_bytes] // each coin's source proof public output
+ in_coin_proofs_history_proofs: [CommitmentMerkleProofs] // witnesses each source proof was committed
+ in_coin_proofs_non_inclusion_proofs: [NonInclusionProof] // witnesses each coin is unseen in own coin_history
+ in_coins_inclusion_proofs: [InclusionProof] // witnesses each coin is in source's out_coins_root
+
+ // Outputs
+ out_coins: [Coin]
+ out_coin_proofs: [NonInclusionProof] // running non-inclusion proofs into the new (initially empty) out_coins_tree
+ next_public_key: PublicKey // sender's rotated key
+}
+```
+
+For the recursive proofs (`prev_proof_public_values` and each `in_coin_proofs_public_values`), the host MUST also supply the actual recursive proof artifact (in SP1: `SP1Stdin::write_proof`). In Plonky2 these become `ProofWithPublicInputsTarget`s and are verified by `verify_proof::(...)` against a fixed `verifier_data` digest.
+
+---
+
+## 8. Circuit Logic
+
+The circuit reads `ProgramInputs`, performs all asserts and field updates, and commits a single `ProofData` as public output.
+
+```
+fn main(inputs: ProgramInputs):
+ vk := inputs.verification_key
+ account_state := inputs.account_state // mutable local
+ history_root := inputs.current_history_root
+
+ // 1. Coin-history root: either default (initial proof) or carried from prev account proof.
+ coin_history_root := match inputs.proof_type:
+ InitialProof:
+ // Mint exception: the special MINTING_ADDRESS may have any starting balance.
+ if account_state.owner != MINTING_ADDRESS:
+ assert account_state.balance == 0
+ DEFAULT_HASHES[0]
+
+ AccountUpdateProof:
+ // Recursively verify the previous account proof.
+ prev := verify_proof(inputs.prev_proof_public_values, vk)
+ assert vk == prev.vk // (a) same circuit
+ assert account_state.hash() == prev.account_state_hash // (b) state continuity
+ mp := inputs.prev_proof_history_proofs
+ assert account_state.hash() == mp.commitment_account_state_hash // (c) opening matches witness
+ assert mp.verify_commitment(history_root) // (d) commitment in history
+ assert mp.verify_previous_root(prev.commitment_history_root, history_root) // (e) extends prior history
+ prev.coin_history_root
+
+ // 2. Apply each input coin (in order).
+ for (i, coin) in inputs.in_coins.iter().enumerate():
+ cp := verify_proof(inputs.in_coin_proofs_public_values[i], vk) // recursive
+ assert vk == cp.vk
+ // Source's out_coins_root must contain this coin.
+ assert inputs.in_coins_inclusion_proofs[i].verify(coin.identifier, cp.output_coins_root)
+ // Source's commitment must be in the global history.
+ mp := inputs.in_coin_proofs_history_proofs[i]
+ assert cp.output_coins_root == mp.commitment_out_coins_root
+ assert mp.verify_commitment(history_root)
+ assert mp.verify_previous_root(cp.commitment_history_root, history_root)
+ // Coin must be unseen in own coin_history and inserted there.
+ nip := inputs.in_coin_proofs_non_inclusion_proofs[i]
+ assert coin_history_root == nip.root
+ coin_history_root := nip.verify_and_insert(coin.identifier)
+ account_state := account_state.apply_coin(coin) // assert recipient == owner, checked_add
+
+ // 3. Build new out_coins_root and rotate pubkey.
+ out_coins_root := account_state.send_coins(
+ inputs.out_coins, inputs.out_coin_proofs, inputs.next_public_key
+ )
+ // send_coins internally:
+ // - For each (out_coin, ncl_proof):
+ // assert out_coins_root_running == ncl_proof.root
+ // out_coins_root_running := ncl_proof.insert(out_coin.identifier)
+ // balance := balance.checked_sub(out_coin.amount) // assert no underflow
+ // - Compute account_hash := H(account_state)
+ // - For each (i, out_coin):
+ // assert out_coin.identifier == H(account_hash || u32_be(i))
+ // - account_state.public_key := next_public_key
+
+ // 4. Commit public output.
+ commit(ProofData {
+ vk: vk,
+ account_state_hash: account_state.hash(),
+ output_coins_root: out_coins_root,
+ commitment_history_root: history_root,
+ coin_history_root: coin_history_root,
+ })
+```
+
+### Note on the minting account
+
+`MINTING_ADDRESS` is a `HashDigest` constant. In the Plonky2/Poseidon build it is a domain-separated placeholder baked into `program-plonky2/src/types.rs::MINTING_ADDRESS` and **overridden at runtime** in `runtime.rs::start_rest_node`: after constructing the minting `ClientAccount` from `minting_secret.bin`, the code sets `minting_client.address = *MINTING_ADDRESS` so the prover circuit and the server state share the same value. This runtime override was added in PR [#36](https://github.com/zk-coins/node/pull/36) to fix a panic-in-tokio-spawn regression (see [`MIGRATION_RESEARCH.md` §7.23](./MIGRATION_RESEARCH.md#723-minting_address-panic-in-tokiospawn-ed-task-swallows-server-bootstrap--medium-codified)). The closed test environment means we are not bound to the historical SP1 minting key.
+
+---
+
+## 9. Public Output (`ProofData`)
+
+```
+ProofData {
+ vk: VerifyingKey
+ account_state_hash: HashDigest
+ output_coins_root: HashDigest
+ commitment_history_root: HashDigest
+ coin_history_root: HashDigest
+}
+```
+
+`vk` is the **circuit's own verifying-key digest**. It's used to enforce that a recursively verified proof was generated by the exact same circuit (preventing a different circuit from forging public values).
+
+In SP1 this is `vk.hash_u32()` (the verifying key reduced to `[u32; 8]`). In Plonky2 the standard pattern is to pass a public input that pins `verifier_data.circuit_digest`. The host MUST hard-code this digest in the on-chain protocol params and the scanner.
+
+---
+
+## 10. Recursion Contract
+
+The circuit verifies recursive proofs of itself. Two requirements:
+
+1. **Same circuit:** every recursively verified proof's `vk` field MUST equal the verifier's own `vk`.
+2. **Public-value binding:** when verifying a recursive proof, the verifier MUST bind the entire `ProofData` it just consumed (`account_state_hash`, `output_coins_root`, `commitment_history_root`, `coin_history_root`) into the rest of the circuit logic. In SP1 this is automatic via `sp1_zkvm::lib::verify::verify_sp1_proof(&vkey, &public_values_digest)`. In Plonky2 this requires connecting each public input of the recursive `ProofTarget` to the corresponding local target.
+
+For the **initial proof** there is no prior account proof to verify. The circuit takes the `InitialProof` branch, asserts `balance == 0` (except for `MINTING_ADDRESS`), and seeds `coin_history_root` with `DEFAULT_HASHES[0]`.
+
+---
+
+## 11. Off-Circuit Responsibilities
+
+### 11.1 Node (`node::account_node::send_coins`)
+
+1. Look up the sender's `Account` (its coin queue, prior account proof, and own coin_history SMT).
+2. For each queued `CoinProof`:
+ - Build a `CommitmentMerkleProofs` for the **coin's source proof** (witness it's on-chain).
+ - Build a `NonInclusionProof` against the account's own coin_history (proves replay safety) and insert into it.
+ - Carry over the per-coin `InclusionProof` (the proof that the coin was in its source's `out_coins_root`).
+3. Build the `out_coins` from invoices, with deterministic identifiers derived from the **next** account state hash.
+4. Build per-out-coin running `NonInclusionProof`s against an empty SMT.
+5. If a prior account proof exists, build a `CommitmentMerkleProofs` for it and choose `AccountUpdateProof`; else choose `InitialProof`.
+6. Call the prover. On success: persist the proof, clear `coin_queue`, set `balance := balance + queued_balance - invoiced_amount`, store the proof as the new `account.proof`.
+7. Return the `CoinProof`s (one per output coin), each containing the new proof + inclusion proof into the new `out_coins_root`. The recipient client later POSTs these to `/api/receive`.
+
+### 11.2 Client (`shared::ClientAccount::create_commitment`)
+
+Given a fresh server response `(proof_id, account_state_hash, output_coins_root)`:
+
+1. Sign `H(account_state_hash || output_coins_root)` with the **current** commitment private key (BIP-32 derivation index = `num_pubkeys - 1` in the reference).
+2. POST `(proof_id, commitment)` to `/api/commit`. The server attaches this commitment to the proof, builds a Taproot commit+reveal tx pair whose commit-tx txid begins with `4242`, and broadcasts.
+
+### 11.3 Scanner (`node::scanner`)
+
+1. Poll Esplora (or any Bitcoin tx source).
+2. Filter txs whose txid hex starts with `4242`.
+3. Extract Taproot inscription payload (`extract_inscription_content`).
+4. Deserialize as `Commitment`.
+5. Verify the Schnorr signature (`Commitment::verify`).
+6. Forward to `State::update([commitment])` and persist `latest_block`.
+
+The block height/order is implicitly authoritative: whoever lands first in the SMT wins. Replay is prevented by the SMT's reject-on-duplicate-key rule.
+
+---
+
+## 12. Migration Notes: Porting to Plonky2 + Poseidon
+
+This list captures the non-trivial decisions a port must make. None of them are optional.
+
+1. **Pick `H`.** Recommended: Poseidon over Goldilocks (`F = GF(2^64 - 2^32 + 1)`), width 12, full+partial rounds per the standard parameter set. `HashDigest` becomes 4 field elements (≡ 256-bit security with appropriate rate).
+
+2. **Re-derive `MINTING_ADDRESS`.** Plonky2 port has it as a domain-separated placeholder (`program-plonky2/src/types.rs::MINTING_ADDRESS`). At server runtime, `runtime.rs::start_rest_node` overrides it by setting `minting_client.address = *MINTING_ADDRESS` on the freshly-constructed `ClientAccount` so the prover circuit and runtime state agree on the value (see `MIGRATION_RESEARCH.md` §7.23). Closed test environment means no requirement to match the historical SP1 minting key.
+
+3. **`AccountState` hashing.** Drop `bincode + SHA256`. Define a canonical field-element layout (e.g. `[owner_limbs(4), balance_lo, balance_hi, pubkey_x_limbs(4), pubkey_parity]`) and hash with Poseidon. Both circuit and host MUST agree.
+
+4. **SMT depth.** Set `TREE_DEPTH` to the bit-length of `HashDigest` in the new field. For Poseidon-256 over Goldilocks treated as 4×64-bit limbs, you can either keep depth 256 (key = bits of all 4 limbs) or move to a smaller depth and accept a tiny non-injectivity probability (not recommended). Recommended: keep 256 with explicit big-endian limb ordering.
+
+5. **Add domain separation.** Replace the current leaf rule `H(value, key)` and internal-node rule `H(left, right)` with tagged variants: `H(LEAF_TAG, value, key)` and `H(NODE_TAG, left, right)`. This is essentially free in algebraic-hash circuits and removes a class of second-preimage edge cases the SHA256 version papers over.
+
+6. **Schnorr message hashing.** secp256k1 BIP-340 Schnorr signs SHA256(msg). You have two choices:
+ - **Keep secp256k1 + SHA256 for the signature only.** The signed *message* becomes `SHA256(account_state_hash || output_coins_root)` where `account_state_hash` and `output_coins_root` are 32-byte serializations of Poseidon outputs. This keeps wallet UX and Bitcoin-native signing unchanged.
+ - **Switch to an in-circuit-friendly signature** (e.g. EdDSA over a Plonky2-friendly curve). Cheaper to verify in-circuit, but breaks Bitcoin-native key reuse.
+ For an MVP, keep option (1).
+
+7. **Verifying-key binding.** Replace `vk: [u32; 8]` with the Plonky2 `circuit_digest` (a `HashOut`). Bind this as a public input on every recursive verification step.
+
+8. **Public-value serialization.** SP1's `bincode::serialize(&ProofData)` doesn't apply. Define `ProofData` as a flat array of field elements committed in order. The hash committed by `verify_proof` is the Poseidon hash of those public inputs.
+
+9. **MMR `ZERO_HASH`.** Replace with the zero field element (or the additive identity in the chosen group). Adjust `DEFAULT_HASHES` derivation accordingly.
+
+10. **`u32_be(coin_index)` in identifier.** Replace with one field element (range-checked to `< 2^32`) for in-circuit efficiency.
+
+11. **Number-of-input-coins bound.** SP1 lets `in_coins.len()` be dynamic at proving time. Plonky2 circuits are fixed-shape — pick a max (e.g. 8 input coins per send, padded with dummy "amount = 0" coins). The circuit MUST treat amount-zero coins as no-ops (skip non-inclusion insertion, skip apply, but still consume one slot of fixed-size arrays).
+
+12. **No `panic!`, no `expect!`.** In Plonky2 every "fail the proof" path becomes a constraint. Replace `Result<_, &'static str>` host code with explicit asserts inside the circuit. Note in particular: `checked_add`/`checked_sub`/`balance == 0`/`recipient == owner`/`coin.identifier == expected_identifier`.
+
+13. **Don't trust the `verify_previous_root` shortcut in the host.** `account_node.rs::get_merkle_proofs` has a `let _ = proofs.verify_previous_root(...)` comment claiming it's redundant. That redundancy holds because the in-circuit predicate re-checks it — for `prev_account` via Stage 5c+'s `CommitmentMerkleProofs` gates, and for in-coin sources via Stage 5d-next-5 Phase 2b's per-slot SPEC §8 (c)(d)(e) chain.
+
+---
+
+## 13. Invariants the Tests Should Encode
+
+A test-suite for the ported circuit MUST cover at minimum:
+
+- **Initial proof, non-mint, balance != 0** → proof rejected.
+- **Initial proof, mint** → proof accepted; coin_history_root is `DEFAULT_HASHES[0]`.
+- **Account update, mismatched `account_state.hash()` vs prev `account_state_hash`** → rejected.
+- **Account update, prev's `commitment_history_root` not in current MMR** → rejected.
+- **Input coin whose source-proof is not in commitment history** → rejected.
+- **Input coin whose identifier is not in source's `output_coins_root`** → rejected.
+- **Double-spend: same input coin twice in coin_history** → rejected.
+- **Output coin with `identifier != H(account_hash || index)`** → rejected.
+- **Sum of outputs > balance + sum of inputs** → rejected (underflow).
+- **Overflow on sum of input amounts** → rejected.
+- **Wrong `vk` on recursive proof** → rejected.
+
+---
+
+## 14. References
+
+- Shielded CSV paper — Jonas Nick, Liam Eagen, Robin Linus. https://eprint.iacr.org/2025/068
+- Shielded CSV reference implementation (normative) — https://github.com/ShieldedCSV/ShieldedCSV
+- `BitVM/zkCoins` Plonky2 prototype (IVC scaffold only) — https://github.com/BitVM/zkCoins
+- Plonky2 implementation — this repository, `program-plonky2/src/circuit/main.rs`
+- Historical SP1 implementation — preserved at tag `v0.last-sp1`
+- Migration research and divergence analysis — [`MIGRATION_RESEARCH.md`](./MIGRATION_RESEARCH.md)
+
+---
+
+## 15. Divergences from Shielded CSV (paper)
+
+This implementation differs from the published Shielded CSV protocol in 11 concrete ways. Each is either a deliberate MVP simplification, a deferred feature, or a privacy/soundness gap that must be closed before mainnet. The detailed analysis lives in [`MIGRATION_RESEARCH.md`](./MIGRATION_RESEARCH.md) §3. Summary table:
+
+| # | This SPEC | Paper | Class | Status |
+| --- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------- | -------------------- |
+| D1 | `identifier = H(asth ‖ u32_be(idx))` (32 B) | `CoinID = tx_hash ‖ idx` (34 B), `CoinIDOnChain = blockchain_loc ‖ idx` (8 B) | Architectural | Accepted for MVP |
+| D2 | `Coin.recipient = Address` (plaintext) | `coin.essence.address = Commitment::commit(acct_id, rand)` (hiding) | **Privacy** | **Must fix pre-mainnet** |
+| D3 | Single Schnorr commitment in Taproot inscription, txid prefix `4242` | Half-aggregate BIP-340 Schnorr `AggregateNullifier` via third-party publishers | Architectural | Accepted for MVP |
+| D4 | Global state = SMT(`H(pk)` → `H(asth ‖ ocr)`) + MMR over `H(smt_root ‖ prev_mmr_root)` | `ToSAcc` tuple-of-sets over `(pk, sig_comm, blockchain_loc, fee_acct_comm)` with prefix proofs | Architectural | Open |
+| D5 | SMT depth 256, hash-keyed (uniform) | `AccM` lex-ordered by `CoinIDOnChain` for subtree pruning | Scalability | Re-evaluate at scale |
+| D6 | No fee field, no fee output | `fee: u64` + `FEE_IDX = 0xffff` reserved coin index for publisher payout | Missing feature | Deferred |
+| D7 | No conditional-noop on reorg | `conditional_nav` degrades tx to no-op if claimed nullifier-accum no longer prefix | **Reorg safety** | **Must fix pre-mainnet** |
+| D8 | `Coin` carries no `nullifier_accum` snapshot | `Coin` carries snapshot; receiver verifies it's in their local history | **Soundness** | **Must fix pre-mainnet** |
+| D9 | No range/uniqueness checks on `coin_index` | `idx` strictly increasing within tx; `idx == FEE_IDX` reserved | Soundness | Cheap fix |
+| D10 | `apply_coin` checks `coin.recipient == self.owner` plaintext | Opens `Commitment::commit(acct_id, rand)` with witnessed `acct_comm_rand` | **Privacy** | **Tied to D2** |
+| D11 | `MINTING_ADDRESS` hard-coded | `payment_init_newacct` for fresh accounts; `issuance(IssuanceProof)` branch | Architectural | Deferred |
+
+**Bottom line:** D2/D10, D7, D8 are blockers for mainnet (privacy + soundness + reorg safety). D6 is a UX/economics blocker (no fee → no publisher incentive). The rest are documented departures from paper fidelity that the MVP accepts.
diff --git a/elf/zkcoins-program b/elf/zkcoins-program
deleted file mode 100755
index e6991fa4..00000000
Binary files a/elf/zkcoins-program and /dev/null differ
diff --git a/node/Cargo.toml b/node/Cargo.toml
new file mode 100644
index 00000000..3b581a05
--- /dev/null
+++ b/node/Cargo.toml
@@ -0,0 +1,99 @@
+[package]
+name = "node"
+version.workspace = true
+edition.workspace = true
+
+[dependencies]
+bitcoin = { workspace = true }
+bitcoin_hashes = { version = "0.16.0", features = ["std"] }
+sha2 = { workspace = true }
+serde = { workspace = true }
+bincode = { workspace = true }
+hex = "0.4.3"
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "net", "time", "sync"] }
+# Event-driven chain ingestion (issue #84): WebSocket subscription to
+# the Esplora-compatible block-event stream replaces the previous
+# 30-s tip polling loop. `rustls-tls-webpki-roots` keeps the TLS
+# stack self-contained on CI hosts (no system openssl), matching the
+# `reqwest` features used by `api_remote`.
+tokio-tungstenite = { version = "0.23", features = ["rustls-tls-webpki-roots"] }
+# `StreamExt` / `SinkExt` are used by `scanner_ws` to drive the
+# tungstenite stream inside `tokio::time::timeout(...)` on each
+# `.next()` call and to send the subscribe frame.
+futures-util = "0.3"
+# Promoted from `[dev-dependencies]` to `[dependencies]` so the
+# scanner can parse the `block` / `blocks` JSON frames returned by
+# the Esplora WS endpoint.
+serde_json = "1.0"
+# Optional ZMQ subscriber path (issue #84, dormant in this PR). When
+# `feature = "zmq"` is enabled the operator can plug a Bitcoin Core
+# ZMQ stream into the same channel the WebSocket scanner publishes
+# on — useful for self-hosters running a node directly. The MVP
+# binary keeps this off; no module references the crate yet, so a
+# missing system libzmq does not break the default build. Pinned
+# with `=` (vs. caret) because the feature is dormant — there is no
+# active integration to validate against a SemVer-compatible bump,
+# so any version drift should be a deliberate code change with
+# review, not a silent `cargo update` side-effect.
+bitcoincore-zmq = { version = "=1.5.4", optional = true }
+esplora-client = { git = "https://github.com/BitVM/rust-esplora-client", branch = "master" }
+axum = { version = "0.7.9", features = ["json", "multipart"] }
+anyhow = "1.0"
+zkcoins-prover = { path = "../script-plonky2/", package = "zkcoins-prover-plonky2" }
+zkcoins-program = { path = "../program-plonky2/", package = "zkcoins-program-plonky2" }
+shared = { path = "../shared/" }
+lazy_static = { workspace = true }
+tower-http = { version = "0.5", features = ["cors", "fs"] }
+# Postgres state-layer. PR-A1 wires the module + migrations + tests
+# only; bootstrap integration happens in PR-A2 + PR-A3 (see the
+# `#[allow(dead_code)]` on the module).
+sqlx = { version = "0.8", default-features = false, features = [
+ "runtime-tokio",
+ "tls-rustls",
+ "postgres",
+ "macros",
+ "migrate",
+] }
+
+[dev-dependencies]
+tower = { version = "0.5", features = ["util"] }
+http-body-util = "0.1"
+# Used by `publisher_tests` for Esplora mocking and by `router_tests`
+# to mock the Esplora HTTP endpoint behind the `/health/ready`
+# readiness probe so the tests never hit the real
+# `https://mutinynet.com/api` from CI.
+wiremock = "0.6"
+# Used by `db_tests` to spin up a real Postgres 17 per test run.
+# The legacy `clients::Cli` of v0.14/0.15 was replaced by a global
+# `runner()` — see `db_tests::setup_pool` for the shape we use.
+testcontainers = "0.27"
+testcontainers-modules = { version = "0.15", features = ["postgres"] }
+# HTTP client for the `api_remote` integration test, which exercises
+# the deployed DEV server end-to-end. rustls (not native-tls) to keep
+# the test runner self-contained on CI hosts without openssl headers.
+reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
+# Random key + suffix generation for the `api_remote` suite so each
+# run picks a fresh wallet and avoids collisions with concurrent
+# DEV-server consumers.
+rand = "0.8"
+# Auto-cleaning scratch directories for the ProofStore tests in
+# `router_tests`. Replaces the ad-hoc `std::env::temp_dir() + nanos
+# + remove_dir_all().ok()` shape — the `TempDir` Drop impl removes
+# the directory even when the test panics, so no test leaves a
+# leaked /tmp/zkcoins-* tree behind.
+tempfile = "3"
+
+[features]
+# All non-MVP features are off by default. When a feature is not enabled, the
+# corresponding routes, handlers, and supporting modules are excluded from the
+# binary at compile time via `#[cfg(feature = "…")]`, so the disabled code
+# cannot run, crash, or be exploited at runtime.
+default = []
+address-list = []
+lnurl = []
+# Dormant self-host operator opt-in (issue #84). When enabled, a ZMQ
+# subscriber publishes block-hash events into the same channel the
+# WebSocket scanner uses. No module activates the subscriber in this
+# PR; the flag exists so the crate dependency is feature-gated and
+# the MVP binary does not require `libzmq`.
+zmq = ["dep:bitcoincore-zmq"]
diff --git a/node/migrations/0001_initial.sql b/node/migrations/0001_initial.sql
new file mode 100644
index 00000000..51be7007
--- /dev/null
+++ b/node/migrations/0001_initial.sql
@@ -0,0 +1,50 @@
+-- Initial Postgres schema for the zkCoins server state-layer.
+--
+-- This migration is part of PR-A1 in the 3-PR Postgres migration
+-- series (file-based bincode -> Postgres). The schema is installed
+-- by `db::connect_and_migrate`; nothing here is wired into the
+-- server bootstrap yet — that happens in PR-A2 (state + latest block)
+-- and PR-A3 (accounts + usernames).
+--
+-- Design notes:
+-- * `smt_state`, `mmr_state`, `latest_block` are singletons keyed
+-- on a fixed `id = 1` row. The CHECK constraint prevents
+-- accidental multi-row inserts that would silently break
+-- `load_*` callers.
+-- * BYTEA is used for binary blobs (bincode-serialized SMT/MMR,
+-- 32-byte block hashes, 32-byte account addresses, raw account
+-- blobs). Postgres TEXT would force base64/hex round-trips for
+-- no benefit.
+-- * `updated_at` / `created_at` audit columns default to NOW().
+-- They are not part of any application invariant — purely for
+-- ops triage.
+
+CREATE TABLE smt_state (
+ id SMALLINT PRIMARY KEY CHECK (id = 1),
+ data BYTEA NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE mmr_state (
+ id SMALLINT PRIMARY KEY CHECK (id = 1),
+ data BYTEA NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE accounts (
+ address BYTEA PRIMARY KEY,
+ data BYTEA NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE usernames (
+ name TEXT PRIMARY KEY,
+ address BYTEA NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE latest_block (
+ id SMALLINT PRIMARY KEY CHECK (id = 1),
+ block_hash BYTEA NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
diff --git a/node/migrations/0002_minting_meta.sql b/node/migrations/0002_minting_meta.sql
new file mode 100644
index 00000000..7456166e
--- /dev/null
+++ b/node/migrations/0002_minting_meta.sql
@@ -0,0 +1,27 @@
+-- Faucet minting counter persistence (PR-A3).
+--
+-- The legacy `minting_num_pubkeys.bin` sibling file tracked the
+-- monotonically increasing BIP-32 child index the faucet uses to
+-- generate each mint's commitment public key. The counter MUST survive
+-- process restarts; otherwise the next mint sends the wrong
+-- `prev_commitment_pubkey` and `send_coins` rejects the transition.
+--
+-- A standalone singleton table is the simplest fit:
+-- * the row is tiny (one `BIGINT`) and updated at most once per mint
+-- (a feature-gated, low-frequency endpoint),
+-- * it is logically independent of the per-address `accounts` rows,
+-- * `ON CONFLICT (id) DO UPDATE` makes the upsert race-free at the
+-- SQL layer (matches the rest of the state-layer's idempotent
+-- write pattern).
+--
+-- `num_pubkeys` is stored as `BIGINT` (signed) even though the in-
+-- memory `ClientAccount.num_pubkeys` is `u32`: Postgres has no
+-- unsigned integer type, and `BIGINT` covers the full `u32` range
+-- without any cast contortion. The application layer rejects values
+-- outside `0..=u32::MAX` when loading.
+
+CREATE TABLE minting_meta (
+ id SMALLINT PRIMARY KEY CHECK (id = 1),
+ num_pubkeys BIGINT NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
diff --git a/node/migrations/0003_pending_inscriptions.sql b/node/migrations/0003_pending_inscriptions.sql
new file mode 100644
index 00000000..216e8c5c
--- /dev/null
+++ b/node/migrations/0003_pending_inscriptions.sql
@@ -0,0 +1,60 @@
+-- Pending inscriptions state-machine table (Phase B of the publisher
+-- crash-recovery hardening, building on PR #105's WS-timeout-race fix
+-- and PR #106's CLI recovery tool).
+--
+-- The publisher constructs a `(commit_tx, reveal_tx)` pair from the
+-- current commitment payload, broadcasts the commit, then broadcasts
+-- the reveal. Anything that fails between the two broadcasts —
+-- container crash, host OOM, lost in-memory `reveal_tx` bytes,
+-- transient Esplora outage — leaves the commit UTXO spent at the
+-- script-path anchor with no on-chain reveal to claim it. The funds
+-- are unrecoverable without re-deriving the exact same `reveal_tx`
+-- (PR #106's CLI exists for this case, manually).
+--
+-- This table closes the gap by persisting the full pair BEFORE the
+-- first broadcast attempt, and walking each row through the
+-- `constructed → commit_broadcast → reveal_broadcast → complete`
+-- state machine as each broadcast lands. A startup-time resumer
+-- (`publisher::resume_pending_inscriptions`) loads any row whose
+-- status is anything but `complete` and re-drives it: the commit (if
+-- not yet sent) or the reveal (if the commit landed but the reveal
+-- did not). Esplora's `txn-already-known` / `bad-txns-inputs-
+-- missingorspent` responses make every step idempotent.
+--
+-- Schema notes:
+-- * `commit_txid` is `UNIQUE` so a retry of the same (commit, reveal)
+-- pair after a transient broadcast failure cannot insert a second
+-- row. The publisher computes the txid deterministically from the
+-- constructed commit tx, so this is stable across restarts.
+-- * `commitment`, `commit_tx`, `reveal_tx` are bincode/consensus-
+-- serialized blobs. The resume path deserializes them via the same
+-- `bitcoin::consensus::deserialize` shape used by the live
+-- broadcast.
+-- * `commit_output_value` carries the script-path anchor output's
+-- value in sats; needed by `build_reveal_only` if a future
+-- rebuilder were to re-derive the reveal from the commitment
+-- payload. Today we persist the full `reveal_tx` so the rebuild
+-- path is not exercised, but the column is cheap to carry and
+-- matches the existing CLI's parameter shape.
+-- * The CHECK constraint enumerates every valid state so a typo in
+-- the application code surfaces as a Postgres constraint violation
+-- instead of a silent state-machine drift.
+-- * The partial index on `status <> 'complete'` keeps the resumer's
+-- boot-time scan O(pending) instead of O(total). After enough
+-- mints this list will be perpetually empty on a healthy server.
+
+CREATE TABLE pending_inscriptions (
+ id BIGSERIAL PRIMARY KEY,
+ commit_txid BYTEA NOT NULL UNIQUE,
+ status TEXT NOT NULL,
+ commitment BYTEA NOT NULL,
+ commit_tx BYTEA NOT NULL,
+ reveal_tx BYTEA NOT NULL,
+ commit_output_value BIGINT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ CHECK (status IN ('constructed','commit_broadcast','reveal_broadcast','complete','failed'))
+);
+
+CREATE INDEX pending_inscriptions_status_idx
+ ON pending_inscriptions (status) WHERE status <> 'complete';
diff --git a/node/migrations/0004_mmr_root_index.sql b/node/migrations/0004_mmr_root_index.sql
new file mode 100644
index 00000000..c75f7fd4
--- /dev/null
+++ b/node/migrations/0004_mmr_root_index.sql
@@ -0,0 +1,38 @@
+-- MMR root index persistence (Phase C of the post-PR-A* state-layer
+-- hardening, follow-on to PR #107's pending_inscriptions table).
+--
+-- `State::root_indices` is the in-memory `HashMap` consulted by `State::get_mmr_inclusion_proof`
+-- whenever an account's prior proof references a historical
+-- `commitment_history_root`. Before this migration the map was rebuilt
+-- empty on every bootstrap (`State::new` / `load_from_pg`), which meant
+-- any account whose latest proof pointed at a `commitment_history_root`
+-- produced before the container restart could never produce a new send
+-- or mint: the lookup returned `Err` and the handler surfaced 422
+-- `Unable to get mmr inclusion proof for the previous root`.
+--
+-- The table mirrors the in-memory shape one row per `(prev_mmr_root)`
+-- key. `INSERT … ON CONFLICT DO NOTHING` handles legitimate replays
+-- (the same `prev_mmr_root` cannot legitimately map to two distinct
+-- `(smt_root, leaf_index)` tuples — the MMR append is monotonic, so
+-- the first writer's value is also the correct value).
+--
+-- Schema notes:
+-- * `prev_mmr_root` is the `HashDigest` byte-encoding produced by
+-- `zkcoins_program::hash::digest_to_bytes` — 32 raw bytes,
+-- reinterpreting a Poseidon `HashOut`. The column is BYTEA
+-- PRIMARY KEY; Postgres TEXT would force hex round-trips for no
+-- benefit (same rationale as the address columns in 0001).
+-- * `leaf_index` is the MMR leaf position assigned at append time.
+-- In-memory it is a `usize` (matches `mmr.leaf_count()`); we
+-- persist it as BIGINT and check at read time that the value fits
+-- `u64`/`usize` (defensive cast — see `db::load_root_indices`).
+-- * `created_at` is informational only; no application invariant
+-- depends on it. Useful for ops triage after a recovery event.
+
+CREATE TABLE mmr_root_index (
+ prev_mmr_root BYTEA PRIMARY KEY,
+ smt_root BYTEA NOT NULL,
+ leaf_index BIGINT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
diff --git a/node/migrations/0005_drop_minting_meta_num_pubkeys.sql b/node/migrations/0005_drop_minting_meta_num_pubkeys.sql
new file mode 100644
index 00000000..5d3bd48c
--- /dev/null
+++ b/node/migrations/0005_drop_minting_meta_num_pubkeys.sql
@@ -0,0 +1,28 @@
+-- Drop `minting_meta` entirely (Phase D).
+--
+-- Pre-Phase-D the singleton `minting_meta` row carried a single
+-- `num_pubkeys BIGINT` counter — how many BIP-32 child indices the
+-- faucet had ever spent in a successful mint. The counter survived
+-- process restarts so the next `current_private_key()` derivation
+-- aligned with the last on-chain commitment.
+--
+-- Phase D removes that counter as a separately-stored value. The
+-- count is now derived from the Sparse Merkle Tree on demand:
+-- `derive_num_pubkeys_from_smt(minting_xpriv, smt)` walks `pk_0,
+-- pk_1, …` and stops at the first `sha256(pk_n.serialize())` whose
+-- leaf is absent from the SMT. The SMT is already the canonical
+-- truth (loaded from `smt_state` at boot, mutated by the scanner
+-- on every inscription) and is the source the previous startup
+-- invariant check measured the counter *against* — collapsing the
+-- two into one removes the desync class that issue zk-coins/node#89
+-- documented.
+--
+-- `minting_meta` had no other columns (id + num_pubkeys +
+-- updated_at), so dropping the whole table is the cleanest shape.
+-- The matching `load_minting_num_pubkeys` / `upsert_minting_num_pubkeys`
+-- helpers and the `commit_mint_tx` counter step are removed in
+-- the same commit. After this migration runs, no code reads or
+-- writes the table; the migration is destructive but the value was
+-- the bug we are fixing — the SMT carries the truth.
+
+DROP TABLE IF EXISTS minting_meta;
diff --git a/server/minting_secret.bin b/node/minting_secret.bin
similarity index 100%
rename from server/minting_secret.bin
rename to node/minting_secret.bin
diff --git a/node/src/account_node.rs b/node/src/account_node.rs
new file mode 100644
index 00000000..888f5e2d
--- /dev/null
+++ b/node/src/account_node.rs
@@ -0,0 +1,1133 @@
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex, MutexGuard};
+
+use crate::db;
+use crate::state::State;
+use bitcoin::secp256k1::PublicKey;
+use serde::{Deserialize, Serialize};
+use shared::commitment::Commitment;
+use shared::{Address, Invoice};
+use sqlx::PgPool;
+use zkcoins_program::hash::{digest_from_bytes, digest_to_bytes, HashDigest, ZERO_HASH};
+use zkcoins_program::inputs::CommitmentMerkleProofs;
+use zkcoins_program::merkle::merkle_mountain_range::MMR_MAX_DEPTH;
+use zkcoins_program::merkle::sparse_merkle_tree::{
+ InclusionProof, NonInclusionProof, SparseMerkleTree, DEFAULT_HASHES, TREE_DEPTH,
+};
+use zkcoins_program::types::{
+ calculate_coin_identifier, AccountState, Amount, Coin, CoinTemplate, ProofData,
+};
+use zkcoins_prover::{InCoinSourceWitness, Proof, Prover};
+
+/// Fixed in-circuit MMR proof depth. Must match
+/// [`zkcoins_program::circuit::main::MMR_PROOF_PATH_LEN`].
+const MMR_PROOF_PATH_LEN: usize = MMR_MAX_DEPTH - 1;
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct CoinProof {
+ pub proof: Proof,
+ pub coin: Coin,
+ pub inclusion_proof: InclusionProof,
+ pub commitment: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Account {
+ pub proof: Option,
+ pub coin_queue: Vec,
+ pub coin_history: SparseMerkleTree,
+ pub balance: u64,
+}
+
+impl Account {
+ /// Deep-clone an `Account` via bincode round-trip.
+ ///
+ /// `SparseMerkleTree` is not `Clone` (the upstream type in
+ /// `program-plonky2` deliberately keeps the API minimal), so we go
+ /// through the serialisation boundary the rest of this module
+ /// already exercises for persistence. The serialiser is the same
+ /// one [`AccountNode::serialize_account`] uses, so any future
+ /// change to the on-disk shape continues to be a single point of
+ /// truth.
+ ///
+ /// Returns the deserialised twin or a `bincode::Error` from the
+ /// round-trip. Both fallible arms are propagated up to the caller
+ /// (`AccountNode::prepare_mint`) which surfaces them as the
+ /// caller-facing "Failed to snapshot minting account" error.
+ pub(crate) fn try_deep_clone(&self) -> Result {
+ let bytes = bincode::serialize(self)?;
+ bincode::deserialize(&bytes)
+ }
+}
+
+/// Result of [`AccountNode::prepare_mint`]: the tentative mutated
+/// minting account (clone — not yet swapped into `self.accounts`)
+/// together with the freshly-generated coin proofs the mint flow needs
+/// to inscribe and deliver. The caller commits the mutation atomically
+/// via [`AccountNode::commit_mint`] once the on-chain broadcast and
+/// the optimistic `minting_meta.num_pubkeys` UPDATE have both
+/// succeeded.
+#[derive(Debug)]
+pub struct MintingPrepared {
+ pub mutated_minting: Account,
+ pub coin_proofs: Vec,
+}
+
+impl Account {
+ pub fn new() -> Self {
+ Account {
+ proof: None,
+ coin_queue: vec![],
+ coin_history: SparseMerkleTree::new(),
+ balance: 0,
+ }
+ }
+ /// Uses the coin_template and next_public_key to create the next account_state and generates a
+ /// Coin with filled in identifier (as it commits to the next account state hash).
+ ///
+ /// Total: caller (`send_coins`) is responsible for upstream balance + slot-count validation;
+ /// once that is done this function cannot fail. Returns `Vec` directly so the call site
+ /// has no dead `?` propagation path.
+ pub fn create_coins(
+ &self,
+ address: HashDigest,
+ next_public_key: PublicKey,
+ public_key: zkcoins_program::types::PublicKey,
+ coin_templates: Vec,
+ ) -> Vec {
+ let mut next_account_state = AccountState {
+ owner: address,
+ balance: self.get_balance(),
+ public_key,
+ };
+ for coin_template in &coin_templates {
+ // Caller (send_coins) already validated balance >= total
+ // invoiced amount before reaching this function. The expect
+ // here is documentation of that invariant.
+ next_account_state.balance = next_account_state
+ .balance
+ .checked_sub(coin_template.amount)
+ .expect("balance was validated by send_coins");
+ }
+
+ let next_account_state_hash = next_account_state.hash();
+ let coins = coin_templates.into_iter().enumerate().map(|(i, template)| {
+ Coin::new(
+ template,
+ calculate_coin_identifier(next_account_state_hash, i as u32),
+ )
+ });
+ // Set the next public key.
+ let _ = next_public_key.serialize();
+ // next_account_state.public_key is intentionally not updated
+ // here because the caller (send_coins) sources `next_public_key`
+ // separately for the Prover witness — once Stage 5d-next-5
+ // Prover-API integration lands, this update + return will be
+ // wired through.
+ let _ = next_account_state;
+ coins.collect()
+ }
+
+ pub fn get_balance(&self) -> Amount {
+ self.coin_queue
+ .iter()
+ .fold(self.balance, |acc, x| acc + x.coin.amount)
+ }
+}
+
+pub struct AccountNode {
+ accounts: HashMap,
+ prover: Prover,
+ state: Arc>,
+}
+
+impl AccountNode {
+ /// Get the keypair to the pubkey this account commited to (which is derived key num_pubkeys -
+ /// 1)
+ // TODO: Move to client.
+ ///
+ /// Test-only after PR-A3 — the production bootstrap rehydrates the
+ /// server from Postgres via `load_from_pg`, never `new`. Kept
+ /// because every test in `account_node_tests.rs`,
+ /// `router_tests.rs`, and `runtime_tests.rs` uses it to
+ /// build a known-empty server before importing fixture accounts.
+ #[cfg_attr(not(test), allow(dead_code))]
+ pub fn new(state: Arc>) -> Self {
+ let accounts = HashMap::new();
+ let prover = Prover::new();
+
+ AccountNode {
+ accounts,
+ prover,
+ state,
+ }
+ }
+
+ pub fn import_account(&mut self, address: HashDigest, account: Account) {
+ self.accounts.insert(address, account);
+ }
+
+ // TODO: User needs to provide a signature and the salt and the secret information for the
+ // address to authenticate.
+ pub fn get_account_balance(&self, account_address: &Address) -> Result {
+ match self.accounts.get(account_address) {
+ Some(account) => Ok(account
+ .coin_queue
+ .iter()
+ .fold(account.balance, |acc, x| acc + x.coin.amount)),
+ _ => Err("No account with this address"),
+ }
+ }
+
+ pub fn get_addresses(&self) -> Vec {
+ self.accounts.keys().cloned().collect::>()
+ }
+
+ pub fn receive_coin(&mut self, coin_proof: CoinProof) -> Result<(), &'static str> {
+ let recipient = coin_proof.coin.recipient;
+ let mut account = self
+ .accounts
+ .remove(&recipient)
+ .unwrap_or_else(Account::new);
+ Self::receive_coin_into(&mut account, coin_proof)?;
+ self.accounts.insert(recipient, account);
+ Ok(())
+ }
+
+ /// Pure-by-account variant of [`Self::receive_coin`]. Validates
+ /// the supplied proof + inclusion proof against the recipient
+ /// account and, on success, pushes the coin into the recipient's
+ /// `coin_queue`. The caller owns the `&mut Account` lifecycle —
+ /// used by the mint flow's prepare-then-commit path to apply
+ /// receives on cloned recipients before the on-chain broadcast
+ /// commit window.
+ pub fn receive_coin_into(
+ account: &mut Account,
+ coin_proof: CoinProof,
+ ) -> Result<(), &'static str> {
+ // PLONKY2 MIGRATION (Step 7): The SP1-era `proof.public_values`
+ // (a writable byte stream) is replaced by Plonky2's
+ // `proof.public_inputs: Vec` (field elements). The
+ // `ProofData::from_field_elements` helper is the canonical
+ // bridge.
+ let pis: [zkcoins_program::F; zkcoins_program::circuit::main::N_PROOF_DATA_PUBLIC_INPUTS] =
+ coin_proof.proof.public_inputs
+ [..zkcoins_program::circuit::main::N_PROOF_DATA_PUBLIC_INPUTS]
+ .try_into()
+ .map_err(|_| "Proof public_inputs too short")?;
+ let proof_data = ProofData::from_field_elements(&pis);
+
+ // Verify the inclusion of the coin in the proof.
+ if !coin_proof
+ .inclusion_proof
+ .verify(coin_proof.coin.identifier, proof_data.output_coins_root)
+ {
+ return Err("Coin inclusion proof verification failed");
+ }
+
+ // Log coin receipt without exposing full address (privacy).
+ let addr_bytes = zkcoins_program::hash::digest_to_bytes(&coin_proof.coin.recipient);
+ eprintln!(
+ "Receiving coin for address: {:02x}{:02x}…",
+ addr_bytes[0], addr_bytes[1]
+ );
+
+ // Reject duplicate coins (replay protection)
+ let coin_id = coin_proof.coin.identifier;
+ if account
+ .coin_queue
+ .iter()
+ .any(|cp| cp.coin.identifier == coin_id)
+ {
+ return Err("Coin already in queue (duplicate)");
+ }
+ if account
+ .coin_history
+ .generate_inclusion_proof(&zkcoins_program::hash::digest_to_bytes(&coin_id))
+ .is_ok()
+ {
+ return Err("Coin already spent (replay)");
+ }
+
+ account.coin_queue.push(coin_proof);
+ Ok(())
+ }
+
+ /// Get all required merkle proofs from the state for the public key and the previous proof.
+ /// Static method: does not access self.accounts, only the state guard.
+ ///
+ /// The returned bundle is shaped for in-circuit consumption: MMR
+ /// proofs are pre-extended to [`MMR_PROOF_PATH_LEN`] siblings and
+ /// the SMT inclusion proof carries the full [`TREE_DEPTH`]
+ /// siblings (the off-circuit SMT produces this length by
+ /// construction).
+ fn get_merkle_proofs(
+ previous_proof: Proof,
+ public_key: PublicKey,
+ state: &MutexGuard<'_, State>,
+ ) -> Result {
+ let account_merkle_proofs = state
+ .get_commitment_proof(&public_key)
+ .or(Err("Unable to get merkle proofs for provided public key"))?;
+
+ // PLONKY2 MIGRATION (Step 7): see `receive_coin` for the
+ // bridge from SP1's `public_values` to Plonky2's `public_inputs`.
+ let pis: [zkcoins_program::F; zkcoins_program::circuit::main::N_PROOF_DATA_PUBLIC_INPUTS] =
+ previous_proof.public_inputs
+ [..zkcoins_program::circuit::main::N_PROOF_DATA_PUBLIC_INPUTS]
+ .try_into()
+ .map_err(|_| "Proof public_inputs too short")?;
+ let proof_data = ProofData::from_field_elements(&pis);
+ let _ = previous_proof; // silence unused-mut warning
+ let previous_root = proof_data.commitment_history_root;
+ let previous_root_proof = state.get_mmr_inclusion_proof(previous_root).or(Err(
+ "Unable to get mmr inclusion proof for the previous root",
+ ))?;
+
+ let proofs = CommitmentMerkleProofs {
+ commitment_root: account_merkle_proofs.2,
+ commitment_proof: account_merkle_proofs.1,
+ // Pad MMR proofs to the fixed depth the in-circuit gadget
+ // expects (`MMR_PROOF_PATH_LEN`). Off-circuit MMR proofs
+ // have variable depth equal to log2(capacity).
+ commitment_root_history_proof: account_merkle_proofs.3.extend_to(MMR_PROOF_PATH_LEN),
+ commitment_root_mmr_sibling: state.prev_mmr_root,
+ previous_root_history_proof: (
+ previous_root_proof.0,
+ previous_root_proof.1.extend_to(MMR_PROOF_PATH_LEN),
+ ),
+ commitment_account_state_hash: proof_data.account_state_hash,
+ commitment_out_coins_root: proof_data.output_coins_root,
+ };
+
+ Ok(proofs)
+ }
+
+ /// Build a syntactically-valid but semantically-empty
+ /// `NonInclusionProof` for inactive in-coin / out-coin slots.
+ /// The slot's `active = false` bit masks the in-circuit check.
+ fn dummy_nip() -> NonInclusionProof {
+ NonInclusionProof {
+ key: [0u8; 32],
+ root: ZERO_HASH,
+ siblings: vec![ZERO_HASH; TREE_DEPTH],
+ }
+ }
+
+ fn dummy_coin() -> Coin {
+ Coin {
+ identifier: ZERO_HASH,
+ recipient: ZERO_HASH,
+ amount: 0,
+ }
+ }
+
+ pub fn send_coins(
+ &mut self,
+ invoices: Vec,
+ account_address: Address,
+ public_key: PublicKey,
+ next_public_key: PublicKey,
+ prev_commitment_pubkey: Option,
+ ) -> Result, &'static str> {
+ // Thin wrapper: borrow the account out of the map, run the
+ // shared `send_coins_inner` body against it, and write it back
+ // on success. The Err arm leaves the map untouched.
+ let mut account = self
+ .accounts
+ .remove(&account_address)
+ .ok_or("Unknown account address")?;
+ match Self::send_coins_inner(
+ &self.prover,
+ &self.state,
+ &mut account,
+ invoices,
+ account_address,
+ public_key,
+ next_public_key,
+ prev_commitment_pubkey,
+ ) {
+ Ok(coin_proofs) => {
+ self.accounts.insert(account_address, account);
+ Ok(coin_proofs)
+ }
+ Err(e) => {
+ // Restore the account untouched. `send_coins_inner` does
+ // not commit mutations until the prove step succeeds, so
+ // the value we put back equals what we removed.
+ self.accounts.insert(account_address, account);
+ Err(e)
+ }
+ }
+ }
+
+ /// Pure-by-account variant of [`Self::send_coins`]. Runs the full
+ /// state-transition (witness assembly, prove, post-prove account
+ /// mutation) against an externally-owned `&mut Account` and returns
+ /// the produced coin proofs. The caller is responsible for deciding
+ /// whether to commit the mutated account back into the node
+ /// (e.g. after on-chain broadcast succeeded — see
+ /// [`Self::prepare_mint`] + [`Self::commit_mint`]).
+ ///
+ /// Identical body to the pre-refactor `send_coins`; the only change
+ /// is that the `account_address` lookup is the caller's
+ /// responsibility (the account is passed in). The "Unknown account
+ /// address" check therefore lives at the wrapper site.
+ #[allow(clippy::too_many_arguments)]
+ fn send_coins_inner(
+ prover: &Prover,
+ state: &Mutex,
+ account: &mut Account,
+ invoices: Vec,
+ account_address: Address,
+ public_key: PublicKey,
+ next_public_key: PublicKey,
+ prev_commitment_pubkey: Option,
+ ) -> Result, &'static str> {
+ let state = &state
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+
+ // Slot-count guards. Done up-front before the expensive
+ // get_merkle_proofs / coin-history-SMT loop so a caller
+ // violating the per-transition slot budget fails fast (and
+ // doesn't pay state-mutation cost first). `out_coins.len() ==
+ // invoices.len()` by construction in `create_coins`, so the
+ // out-coin guard collapses to `invoices.len() > MAX_OUT_COINS`.
+ const MAX_IN_COINS: usize = zkcoins_program::circuit::main::MAX_IN_COINS;
+ const MAX_OUT_COINS: usize = zkcoins_program::circuit::main::MAX_OUT_COINS;
+ if account.coin_queue.len() > MAX_IN_COINS {
+ return Err("Too many in-coins for one transition");
+ }
+ if invoices.len() > MAX_OUT_COINS {
+ return Err("Too many out-coins for one transition");
+ }
+
+ // Check if the account balance is enough
+ let balance = account
+ .coin_queue
+ .iter()
+ .fold(account.balance, |acc, x| acc + x.coin.amount);
+ let invoiced_amount = invoices.iter().fold(0, |acc, x| acc + x.amount);
+ if balance < invoiced_amount {
+ return Err("Insufficient funds");
+ }
+
+ // TODO: Copy this over to the client because they too have to check that the
+ // out_coins_tree is correct and only contains the coins from the invoices.
+ // Create the coin templates.
+ let mut coin_templates = vec![];
+ for invoice in invoices {
+ coin_templates.push(CoinTemplate::new(invoice.recipient, invoice.amount));
+ }
+
+ let mut coin_history_proofs = vec![];
+ let mut coin_non_inclusion_proofs = vec![];
+ let mut coin_inclusion_proofs = vec![];
+ let mut in_coins = vec![];
+ for coin_proof in &account.coin_queue {
+ coin_history_proofs.push({
+ match &coin_proof.commitment {
+ Some(commitment) => Self::get_merkle_proofs(
+ coin_proof.proof.clone(),
+ commitment.public_key,
+ state,
+ )?,
+ None => return Err("Coin is missing commitment"),
+ }
+ });
+ let coin_id_bytes = zkcoins_program::hash::digest_to_bytes(&coin_proof.coin.identifier);
+ coin_non_inclusion_proofs.push({
+ account
+ .coin_history
+ .generate_non_inclusion_proof(coin_id_bytes)
+ .or(Err("Should provide an inclusion proof"))?
+ });
+ coin_inclusion_proofs.push(coin_proof.inclusion_proof.clone());
+ in_coins.push(coin_proof.coin.clone());
+ account
+ .coin_history
+ .insert(coin_id_bytes, coin_proof.coin.identifier)
+ .or(Err("Coin should not exist in coin history tree"))?;
+ }
+ // PLONKY2 MIGRATION (Step 7): SP1's `ProgramInputsBuilder` has
+ // no Plonky2 analogue — the cyclic-recursion circuit's API
+ // takes per-slot witnesses (`InCoinSlotWitness`) directly. The
+ // construction below builds the same witness data, threaded
+ // through to the `Prover::prove_*` calls instead of a single
+ // builder struct.
+ let account_state_for_prove = AccountState {
+ owner: account_address,
+ balance: account.balance,
+ public_key: public_key.serialize(),
+ };
+
+ let out_coins = account.create_coins(
+ account_address,
+ next_public_key,
+ public_key.serialize(),
+ coin_templates,
+ );
+ // SparseMerkleTree::new() always returns DEFAULT_HASHES[0] as
+ // its root, and a non-inclusion-proof-driven update produces the
+ // same root as a direct insert — both invariants are part of the
+ // SMT impl's own test suite. We do not double-check here.
+ let mut out_coins_tree = SparseMerkleTree::new();
+ let _initial_root = DEFAULT_HASHES[0];
+
+ let mut out_coin_proofs = vec![];
+ for coin in &out_coins {
+ let coin_id_bytes = zkcoins_program::hash::digest_to_bytes(&coin.identifier);
+ let non_inclusion_proof = out_coins_tree
+ .generate_non_inclusion_proof(coin_id_bytes)
+ .or(Err("Coin should not exist in tree yet"))?;
+ out_coin_proofs.push(non_inclusion_proof.clone());
+ out_coins_tree.insert(coin_id_bytes, coin.identifier)?;
+ let _expected = non_inclusion_proof.insert(coin.identifier);
+ }
+
+ // Defense-in-depth: validate the source-side properties
+ // off-circuit before paying the prove cost. The in-circuit
+ // gate-set (Stage 5d-next-5 Phase 2b — merged in PR #23) is
+ // the authoritative enforcement; this off-circuit pass exists
+ // to (a) reject malformed requests with a specific HTTP error
+ // string within microseconds instead of an opaque
+ // `prove failed` after minute-scale prove cost, and (b) catch
+ // any future drift between off-circuit witness construction
+ // and the in-circuit predicate. Memory
+ // `feedback_threat_model_over_checklist`: the cost is
+ // microseconds vs minute-scale prove, so the defense-in-depth
+ // wins. See `MIGRATION_RESEARCH.md` §7.22 for the in-circuit
+ // architecture (aggregator pattern + Phase 2b per-slot SMT
+ // inclusion + SPEC §8 (c)(d)(e) chain).
+ for ((coin, source_cmp), source_inclusion) in in_coins
+ .iter()
+ .zip(coin_history_proofs.iter())
+ .zip(coin_inclusion_proofs.iter())
+ {
+ if !source_inclusion.verify(coin.identifier, source_cmp.commitment_out_coins_root) {
+ return Err("In-coin not present in source's output_coins_root");
+ }
+ if !source_cmp.verify_commitment(state.mmr.root_extended(MMR_PROOF_PATH_LEN)) {
+ return Err("Source commitment not present in history MMR");
+ }
+ }
+
+ // Build the fixed-shape MAX_IN_COINS slot tuples. Active
+ // slots come from account.coin_queue; inactive slots use the
+ // ZERO_HASH dummies. Slot-count guards live at the top of
+ // `send_coins`; by the time we reach this point both
+ // `in_coins.len() <= MAX_IN_COINS` and `out_coins.len() <=
+ // MAX_OUT_COINS` are invariants of the function.
+ let dummy_nip = Self::dummy_nip();
+ let dummy_coin = Self::dummy_coin();
+ let mut in_coin_slots: Vec<(bool, &Coin, &NonInclusionProof)> =
+ Vec::with_capacity(MAX_IN_COINS);
+ for (coin, nip) in in_coins.iter().zip(coin_non_inclusion_proofs.iter()) {
+ in_coin_slots.push((true, coin, nip));
+ }
+ for _ in in_coins.len()..MAX_IN_COINS {
+ in_coin_slots.push((false, &dummy_coin, &dummy_nip));
+ }
+
+ // Stage 5d-next-5 Phase 2b: per-slot source witnesses. Each
+ // active in-coin's source proof, SMT-inclusion path, and
+ // CommitmentMerkleProofs bundle (already built into
+ // `coin_history_proofs` / `coin_inclusion_proofs`) are
+ // threaded into the prover. Inactive slots get `None`.
+ let mut sources: Vec