feat: launch hardening — SP1 produce-and-submit, SOR execution, BSM slash lifecycle, CI guest parity#16
Merged
Merged
Conversation
…ier wiring - BSM: close the "leave between default and challenge" dodge — a departed operator stays slashable through a 7-day exit-grace window (matches the protocol exitQueueDuration); track slashId->defaultId; emit slash-lifecycle attribution (onUnappliedSlash/onSlash); add governance resetChallenge() to recover a one-shot consumed by a disputed/cancelled core slash; carry the offender's exposureBps in DefaultChallenged. - Deploy: wire the SP1 verifier (mock on anvil, SP1_VERIFIER+SP1_VKEY on real chains) via setSp1Verifier, and register an initial settlement book (attester quorum) so the attested AND proven batch paths work from block 0 (dev/testnet or REGISTER_BOOK=1; production governance registers the real quorum). forge: 112 tests pass; anvil deploy verified (book threshold + verifier read back).
…ettle path The direct-flush path verified the deployed contract's EIP-712 domain separator (chain id + contract address) before submitting, but the CLOB attested driver — the primary live settlement path — did not. A wrong SURPLUS_RPC_URL chain-id or contract address would silently produce batch digests the quorum can't verify, failing every settle confusingly. chain_client() now asserts the domain once (cached via a domain_checked flag) and fails closed on drift.
The zk job built the host with SP1_SKIP_PROGRAM_BUILD=1 (a stub ELF) and a comment claimed the full guest build ran in "a separate, optional job" that did not exist — so a broken circuit or a guest/host public-values drift shipped unnoticed. Add a real zk-guest job: install the Succinct toolchain, compile the RISC-V guest, run it in execute mode over a real signed crossing set, and assert its committed public values equal the host's recomputation. Verified locally (2 orders -> 1 fill, parity holds). Fix the false comment in the host job.
…oundary - Buy: price AND execute against the merged cross-venue NBBO ladder. The quote now reads the aggregated book (useAggBook) instead of a single home venue, and execute() walks planRoute into a split plan, filling each venue's slice through the firm path (a single-venue set pins that operator) — so the savings shown are exactly the liquidity swept. Fixes the quote/execution divergence and makes the previously-dead SOR (planRoute) live. Falls back to best-across-all when no agg book (single-venue dev). - Operators: "quoting / serves the venue API" now reflects the live venue registry (healthy endpoint per operator), not a hardcoded index-0 — service membership order is not a quoting signal. - Add a route-level error boundary (RouteError) so a malformed response degrades to a panel instead of white-screening the app. - Add app/.env.example documenting VITE_* (incl. the shared WalletConnect id). app: tsc clean + vite build pass.
The proven path produced a proof but nothing submitted it (the audit's "no automation ever submits an SP1 proof"). Close it: - chain client: settle_batch_fills_proven(book_id, ordersCommitment, fills, proof) — the proven mirror of the attested fills-variant, so the BatchFill[] calldata is byte-identical to what the matcher produced and the proof's public values bind to. - prover: --submit/--rpc/--key. After generating the groth16 proof it connects, asserts the on-chain domain separator, and calls settleBatchProven with the exact fills — the prover now PRODUCES and SETTLES a batch. - prove-batch.sh: a `submit` mode (groth16 + on-chain submit) gated on SURPLUS_RPC_URL + SURPLUS_SUBMITTER_KEY; completes the runbook. Wiring is build-verified; the on-chain settleBatchProven call is the same one Batch.t.sol exercises against the SP1 verifier, and CI now proves guest==host public values. Real groth16 generation needs the SP1 prover (cpu/cuda/docker), so the produce-and-settle e2e runs via this runbook, not per-CI.
- CI: add an `e2e` job running scripts/settlement-e2e.sh — the full guarantee loop on anvil (deploy with book+verifier wired -> atomic fill -> receipt redemption -> collateral default -> attested batch -> proven batch against the strict mock verifier). Eleven e2e scripts existed; none ran in CI. This one is self-contained (foundry + cargo) and exercises the chain client's live settle paths, including settleBatchProven. - Deploy definitions advertised supported_memberships=["dynamic"] only while the BSM and the app manifest support BOTH; declare ["fixed","dynamic"] so a service can be requested either way.
… == sum of lot notional) The resale path is the one fill where total notional is not conserved: a buyer paying below the carved pro-rata shrinks both the issuer's liability and the lot's notional by the shortfall. Pin the invariant that matters — issuer liability always equals the sum of their outstanding lots' notional — for a below-prorata resale, so the accounting can't drift unnoticed (was untested).
Completes the fail-closed domain guard across all settlement entry points: a spend-only operator (no CLOB or direct fills) has the spend flush as its sole chain path, so it must catch a wrong chain-id/contract before settling. Checked once via a domain_checked flag, mirroring the CLOB driver.
The publish/consume halves were already wired — the operator surfaces its onion over /health and the app registry dials it under privacy mode — but nothing ran Arti, so no operator had a real .onion. Close it at the deploy layer (Arti as a managed sidecar, not embedded — keeps the operator binary lean): - arti.toml: the venue as an onion service (onion :80 -> local venue port). - surplus-arti.service: runs Arti; publish-onion.sh copies the generated <hash>.onion to a stable path once it bootstraps. - operator: /health now reads the onion from SURPLUS_ONION_FILE (live) as well as SURPLUS_ONION_URL, normalizing the scheme — so the freshly generated hostname is published without a venue restart. - runtime unit wires SURPLUS_ONION_FILE + the Arti dependency. Consumer IP-privacy to the operator is now real end to end (operator reachable via .onion; PRIVACY_MODE already tunnels outbound inference through Arti's SOCKS).
drewstone
added a commit
that referenced
this pull request
Jun 13, 2026
PR #16 wired the SOR into the main Buy funnel (price + execute against the merged NBBO via planRoute + pinned buyLeg). This applies the SAME pattern to the ModelMarket quick-buy ticket — which already PRICED against the merged book but still EXECUTED single-venue: walk planRoute over the aggregated ask ladder, fill each operator's slice through the firm path (single-venue set pins the operator), and collapse the per-leg receipts into one blended summary for this single-receipt surface. Reconciled with #16: dropped my earlier redundant `buySplit` hook (#16's buyLeg-with-pinned-venue pattern is leaner and already established) and the Buy.tsx change (#16 owns that). Net change is one file. App tsc + vite build clean.
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.
Closes the launch-blocking gaps from the post-merge implementation audit. One branch, verified in stages.
SP1 proven path → produce-and-submit
zk-guestjob): compiles the RISC-V guest, runs it in execute mode over a real signed crossing set, and checks its committed public values equal the host's recomputation (domainSeparator, bookId, nonce, ordersCommitment, fillsHash). Previously the job used a stub ELF (SP1_SKIP_PROGRAM_BUILD=1) and a false "separate optional job" comment, so a broken circuit could ship. Verified locally (2 orders → 1 fill, parity holds).settle_batch_fills_provenon the chain client +--submit/--rpc/--keyon the prover (connect → assert domain →settleBatchProvenwith the exactBatchFill[]).prove-batch.sh submitcompletes the runbook.setSp1Verifier(mock on anvil,SP1_VERIFIER+SP1_VKEYon real chains) and an initialregisterBook, so the attested AND proven batch paths work from block 0 (production governance registers the real quorum).Bugs fixed
execute()walksplanRouteinto a split plan, filling each venue's slice through the firm path. The previously-unused SOR is now live; savings shown == liquidity swept.BSM hardening
onUnappliedSlash/onSlash),slashToDefaultmapping, governanceresetChallenge(recover a one-shot consumed by a disputed/cancelled core slash), exposureBps carried inDefaultChallenged.Resilience / DX
RouteError) — a malformed response degrades to a panel instead of white-screening.app/.env.exampledocumentingVITE_*(incl. the shared WalletConnect id).Verification
forge test: 112 pass (16 BSM incl. grace-window + reset + slash lifecycle; anvil deploy read-back confirms book + verifier wiring).chain/meshbuild; operator tests pass; app tsc + vite build pass; prover host build + arg validation pass; guest parity verified locally.Deliberately NOT changed (security/architecture rationale)
rotateAttestersstaysonlyOwner(timelock/multisig). Auto-wiring the BSM to rotate the quorum would bypass the timelock — wrong for the multisig-governance launch.ordersCommitment(on-chain enforcement needs data availability) — unchanged, by design.Known tail (follow-ups, not launch-blocking)
Operator-driven attested-redemption/
claimDefaultautomation; spend-rail streaming (stream=true); an e2e CI job; the deployable-definition membership-model label (dynamic-only vs the BSM's both); real Arti/Tor process management. Happy to take these next.