Promote: staging -> develop - #200
Merged
Merged
Conversation
… subset parallelism (#196) The "DB Subset Tests" CI job runs a narrow nextest selection (`db::tests` + `job_store::tests` + `router::tests::jobs_*` + ...) under `--test-threads 8`. In that job the five SSE tests `router::tests::jobs_endpoint_tests::jobs_stream_*` intermittently fail with `create: PoolTimedOut` after running >100 s, while the full coverage gate (same `--test-threads 8`, on the same SHA) keeps them green. Root cause is migration-replay contention, not raw connection exhaustion. Every test that calls `crate::test_db::setup_pool()` CREATEs a fresh per-test schema and replays the full migration suite (16 DDL files: tables, triggers, views) into the single shared `postgres:17` container. Postgres serialises concurrent DDL on its system catalogs, so when the DB subset packs the migration-replaying tests together and eight run at once, each `setup_pool()` stretches from <1 s to tens of seconds. The `jobs_stream_*` tests additionally hold their pool across deliberate sleep/timeout windows, so under that contention their connection acquisition exceeds the pool's 60 s `acquire_timeout` and surfaces as `PoolTimedOut`. The full gate stays green because the same heavy tests are interleaved across the entire suite rather than clustered. Peak server connections stay ~14/100 throughout, confirming the bottleneck is DDL catalog locking. Fix: add a workspace-root `.config/nextest.toml` test-group that caps the `router::tests::jobs_endpoint_tests` module at 2 concurrent threads. This bounds simultaneous migration replays for the heaviest module so connection acquisition stays well under the 60 s timeout, while keeping useful parallelism for the rest of the suite. The config is honoured by both `cargo nextest run` (the subset gates) and `cargo llvm-cov nextest` (the coverage gate), carries no coverage semantics, and touches no test pool — the deliberately-narrow error-path `dead_pool` (`max_connections(1)` / 50 ms timeout) keeps exercising its `PoolTimedOut` arms verbatim. Verified locally: the exact DB-subset selection now passes 239/239 twice with no `PoolTimedOut` (the `jobs_stream_*` tests drop from ~34 s to ~15 s each), and the 100% line + function coverage gate is unchanged.
* docs: add decentralization roadmap (run-your-own-node, SPEC-anchored) * docs: rename to DECENTRALIZATION_ROADMAP.md (ROADMAP.md already taken) * docs(roadmap): set decentralization as the current focus (fold in S1-S7 + D2/D7/D8) * docs(roadmap): set decentralization as the current focus (fold in S1-S7 + D2/D7/D8)
TaprootFreak
marked this pull request as ready for review
June 4, 2026 15:09
…the jobs API (#198) The jobs-API admit handlers (`POST /api/jobs/mint`, `POST /api/jobs/send`) require the `Idempotency-Key` request header (`read_idempotency_key`). A browser sending that header triggers a CORS preflight (OPTIONS), but the router's `CorsLayer` only allowed `Content-Type` in `Access-Control-Allow-Headers`. The preflight therefore failed and the web frontend could not mint or send. Add `idempotency-key` to the CORS `allow_headers` list so the preflight succeeds. `HeaderName::from_static` requires the lowercase form. Cover the fix with a CORS preflight test (`OPTIONS /api/jobs/mint` with `Access-Control-Request-Headers: idempotency-key`) asserting the response echoes both `idempotency-key` and `content-type` in `Access-Control-Allow-Headers`.
…#195) * feat(jobs): expose account_state_hash + output_coins_root on awaiting_signature job result A pure-TypeScript wallet must know account_state_hash (ash) and output_coins_root (ocr) to sign the send commitment, but until now the awaiting_signature JobStatus carried only proof_id. The hashes were reachable solely via GET /api/proof/{id} as a binary bincode CoinProof blob that only Rust/wasm can decode — breaking the thin-client rule (wallet = key only, trusts the node, no heavy client-side logic). This change writes ash + ocr as lowercase hex into the job result when a send job transitions to awaiting_signature, so GET /api/jobs/:id and the SSE stream surface them under result.account_state_hash / result.output_coins_root — the exact keys @zkcoins/sdk's pay() reads. ash/ocr come from the same source the completed mint/commit results use: ProofData::from_field_elements over the send proof's public inputs, hex-encoded via digest_to_bytes. Extraction is factored into a shared flow::send_commit_hashes helper that mint_flow, send_flow, and commit_flow all call, so the hex is bit-identical to what createCommitment expects and commit_flow re-derives. Purely additive: completed result shape, proof_id top-level field, and the JobStatusResponse wire schema (result is already free-form JSON) are unchanged. No new endpoint, env var, or migration. set_awaiting_signature stores the result in the existing response_body column (the terminal complete body overwrites it later); the GET handler and SSE initial frame now surface result for awaiting_signature in addition to completed, and a post-restart resume re-publishes the persisted hashes. Tests: api_remote send roundtrip asserts the awaiting_signature result hex equals the proof-decoded ash/ocr; job_store + router unit tests cover the new persistence and snapshot paths (100% line + function gate green). * docs(jobs): correct response_body field comment for awaiting_signature
Add a typed, lowercase string enum field `bitcoin_network` to the /api/info response with exactly two variants: "mainnet" and "mutinynet". The value is derived from the existing NETWORK_CONFIG.is_mainnet flag via a pure, unit-testable helper (bitcoin_network_label) — no new env var, no new config source. The free-text `network` field (e.g. "Mainnet"/"Mutinynet" from NETWORK_CONFIG.network_name) is retained unchanged for backward compatibility; bitcoin_network is additive. This fixes the latent case-mismatch foot-gun documented for the wallet/SDK, which should switch behaviour on the typed identifier rather than matching the operator-overridable free-text label. Register BitcoinNetwork as a ToSchema component in the OpenAPI spec. Cover both helper arms with a unit test, assert the field in the existing /api/info handler tests, add OpenAPI smoke drift guards, and add a no-fallback contract assertion in the api_remote E2E suite.
* feat(state): self-heal persisted proofs on circuit change
The Plonky2 state-transition circuit is cyclic: every proof is fed back
as the recursive inner proof on the next transition. When a circuit
change breaks recursion, persisted account proofs become incompatible
and the next mint/send aborts witness generation with a "Partition ...
was set twice with different values" copy-constraint conflict, surfaced
to the wallet as "prove failed". This took DEV down and required a
manual reset-zkcoins-node.
Add a boot-time self-heal that detects the incompatibility and resets
the proof-dependent state to genesis (the documented tabula rasa,
permitted in the closed test env), storing the live circuit digest so
subsequent boots are an O(1) comparison.
Detector. Two stages: (1) compare the persisted circuit_digest against
the live one — the cheap steady-state fast path; (2) on the adoption
boundary (no digest recorded yet) run a canary recursion: recurse a
persisted proof through the live circuit's AccountUpdate branch with the
real commitment-merkle witnesses from the loaded state. Stale ⇒ reset.
Why the canary and not Prover::verify / a digest comparison alone:
verified against the live DEV dump, the breakage does NOT change the
verifier-key circuit_digest. Plonky2's circuit_digest hashes the
constants/sigmas cap + domain separator + degree but NOT the gate
constraints (upstream circuit_builder.rs "TODO: This should also include
an encoding of gate constraints"). The DEV proofs' embedded digest was
byte-identical to the current build's and Prover::verify passed on them,
yet the recursive prove still failed. Only running the real recursion
reproduces the failure.
The digest is deterministic across separate builds of identical circuit
code (no nonce/timestamp), so a digest CHANGE still reliably signals a
circuit change — it just has a blind spot for constraint-only changes
that the canary closes.
- migration 0015: singleton circuit_digest_meta table
- db: load/store circuit digest + transactional proof-dependent reset
- account_node: CanaryOutcome + canary_recursion (real AccountUpdate
recursion probe) + take_prover for the post-reset reload
- self_heal: reset_decision (pure, exhaustively unit-tested) +
heal_circuit_digest orchestrator
- main: build prover once, load state+accounts, heal, reload from
genesis on reset (prover reused — circuit built once)
* fix(state): use real account state in self-heal canary to avoid false-positive reset
The boot self-heal canary recursed a persisted proof through the live
circuit's AccountUpdate branch with a SYNTHETIC surrounding AccountState
({ owner: ZERO_HASH, balance: 0 }). That state violates the §8(b)/(c)
state-continuity constraints, so the canary returning Ok relied on the
fragile Plonky2 invariant that arithmetic gate constraints are not
evaluated at witness/prove time. Worse, there was no proof the canary
returns Compatible (not a false Stale -> genesis wipe -> production data
loss) on a genuinely compatible but digest-less DB — the first-boot case
of every existing node adopting this fix when no breaking change occurred.
Rebuild the REAL account state, exactly as the production prove path
(account_state_for_prove): owner = account address, balance =
account.balance, public_key = the account's CURRENT key. The current key
is NOT the persisted commitment_public_key: the circuit commits
ProofData.account_state_hash as final_account_state_hash, which embeds
the producing transition's next_public_key (the key it rotated TO). By
the rotation chain that equals the next transition's public_key; for the
minting account it is generate_public_key(derive_num_pubkeys_from_smt()).
commitment_public_key is still used, but only to look the commitment up
in the SMT via get_merkle_proofs (mirroring send_coins_inner's prev_cmp).
The boot path supplies the current-key resolver, reconstructed from the
same compile-time minting secret the node already uses and resolved off
the SMT the canary already holds (the resolver MUST NOT re-lock state —
the canary holds it, and a re-lock deadlocks the non-reentrant guard).
With the real state both §8(b)/(c) are satisfiable for a compatible
proof, so the only remaining prove-time failure path is the recursion
copy-constraint set_proof_with_pis imposes on the inner proof — exactly
what a breaking circuit change violates. Err => Stale no longer depends
on which constraints Plonky2 evaluates at prove time. Prover::verify
stays out of the detector.
Verified by a live boot-gate in BOTH directions: a stale DEV dump still
resets to genesis (Canary Stale -> Reset, post-reset mint completes), and
a genuinely compatible digest-less DB now baselines without wiping
(Canary Compatible -> Baseline, accounts preserved, mint completes).
Also:
- canary_recursion: document the append-only proof-data PI-slot
assumption (a future circuit change reordering the first
N_PROOF_DATA_PUBLIC_INPUTS slots would make get_merkle_proofs Err for
every sample -> NoSample -> Baseline -> no reset despite staleness, a
False Negative). Emit a tracing::warn when proof-carrying accounts
exist but all are skipped. NoSample stays Baseline (the data-loss-safe
direction for benign state gaps), not Stale, by design.
- reset_proof_dependent_state_tx: state the exact wipe set, note
usernames is intentionally preserved (not proof-dependent), and note
coin_proof_store (migration 0008) is unused schema groundwork with a
MIGRATION_RESEARCH note to add it to the reset if the DB-backed
ProofStore bootstrap later lands.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automatic Promote PR
Commits: 1 new commit(s)