Igneum bench log
Append-only. Every number here was measured on the machine named, on the date given.
2026-10-03 proto-metal / igneum-bench, first run
@@ -512,6 +512,14 @@ th{font-family:var(--f-mono);font-size:12px;letter-spacing:.12em;text-transform:The adopted fee table (spec 05 section 5.11) reaches the devnet by fees_v1_activation_daa (docs/plans/fee-switch-devnet.md). Until today the prover's guest carried only the prototype table (B_p 30 M, S_p 7.5 M, intrinsic 200), so every shard statement after the switch would have differed from the node's plan. Now igneum-prove-core mirrors the node's fees.rs (both tables, FeeSchedule::at), the shard input carries the schedule and the block's DAA score, and the executor raises the carried base fees to the set's floors as the node does. The 328-byte public values are unchanged: the node's native veto (it recomputes every statement) is what pins the schedule a prover claims.
| Check | Command | Result |
|---|---|---|
| The node reads the switch | cargo test --release -p igneum-exec on fork 2b6d23ef (this Mac, target vendor/igneum-node/target-036, 15:32:27Z to 15:36:30Z) | 11 passed, 0 failed, among them the_fee_switch_meters_by_the_block_daa_score |
| The digest for H = 210,000 | 20 s scratch node, override {"difficulty_v2_activation_daa":33000,"proving_v0_activation_daa":84100,"fees_v1_activation_daa":210000} | ab8847da538dead1dc10e046dfaadab3c1c35928e3748810c4e050d4a886087a |
| One chain across the switch | private simnet on the 2b6d23ef binaries, override {"fees_v1_activation_daa": 200}, tools/prove-fixtures/gen.mjs before and after DAA 200, one igneum_exportSegments dump with every segment's daaScore | one chain, 358 segments, DAA 0 to 1,121: block 51 (DAA 187, prototype, 11 transactions, 7,494,392 pgas, one shard at S_p 7.5 M) before the switch; blocks 351 (DAA 1,105, 11 transactions, 36,934 pgas, two shards at S_p 30,000) and 355 (DAA 1,117, 14 transactions, 69,292 pgas, three shards) after it; igneum_getBudgets went from provingGasLimit 0x1c9c380 to 0x1d4c0 and the base fees to 100 gwei and 10,000 gwei at the switch. Found on the way: a burst signed with a 21 gwei cap just before the switch never executed after it (the floor is 100 gwei), and under v1 a modexp bomb's proving charge (pgas x 10,000 gwei) crosses the signed budget unless the gas limit covers it (design 4.1); gen.mjs now sizes both from the node's budgets |
| The port replays both sides | igneum-prove-export on that dump: every segment's state root equals the node's, prototype below 200 and v1 at and above it | replayed 358 segments from genesis; every state root equals the node's for each of the three cuts; fixtures fees-switch-prototype, fees-v1-shards2, fees-v1-shards3; igneum-prove-host --mode native on each: MATCHES, the three tamper cases REJECTED |
| The pinned guest on both sides | igneum-prove-host <fixture> --mode execute on this Mac (setup 37 s, pinned: setup matches the manifest) | fees-switch-prototype shard 0: 66,043,259 cycles, 44 cycles per EVM gas, 9 cycles per prototype pgas, 2.88 s; fees-v1-shards2 shard 0: 4,717,439 cycles (213 cycles per v1 pgas, 0.38 s), shard 1: 3,485,430 cycles (236 per pgas, 0.26 s); aggregator 1.4 M cycles over 2 shards; every tamper case REJECTED. Under v1 these transfer-and-modexp shards run at about a quarter of the unit (1,000 cycles per pgas): the table over-charges them, which is the safe side of design R1's calibration |
| The new pin | proving/igneum-prove/pin-guests.sh | shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a (2,832,504 bytes), aggregator 0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896, pinned 16:20:38Z; the 0.3.8 ids (0x0dfade07..., 0x135e67e7...) are what every machine runs until 0.3.9 |
Block rate for H: DAA 111,230 at 15:23Z, 112,227 at 15:40:13Z, 0.965 blocks/s; H = 210,000 is 24 h ahead of a publish before about 19:50Z on 5 October (the runbook moves it otherwise).
+5 October 2026 (night), the C4 fix: certificate-driven reorg
+Owner: the consensus engineer and cryptographer agent, worktrees igneum-wt-c4 (branch c4-fix) and vendor/igneum-node-c4 (fork branch c4-fix on release-0.3.6 a24ab01a). Harness tools/finality-attacks/c4.mjs on the fast-time 3-node network (100-ms proxied links), node built on the Apple M5 Max in vendor/igneum-node/target-c4 from the fork worktree (an APFS clone of target-036), suites on PC 2 through tools/build-job.mjs. The Mac carried two other builds and the M20 live sync throughout; every figure is a count, an index or a second from the harness clock.
The cause, in the code. processes/finality.rs: ingest_certificate verified a certificate only when its block was the node's own determination at that index (cp.hash == cert.checkpoint); any other block went to hold_pending, and nothing ever tried the pending certificate against the table at its own block. fork_choice_lock reads state.locks, which only evaluate filled, and evaluate only ever ran over the node's own determination. So a certified checkpoint off the node's chain never became a lock and never constrained the sink search, whatever spec 3.5 says. Second cause, found tonight on the harness: protocol/flows/src/v10/blockrelay/flow.rs skips a relayed block whose blue work is under the virtual's merge-depth root ("hence we are skipping it"), and the certified chain is lighter by construction, so the node on the heavier side never received the certified chain's blocks at all: in the first runs on the fixed consensus n0 held B's certificates by gossip for the whole heal window and B's blocks never arrived (n0's log shows only its own blocks "via submit block" after the reconnect).
The fix. Fork: ingest_off_chain (verifies against voters_at of the certificate's own block, Q3 and Q5 by quorum_at from that block's past, the lock chain by off_lock_chain, then LOCKED with a FinalityLock notification and a VirtualStateProcessingMessage::Resolve nudge so the sink moves without waiting for a block); retry_pending_off_chain on every virtual change; the lock-chain guard in evaluate (a determination off the chain through the node's nearest locks never locks and never aggregates); fork_choice_lock reports a lock beyond the depth-based finality point once; wants_unknown_certified_block and the relay-flow bypass of the merge-depth skip while a pending certificate names a block the node lacks (finality_wants_blocks through ConsensusApi and the session). Not gated on finality_v3_activation_daa: rule v2 took the same pending path.
Unit tests (PC 2, job build-20261005-180827, 18:09 UTC): kaspa-consensus 97 passed, 0 failed, 3 ignored; kaspa-consensus-core 101 passed. New: a_lighter_certified_chain_wins_and_a_heavier_uncertified_one_does_not_override_it (main chain 77 blocks locks to 13, a 7-block side chain's index-14 certificate is adopted, the sink moves to the side tip with no new block, ten more main-chain blocks do not move it back, a second certificate at 14 over the main block is CONFLICTING and the lock stands, the side chain then locks 15), the_certificate_driven_reorg_holds_under_rule_v2 (the same at finality_v3_activation_daa never), a_chain_that_misses_an_adopted_lock_never_locks_here (the evaluate guard and the off-lock conflict). reorg_past_an_unlocked_checkpoint_re_determines_it_and_verifies_the_pending_certificate rewritten for the new behaviour (pending while the block is unknown, adopted when it arrives). kaspa-p2p-flows lib tests do not compile on release-0.3.6 before or after this change (nine epoch_seed_headers errors in the pruning-proof message tests; the M20 job build-20261005-172340 hit the same nine an hour earlier). Six PC 2 jobs were lost tonight to two tooling faults, both fixed in the class: igneum-ota-sign embedded | head -1 under pipefail (SIGPIPE panic, four scripts, tools/ci/signer-pipe-check.sh) and the one shared build-inputs.zip in the downloads folder (a job published while another agent's pack landed pinned that agent's sources, three times; build-job.mjs now names every job's zip).
Harness, weight against work (B four keys and 70% of the weight, A two keys and 30%; at the cut A mines 0.6 and B 0.4 blocks/s; WINDOW = weight window, ban and min_daa at fast time). The 120-DAA window of the earlier runs turns every long split into F21's partition-longer-than-a-window shape once the p2p reconnect is added: n0 dials the proxy again on the connection manager's backoff, 84 to 114 s after the heal in every run tonight (the original sweep's 6 s was a short cut), so A's chain is 130 + 84 s = 128 DAA past the cut before any certificate can reach it, past its 120-DAA frozen table (v3) or its own two-thirds share of a sliding table (v2, 126 DAA at W 240 and s 0.3), and A locks alone first. With WINDOW=240 and WARM=320 the bound is 400 s (v3) or 210 s (v2) after the cut.
| Run | Node | Rule, W, split | B locks during the split | n0 reconnected | n0 adopted off-chain | Final chain | Conflicting | Disagreeing | Verdict |
|---|---|---|---|---|---|---|---|---|---|
| on 90 s (the sweep's framing) | c4 consensus fix, no sync hook | v3, 120, 90 s | 0 (36 blue blocks for B, a new index needs 50) | 6 s | 0 | A, all three (no certificate to follow) | 0 | 0 | not the C4 shape |
| v2 90 s | same | v2, 120, 90 s | 1 (index 8) | 6 s | 0 | apart | 5 / 5 / 5 | 2 | n0 locked 10 alone at 18:32:42, B's certificate for 8 reached it at 18:32:43: F21's bound (63 DAA of A's own chain) crossed before the heal |
| on 130 s | same | v3, 120, 130 s | 1 (index 9) | 84 s | 0 | apart | 9 / 3 / 3 | 2 | n0 locked 12 alone at DAA 359, one window after lock 8 at 239, 6 s before the reconnect |
| off 150 s (control) | same | no certificate, 150 s | 0 | 96 s | 0 | A (heavier), all three; B's nodes re-determined 2 indices | 0 | 0 | PASS, as in the sweep |
| on 130 s, W 240 | same | v3, 240, 130 s | 2 (10, 11) | 114 s | 0 (certificates 13 and 14 pending, blocks unknown) | apart | 0 | 0 | the sync gap: n0 never received a B block |
| v2 130 s, W 240 | same | v2, 240, 130 s | 2 (10, 11) | 114 s | 0 | apart, n0 locked 16 alone at 293 s | 0 / 1 / 1 | 0 | the sync gap again (n0 reconnected after v2's 210-s bound) |
| v2 130 s, W 240 | c4 fix with the sync hook | v2, 240, 130 s | 1 (index 12) | 84 s | 3 (12, 13, 14 within 2 s of the first B block; 11 re-determined) | B, all three, A's split tip abandoned | 0 | 0 | PASS |
| on 130 s, W 240 | same | v3, 240, 130 s | 0 (Poisson: 52 blue blocks, the index fell just short) | 84 s | n1 1, n2 2 (B's nodes adopted A's post-heal certificates and moved before IBD) | A, all three | 0 | 0 | the mirror case; not the C4 shape |
| on 140 s, W 240, addPeer at the heal | same | v3, 240, 140 s | 2 (11, 12, first at 12 s) | 3 s (the harness now dials through addPeer; the address goes as {ip, port}) | 1 (12 by certificate; 11 verified on the new chain) | B, all three, A's split tip abandoned | 0 | 0 | PASS |
Reading. With the consensus fix and the sync hook, a node on the heavier chain that receives a certificate for a chain it has never seen fetches that chain, verifies the certificate at its own block, locks it, moves its sink to the lighter certified chain and re-determines its own records onto it (the v2 W 240 row: 0 conflicts, 0 disagreements, every node on B's chain, which is the spec's F1 and the design's Fork choice items 1 to 4). The same holds under rule v3 with the frozen table on (the last row: B certified 11 and 12 during a 140-s split, n0 reconnected 3 s after the heal once the harness dialled through addPeer, adopted 12 by certificate and ended on B's chain with the other two, 0 conflicts, 0 disagreements). The fix does not and cannot cover a partition that outlasts the bound before the certificate arrives (rows 2, 3 and 6): there the node has already locked alone and 3.11.4 keeps that lock, the late certificate is CONFLICTING for the operator. On the live devnet (W 7,200 DAA, two hours) the bound is two hours after a side's last lock, so every partition under that heals by certificate. Raw: scratchpad c4-results-*.md, node logs c4-*-n0.log.
5 October 2026 (evening), FUD ledger sweep round 6
Owner: the consensus engineer and cryptographer agent, worktree igneum-wt-fud-a (branch fud-a), 15:45 to 16:40 UTC. The Mac was loaded throughout (two cargo builds, a txgen run and a fee-switch simnet by other agents; load average over 100), so every figure below is a count, an index, a byte or a number from another machine; the only millisecond figures are the browser verifier's, taken as ratios and labelled. Live reads through the Apple M5 Max node's wRPC (ws://127.0.0.1:28640) and the log intake (Neon HTTP SQL, lines split server-side), never a restart.
Rolled-out fixes, the live evidence (F23, F24, G12, X18, M30, M31, F25, M20, M26, M27, X21). The 0.3.5 cut at 06:33 UTC (master 2054ae3, fork 20139145) carried fud-consensus, m20-pruning and miner-reliability; every reachable app machine was on the node line by 09:32 UTC and the hand nodes and the seed restarted on it for the proving activation (docs/plans/release-0.3.6.md 8j, "live devnet: the first shards proven"). Node logs of the five app machines over the 10 hours to 15:45 UTC, one intake query (scratchpad fud-a/nodelines.mjs):
M1, M16, P14, F16: arithmetic and documents, no run. M1: the version 2 program space from the generator's draws (slot subset 48.4 bits, operation entropy 3.26 bits, about 770 bits of operations and registers per program, about 1,550 with rotation, bit and mask fields, capped by the 256-bit seed). M16: docs/analysis/m16-recompute-attacker-2026-10-05.md. P14: next_base_fee in igneum/exec/src/executor.rs:501 is the one controller; spec 05 section 5.1 now says so. F16: the two options priced from sim/results_v2.md H and M5 and the cloud F21 numbers, in the ledger entry.
C4, the overlay against GHOSTDAG, measured (tools/finality-attacks/c4.mjs, the live node line target-036 2b6d23ef, fast time, 3 nodes, 100-ms proxied links, ports 29800+; "weight against work": side B with four keys and 70% of the weight table, side A with two keys and 30%; at the cut the rates swap, A at 0.6 and B at 0.4 blocks/s for 150 s, so A builds the heavier chain while only B can certify; heal window 200 s). The first run went out with the override unapplied (the harness library reads it at import; fixed the same hour) and is kept as the rule v2 control:
| Run | Rule | New locks during the split A / B | First lock A / B (s) | Blue score A / B at the heal | Sinks after the heal | Final chain | Conflicting certificates | Disagreeing locked indices |
|---|---|---|---|---|---|---|---|---|
| control | v2 (the live devnet's rule), min_daa 120 | 4 / 3 | 133 / 9 | 335 / 296 | apart (n0 on its own) | none (a finality fork, F21) | 1 on n0 | 1 |
| off | no certificates (min_daa never) | 0 / 0 | none / none | 330 / 301 | one sink on all three | A's (the heavier) | 0 | 0; B's nodes re-determined 2 indices onto A's chain; n0 reconnected 36 s after the heal |
| on, split 150 s | v3 from checkpoint DAA 0 | 0 / 2 | none / 39 | 326 / 313 | apart | none | 3 on n0 | 2 (n0 reconnected 66 s after the heal, A's chain past the 120-DAA table by then) |
| on, split 90 s | v3 | 0 / 2 | none / 3 | 278 / 265 | apart | none | 3 on n0 | 2 (n0 reconnected 6 s after the heal, A's chain at about 58 DAA, inside the table) |
Reading (the NEW finding, ledger C4). With the module off GHOSTDAG alone converges on the heavier chain and the losing side's records re-determine (F24 works when the chain moves). With the module on the overlay holds during the split (A, with 30% of the frozen table, locks nothing; B locks 7 and 8) and then fails at the heal in the shipped node: B's certificates for blocks off n0's chain are "kept pending until the chain decides (no lock at this index)", n0's chain never decides because GHOSTDAG keeps its heavier tip and nothing turns the certificate into a fork-choice constraint, and once n0's last lock (index 7, DAA 209) is one window old (DAA 329) the frozen table stops applying on A's chain ("no frozen table (no lock on this chain inside the window)"), A's two keys are 100% of A's own window (B's post-cut blocks are red there) and n0 locks 10, 11, 12 alone; B's certificates for 10 and 11 then log CONFLICTING on n0 (n0 log, 16:27:04 to 16:29:54 UTC). A finality fork from a 96-s honest partition, no attacker, table intact at the heal; the 150-s run and the v2 control end the same way. The spec's fork choice ("GHOSTDAG among tips through all certified checkpoints", 3.5) is therefore implemented only for certificates over blocks already on the node's chain. Fix named in the ledger entry: verify an off-chain certificate against the table at its own block and let it constrain fork choice (a certificate-driven reorg), then re-determine. Raw: scratchpad fud-a/c4-results-*.md, node logs c4-on90-tmp/, c4-v2-control-tmp/.