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 |