Skip to content

feat: launch hardening — SP1 produce-and-submit, SOR execution, BSM slash lifecycle, CI guest parity#16

Merged
drewstone merged 10 commits into
mainfrom
feat/launch-hardening
Jun 13, 2026
Merged

feat: launch hardening — SP1 produce-and-submit, SOR execution, BSM slash lifecycle, CI guest parity#16
drewstone merged 10 commits into
mainfrom
feat/launch-hardening

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Closes the launch-blocking gaps from the post-merge implementation audit. One branch, verified in stages.

SP1 proven path → produce-and-submit

  • CI now builds the real guest ELF and asserts in-circuit parity (zk-guest job): 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).
  • The prover now PRODUCES and SUBMITS: settle_batch_fills_proven on the chain client + --submit/--rpc/--key on the prover (connect → assert domain → settleBatchProven with the exact BatchFill[]). prove-batch.sh submit completes the runbook.
  • Deploy wires it: setSp1Verifier (mock on anvil, SP1_VERIFIER+SP1_VKEY on real chains) and an initial registerBook, so the attested AND proven batch paths work from block 0 (production governance registers the real quorum).

Bugs fixed

  • Frontend quote/execution divergence + dead SOR: Buy now prices AND executes against the merged cross-venue NBBO ladder — the quote reads the aggregated book and execute() walks planRoute into a split plan, filling each venue's slice through the firm path. The previously-unused SOR is now live; savings shown == liquidity swept.
  • Operator domain guard: the CLOB attested driver (the primary live settle path) now asserts the on-chain EIP-712 domain separator before submitting (fail-closed), matching the direct-flush path.
  • Operators page: "quoting / serves the venue API" reflects the live venue registry, not a hardcoded index 0.

BSM hardening

  • Closes the "leave between default and challenge" dodge (departed operators stay slashable through a 7-day exit-grace window).
  • Slash-lifecycle attribution (onUnappliedSlash/onSlash), slashToDefault mapping, governance resetChallenge (recover a one-shot consumed by a disputed/cancelled core slash), exposureBps carried in DefaultChallenged.

Resilience / DX

  • App route-level error boundary (RouteError) — a malformed response degrades to a panel instead of white-screening.
  • app/.env.example documenting VITE_* (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).
  • Rust workspace + chain/mesh build; operator tests pass; app tsc + vite build pass; prover host build + arg validation pass; guest parity verified locally.

Deliberately NOT changed (security/architecture rationale)

  • BSM → settlement quorum is governance-driven (not auto-projected): rotateAttesters stays onlyOwner (timelock/multisig). Auto-wiring the BSM to rotate the quorum would bypass the timelock — wrong for the multisig-governance launch.
  • On-chain completeness/censorship for the proven path remains an off-chain audit against the emitted ordersCommitment (on-chain enforcement needs data availability) — unchanged, by design.

Known tail (follow-ups, not launch-blocking)

Operator-driven attested-redemption/claimDefault automation; 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.

drewstone added 10 commits June 13, 2026 09:47
…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 drewstone merged commit 07aaafe into main Jun 13, 2026
7 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant