igneum/docs/plans/second-prover.md

9.9 KiB

Mission item 9: the second proof system on the active list and the one-third stop switch

7 October 2026, from 10:1x UK (the project lead: build it now). The proving lane, branch second-prover (worktree igneum-wt-second-prover), node side on the fork branch second-prover-node (from release-0.3.19-node e69e8a39). What the mission asks (docs/analysis/mission/mission.md 2.9; future.md sections 5 and 9; frontier I4; ledger P7 and D6): a second zkVM behind the ProofSystem trait proving the same segments on one fleet box as a shadow; a disagreement is a public alert and the pool stops paying on proofs until one third of weight signals which system to trust; full nodes keep the native-execution veto whatever happens. Gate: 1,000 segments agree across both systems; one injected bad proof disagrees, pays nothing and raises the alert.

1. The system chosen: RISC Zero 3.0.6, and why

The choice was made on evidence that already existed in the tree, not from memory.

Criterion RISC Zero 3.0.6 OpenVM 2.0.2 Source
Independence from SP1's code its own circuit stack (rv32im circuit, BabyBear, Poseidon2, its own FRI and recursion); shares no prover code with SP1 built on Plonky3, the stack SP1 came from before Hypercube; a bug in a shared layer hits both docs/analysis/proving-methods.md 1.5 and 2.1; the SP1 v3.4.0 disclosure was partly a Plonky3 evaluation gap (future.md 5.3)
Toolchain maturity rzup, cargo-risczero, r0vm, a release line since 2022; the May 2025 soundness fix shipped with an estop 2.0.2 of 14 Aug 2026; no shipped recursion-to-constant-size path measured here docs/analysis/amd-proving.md table; proving-methods.md 2.1
Guest portability of the shard program igneum-prove-core ports as is: methods/guest/src/main.rs is 14 lines (read the frame, run shard_statement, commit the 328 bytes); RISC Zero's own patches for keccak (tiny-keccak), sha2, k256 the same port would work; no measurement exists here proving/igneum-prove-r0/methods/guest
Proof size succinct receipt 223,882 bytes, constant not measured here bench rows on the 4090, 6 October 2026
Prover time on a 4090 v1 shard (10.0 M cycles): 23.1 s at po2 20, 32.7 s at po2 19; the 89.5 M-cycle shard 178 s; the 134.7 M-cycle shard 355 s not measured here /root/rows on rz-4090 and bench/proof-systems rows, 6 October 2026 13:35Z to 14:01Z
What already existed a pinned guest (image id 0x9ae0f416...), a host with the SP1 host's CLI, a CUDA build on the fleet box nothing branch proving-v2 (the proving-methods route D measurement)

RISC Zero. The independence row decides it: the second system exists to disagree with the first when the first is wrong, and a shared layer is a shared bug. The rest is cost: the guest, the pin and the measurements were in the tree.

2. What was built

