Skip to content

kona-sp1-proposer: defend challenged games with SP1 super-aggregation proofs - #22121

Open
digorithm wants to merge 13 commits into
developfrom
feat/kona-sp1-proposer-defend
Open

kona-sp1-proposer: defend challenged games with SP1 super-aggregation proofs#22121
digorithm wants to merge 13 commits into
developfrom
feat/kona-sp1-proposer-defend

Conversation

@digorithm

@digorithm digorithm commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports the defend path from op-succinct's fault-proof proposer (@ 13716c2c): when a game the proposer can prove gets challenged, it fetches the span's super roots, collects witnesses, obtains an SP1 super-aggregation proof bound to its signer, and submits prove(). The existing resolution and claim tasks then finish the game (including the challenger-bond prover credit).

Closes #22147. Fast finality (prove at creation) is deliberately not ported; tracked in #22112. Stacked on #22097 (merged; now targeting develop).

What is upstream and what is ours

  • Ported near-verbatim (diff against upstream side by side): prover.rs (SPN request/poll state machine; cluster arm and two request knobs dropped), the defense scheduling in proposer.rs (spawn_game_defense_tasks, should_skip_proving, spawn_game_proving_task, deadline tiers), RangeSplitCount.
  • Ours by necessity (no upstream analog): proving.rs replaces upstream's single-chain witness/stdin machinery with the super-root stack (super-range-executor witness collection, SuperAggregationInputs); synthesize_execution generalized to timestamp spans (host-side only, no vkey change); the multi-prestate PrestateCache gains per-prestate proving keys and the deferred vkey verification (agg vk must hash to the on-chain prestate).
  • Ownership is now prestate-based (option A, kona-sp1-proposer: which games should the proposer defend when challenged #22111): the proposer proves, resolves, and claims every game whose prestate artifacts it can load, regardless of creator. Prove set = resolve set = claim set.

No real proofs in this PR

There are no SPN calls and no SNARK cryptography in any test. All coverage runs the mock provider: submitted proof bytes are a placeholder constant and devstack's ZKMockVerifier accepts any bytes. What the tests exercise for real is every computation around the proof: span fetching, witness collection from live nodes, native re-derivation of the state transition (the same code the guest programs compile from), consolidation checks, aggregation-input validation, and the exact public-values tuple prove() reconstructs on-chain. The one unexercised seam, generate_*_proof() returning verifier-accepted bytes, is upstream's code nearly verbatim and gets a manual SPN smoke once credentials are provisioned.

Hardening beyond upstream

Two serious issues caught in pre-merge review, fixed with pinning unit tests. Neither exists upstream; both are invariants upstream gets structurally for free that our architectural departures initially violated:

  1. Permanent verdicts are trust-gated: the undefendable skip set (added to bound retry spend; upstream retries forever) only condemns a game on a supernode answer with current_l1 >= verified_required_l1. Without the gate, a mid-rewind stale answer could permanently mark an honest game undefendable and burn its bond.
  2. Poisoned prestates close the creation gate: upstream verifies embedded ELF vkeys before creating; our runtime-fetched artifacts verify at first key setup, which initially left the existence-only gate open, letting the proposer keep bonding games it had proven it cannot defend. ensure_loaded now returns false for poisoned entries and heals only when the published artifacts actually change.

Smaller deliberate divergences, flagged inline: pre-submit re-check before the prove tx (upstream submits blind), sync loads every cached game's prestate so post-rotation games stay owned (upstream is single-identity), mock provider is native-core rather than emulator-based (stub artifacts are not ELFs).

Test plan

  • Rust: 84 unit tests (proposer + super-range-executor) incl. ported upstream tables, six migrated span tests, and the two hardening pins; clippy --all-features --all-targets --locked and just lint-docs clean.
  • Go: gofmt/vet/op-golangci-lint clean.
  • Acceptance (sysgo, stub mode, PR CI lane): full ZK suite 10/10, incl. TestChallengedValidProposalAnchors now proposer-driven (the TODO(#21463) flip) and new TestProposerDefendsForeignValidGame pinning option A end to end (foreign game proven, resolved, challenger bond claimed).
  • Real-ELF lane: TestChallengedValidProposalAnchors + TestProposerClaimsBondAfterResolution pass with real artifacts.
  • TestZKDisputeGameWithdrawal stays skipped: its single-chain preset fails bring-up installing the ZK game type (pre-existing, unrelated to the proposer; issue to follow).

digorithm and others added 10 commits July 28, 2026 20:35
Dead code (state snapshot trio, duplicate confirmation constants, backup-era
serde derives and deps, devstack metrics block), PrestateCache newtype,
startup logging for signer and resolved config with URL redaction,
GamesCreated and GamesBondsClaimed double counting, tokio::fs artifact reads,
named anchor root fields, missing-to signer error, TX_CONFIRMATION_TIMEOUT=0
rejection, required super-root timestamp fields, doc fixes.

Part of #22086.
Pending-exclusion contrast test, bond_claim_action ordering cases,
ProposalStatus decode rejection, PROPOSAL_SAFETY field selection test,
revert-prefix pin test, super-root response timestamp verification,
WithoutHonestChallenger honored in the non-ZK path, wait.For-based factory
waits, opportunistic root check in WaitForAnchorRootAtLeast, race-free
chaining test assertions.

Part of #22086.
One unreadable game no longer aborts the sync cycle (walk, pending
re-validation, and status classification each contain per-game errors and
surface them via a new game_sync_error counter; the cursor only advances
past a complete walk). Pending entries whose deadline fell behind the anchor
beyond the lag cutoff are evicted (one-sided; fresh games under a stalled
anchor stay re-checkable). Own-game super-root mismatches stay re-checkable
instead of terminally dropping our bond on bad supernode data.
is_parent_resolved only counts DefenderWins parents. Creation checks the
chosen parent against the registered registry's isGameBlacklisted and
isGameRetired (a retired or blacklisted parent reverts child creation
forever and nothing pruned it; the stall survived restarts).

Structural decomposition of sync_games stays in #22019 (blocked on #21463).

Part of #22086.
A create tx that lands after its confirmation timeout left the duplicate
creation guard unarmed; the next tick treated the collision as third-party,
advanced the timestamp, and bonded a duplicate game on the same parent. The
collision arm now consults gameCreator(): our own game is adopted (guard
re-armed), third-party collisions still advance.

MAX_FEE_PER_GAS and MAX_PRIORITY_FEE_PER_GAS (wei, unset = uncapped) bound
what the fee estimator may set: fees are clamped after fill in both signer
arms, restoring the priority <= max_fee invariant. The local signer fills on
a wallet-less provider first, since a wallet provider signs during fill and
a signed envelope cannot be clamped.

Part of #22086.
Co-authored-by: Inphi <mlaw2501@gmail.com>
Inline the parent_status_resolves helper into is_parent_resolved and drop
its tautological test (review feedback: PartialEq comparison needs no
helper). Add a TODO to strengthen the chaining test's anchor assertion via
the start-proposer-mid-test hook once #22105 lands.

Part of #22086.
…amp spans

The defend path proves whole game spans, not single timestamps. The guest
input types already supported spans; this generalizes synthesize_execution
host-side (responses slice covers span.start-1..=span.end) so no vkey
changes. The executor binary keeps single-timestamp behavior, pinned by a
bundle-equality regression test plus the six migrated tests.

Also extracts HostInputs (deployment-scoped endpoints and config paths) and
makes the witness-collection helpers pub so the proposer can drive the same
machinery as a library.
Port of upstream fault-proof (@ 13716c2c) defense: challenged games in the
owned set get a defense task (deadline-ascending, capped, per-game dedup)
that fetches the span's super roots, collects witnesses, obtains an SP1
super-aggregation proof bound to the signer, and submits prove(). The
existing resolution and claim tasks finish the game.

Departures from upstream, each forced by this repo's architecture:
- proving.rs replaces the single-chain host machinery with the super-root
  stack; the aggregation guest validates one shared l1Head, so the header
  chain walk is gone.
- Ownership is prestate-based (option A, #22111): prove, resolve, and claim
  are one set, keyed by loadable prestate artifacts. Sync loads every cached
  game's prestate so post-rotation games stay owned.
- Providers are Network (SPN request/poll, ported) and Mock (native-core,
  no ELFs, placeholder bytes; dev verifiers only). No cluster arm.
- Proving keys are per-prestate; setup verifies the aggregation vkey hashes
  to the on-chain prestate (the PR-1 deferred check). Poisoned prestates
  close the creation gate and heal only when artifacts change.
- Permanent unprovable verdicts are trust-gated on current_l1 so a stale
  mid-rewind supernode answer cannot burn an honest game's bond.
- Pre-submit re-check avoids guaranteed-revert prove() txs after long
  proving runs. No restart recovery, matching upstream.

Part of #21463. Fast finality is deliberately not ported (#22112).
The devstack launcher now plumbs the witness endpoints (L1 beacon, both L2
ELs) and materializes the rollup, L1, and depset configs the InteropHost
needs, running the proposer with PROOF_PROVIDER=mock.

TestChallengedValidProposalAnchors drops its manual prove(): the proposer
detects the challenge and submits the proof itself. The new
TestProposerDefendsForeignValidGame pins prestate-based ownership end to
end (foreign game proven, resolved, challenger bond claimed), created off
the proposer's proposal grid to avoid a factory UUID collision. The ZKGame
Prove DSL helper is deleted with its last caller.

zkProveDuration goes to 120s: the defend flow now does real work inside the
prove window on CI runners, and time-traveled expiries keep wall cost flat.
TestZKDisputeGameWithdrawal stays skipped for its real blocker: the
single-chain preset fails bring-up installing the ZK game type.
@digorithm
digorithm requested a review from a team as a code owner July 30, 2026 03:50
@digorithm
digorithm requested review from Inphi and wwared July 30, 2026 03:51
@wiz-0f98cca50a

wiz-0f98cca50a Bot commented Jul 30, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities 3 High 2 Medium 3 Low
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings -
Software Management Finding Software Management Findings -
Total 3 High 2 Medium 3 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

A stale local format pass committed leading-operator style on untouched
lines; re-running the pinned nightly formatter restores the repo style.
Groth16 is the SNARK scheme behind AGG_PROOF_MODE=groth16, not a typo for
growth.
@digorithm digorithm self-assigned this Jul 30, 2026
Public docs may not link private items under -D warnings; inline the trust
condition instead.
Base automatically changed from kona-sp1-proposer-nits to develop July 30, 2026 22:08
…oser-defend

# Conflicts:
#	rust/Cargo.lock
#	rust/kona/sp1/crates/proposer/src/bin/kona-sp1-proposer.rs
#	rust/kona/sp1/crates/proposer/src/config.rs
#	rust/kona/sp1/crates/proposer/src/metrics.rs
#	rust/kona/sp1/crates/proposer/src/proposer.rs
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.

proposer: port defend/prove path

1 participant