Part Where State
The ProofSystem trait in one SDK-free crate proving/igneum-prove/core/src/proof_system.rs (trait, SegmentClaim, ShardWitness, PgasTable, VERSION_SP1 = 1, VERSION_RISC0 = 2, system_name); the SP1 host re-exports it; the RISC Zero host implements it (proving/igneum-prove-r0/host/src/proof_system.rs, Risc0ProofSystem, and --mode compressed runs through prove_shard) built on the box
The second system's host, guest and pin proving/igneum-prove-r0/ from proving-v2 (RISC Zero 3.0.6, proof system 2, image id 0x9ae0f416ee43e9ea8908c555d424fe23e3592677ffc42a763476623d0eb5cf72); tools/ci/pinned-guests-check.sh now checks its manifest and allows only its pin script to build it as measured on 6 October
The shadow runner tools/shadow/shadow.py (both hosts on every shard of every exported segment, each proof re-verified by its own host, the row, the summary, the alert; `--inject soundness bytes), tools/shadow/tunnel.sh` (the read-only line to a node)
The active list igneum_exec::proving::ProvingConfig::active_systems (version 1 alone by default; IGNEUM_PROOF_ACTIVE_SYSTEMS=1,2 with IGNEUM_PROOF_VERIFIER_R0=<igneum-prove-r0-host> adds version 2 with its image id from the host's --mode id); check_record refuses a version off the list; the verifier thread and the consensus verdict dispatch by version (version 2 through its host); ProofRecord::version 2 = RISC Zero (kaspa_consensus_core::proving::VERSION_RISC0); the status carries activeSystems fork branch second-prover-node
The stop switch ProofPool::stop_on_disagreement (RPC igneum_proofDisagreement): the node's templates carry NO proof record until one third of the weight window's blue blocks signal a system (allowed_systems, trust_tally), then only that system's; igneum_proofTrustClear resets; igneum_getProofTrust and igneum_getProvingStatus.trust show it fork branch second-prover-node, unit-tested
The trust signal a coinbase section IGNT (`version_le16
The alert the node's PROOF DISAGREEMENT warn line and status; the runner's ALERT line, out/alert.json, out/alerts.jsonl, the RPC post, and the Discord incident hook when --discord names tools/community/discord-hooks.mjs (dry run unless --discord-live) runner and node

2.1 Why the stop switch is a pool rule and not a consensus rule

A record pays at the chain block that carries it (carried_payouts, spec 7.7). Whether a block carries a record is the producer's choice: template_sections builds the coinbase sections from the pool, and a block with no record section is as valid as one with eight. So "the pool stops paying" is implemented as "no stopped node carries a record", which every node can do without a rule change and which a block carrying a record never violates. The limit, stated: a producer that ignores the switch can still carry records and the chain still pays them, exactly as the pool pays today; the switch is the honest producers' rule, the alert is public, and the veto stays. Nothing in consensus moved. Checked against the reading of this task: the switch is a pool rule and an alert.

2.2 What IS a consensus-shaped change, held for the project lead's word

Since 0.3.16 the body rule asks the proof oracle about every carried proof and a proof that does not verify makes the carrying block invalid. A version 2 record carried on a network of version-1-only nodes is such a proof. So widening the active list on a live network (carrying RISC Zero records) is a change every node makes together, and the node refuses to start with version 2 on its list unless its operator names the RISC Zero host. The code ships with version 1 alone as the default and nothing on any live network changes. When the project lead says so, the activation goes into the override object as proving_second_system_activation_daa with a floor height, under the 95 percent signal rule, and crosses Devnet 2 first. Not done here, on purpose.

3. The guest statement, with the fin-proof lane

Agreed with the fin-proof lane on 7 October (09:3x UK): the shard layout is untouched (ShardOutput, 328 bytes), the aggregator's 340-byte BlockOutput prefix stays byte for byte, with an optional 164-byte finality extension when the aggregation takes a finality input (never until finality_in_proof_activation_daa is set). The second system commits the same 328 bytes today; it has no aggregator (a block's shards are one family, proving-methods.md route D), so the aggregated statement stays SP1's. The trait's aggregate(prev, shards) signature is unchanged; the fin-proof lane's aggregate_with(prev, shards, fin) stays an inherent method.

4. The gate

Filled in from rows.jsonl and summary.json on rz-4090 (RunPod RTX 4090 24 GB, the fleet's Ubuntu 24.04 CUDA 12.8 image, the one-shot rule, USD 0.74 an hour), segments from Devnet 2's dn2-1 (read-only, tunnelled).

Line Result
1,000 segments agree across both systems PENDING
One injected bad proof disagrees, pays nothing, raises the alert PENDING

Prover times and sizes: PENDING (section 5).

5. Numbers and their consequences

PENDING.

6. Operations

Step Command
The tunnel on the shadow box tools/shadow/tunnel.sh start /root/.ssh/fleet-internal 11340 root@<dn2-1> /root/sp-shadow/tunnel.pid
The run python3 tools/shadow/shadow.py --sp1-host <igneum-prove-host> --r0-host <igneum-prove-r0-host> --export <igneum-prove-export> --rpc http://127.0.0.1:26790 --out /root/sp-shadow/out --segments 1000 --sp1-env "HOME=/opt/igneum-floor/home SP1_PROVER=cuda SP1_GPU_ELEMENT_THRESHOLD=67108864 RUST_LOG=off" (nohup; stop with touch out/STOP or kill $(cat out/shadow.pid))
The injected case the same with --inject soundness --inject-at 1 --segments 1 --alert-rpc http://127.0.0.1:<node evm rpc> against a node of the fork build
A node that verifies both systems IGNEUM_PROOF_ACTIVE_SYSTEMS=1,2 IGNEUM_PROOF_VERIFIER=<igneum-prove-host> IGNEUM_PROOF_VERIFIER_R0=<igneum-prove-r0-host> IGNEUM_PROOF_TRUST=sp1 igneumd ...
The switch by hand igneum_proofDisagreement [{number, shard, reason, systems, source}], igneum_getProofTrust, igneum_proofTrustClear on the EVM RPC

7. What is still open

Item Why
In-process RISC Zero verification in the node version 2 verifies through the host subprocess (the Windows path SP1 already uses); linking risc0-zkvm verify-only into the node is a build-weight decision
A version 2 aggregator composition over the shard receipts; a block's shards stay one family until then
The activation on a live network section 2.2, the project lead's word
The shadow runner as a standing fleet role today a one-shot box; tools/fleet/box-prover.py is where a standing shadow would live