diff --git a/docs/analysis/review-2026-10-08-b/dispatch.md b/docs/analysis/review-2026-10-08-b/dispatch.md new file mode 100644 index 000000000..7f51e9fc8 --- /dev/null +++ b/docs/analysis/review-2026-10-08-b/dispatch.md @@ -0,0 +1,28 @@ +# External review B (8 October 2026, 19:44 UK): dispatch + +The second external review read the six packs sent on 8 October (algorithm, v6 freeze tree, proving, Ember, node and DAG, mining workers and pool). Its verdict: real progress, not yet one demonstrated release; fix correctness and integration first, then make honest operation cheaper. Fourteen findings, forty-four regression requirements. The review, its structured findings and its reproduction pack sit beside this file. Nothing in it changes the 2.0 objective, the four properties or the five deliverables; every finding maps onto the Test and Acceptance Standard as a new suite, REV, whose cases are the reviewer's regressions verbatim. + +Rule for every lane: a finding closes only when its regression runs green on the pinned release and the record carries the evidence file. "Fixed by construction" is a claim until the regression says so. + +| Finding | Priority | Owner | Default action | Clock (UK) | Blocks | +|---|---|---|---|---|---| +| F01 proof-verdict cache omits the statement | P0 | proving lane, node lane | cache the verified facts (kind, program identity, public-values digest, format version) and compare the carried statement and the permitted identities on every lookup; invalid bytes and wrong-context refusals cached apart; no zero pinned id; known-failed test first, then a two-node cache-history test | fix sha 21:30; rides node 2.0.2 | value-bearing release | +| F02 proof rule fails open | P0 | proving lane, CI steward | test bypass compiled out of release builds (CI check); missing oracle or pinned keys after activation = not ready, never pass; pending bytes retry, invalid reject | with F01, 21:30 | value-bearing release | +| F03 one release manifest | P0 | CI steward, hash lane (ProgramClass::V6 on freeze), pool lane | one manifest pinning node, generator, dataset policy, acceptance, host ABI, miner app, pool, guests, keys, activation; pool and every worker built against it in CI (rule 24 extends to the pool); the EpochSeeds shadow_reps seam closed; the cross-backend same-work test (node, CPU reference, CUDA, Metal, OpenCL, pool) before, during and after a transition | manifest 23:00; cross-backend test with the freeze object | D1 | +| F04 recovery lock weaker than final | P0 | node lane, reference apps, site (explorer) | the 20:00 default (recovery) stands with the reviewer's condition: a recovery lock is never presented as a final lock; the checkpoint carries the state, the explorer, receipts, the light client and the oracle show it; the native 40/40/20 case runs on real nodes past the window; the pause-only alternative gets the backfill test | state flag 22:00; native case tomorrow | D5, public finality claim | +| F05 reg64 address mixer has an exact incremental form | P1 | hash lane, adversary lane, floor lane 3 | implement the prefix-tree form natively, prove equivalence across every source register with real instruction updates, then full-kernel equivalence and power on the rented cards; the chip model prices prefix caching, never an assumed 63-read cost per load; no new mixing work added in answer | native equivalence 22:30; chip rows 23:30 | P04 bracket, freeze | +| F06 acceptance and census do not cover the final execution | P1 | hash lane, research lane D | the freeze object's acceptance evaluates the actual 64-register schedule (reg64 liveness wired into canonical acceptance, not a side check); the full live-dataset census on unseen seeds and the complete pack runs on the frozen object; rw2 stays a rejected control; a failed rule never exhausts generation silently | acceptance wiring 23:00; live census overnight, a D1 gate before the object is called frozen | D1 | +| F07 one per-device scheduler | P1 | app lane, fleet lane | explicit modes: simultaneous (measured headroom only), time-share (dataset evicted, memory confirmed free, prover runs, dataset rebuilt), mining-only; one per-device coordinator across mining, proving, aggregation, benchmark and next-epoch preparation; admission counts transfer, startup, aggregation, rebuild and the payment deadline | modes in 2.0.2 (the 16 GB time-share copy already planned); mine-evict-prove-rebuild test 01:00 | consumer proving income claim | +| F08 proving pipeline hides expired work | P1 | proving lane, fleet lane, site (ops page) | outcome ledger for every eligible job: completed, active, expired, cancelled; deadline misses and useful proof throughput published, not queue depth; failure attributed by cause; the two-hour declared-load hold rerun on the pinned release | ledger 23:00; two-hour hold on node 2.0.2 | D4, consumer proving claim | +| F09 economic table and the failed specialist | P1 | research lane, floor lane 3, coordinator | every pass or fail cell generated from the declared inequality and its inputs (the USD 18 M against 40 to 80 M cell is wrong as displayed); condition (g) adds the hybrid operator who owns companion GPUs or buys proofs; the N2 SRAM die stays the open failed case until an independent physical-design read prices it | regenerated table in the 21:00 landing; hybrid row 22:00 | property 2 claim | +| F10 CUDA readback: port the OpenCL select pass | P2 | worker lane (new) | a separate selection pass with sentinels, exact comparison and overflow fallback, then asynchronous overlap; publish kernel throughput and end-to-end accepted work per wall joule as two numbers | equivalence 23:30; measured rows 01:00 | nothing (byte-preserving) | +| F11 Ember search and identity | P2 | Ember lane (new) | objective-aware bounded two-sided search, rebase on goal change; workload fingerprint (backend, dataset geometry, compiler, schedule, concurrent proving) invalidates certification while keeping hints; paired A/B/A, soak, rejected-work check; mining-only, proving-only, hybrid profiles under the device scheduler | search 23:00; identity 01:00 | nothing | +| F12 pool payout durability | P1 | pool lane (new) | durable idempotent intent and the exact signed transaction before broadcast; balance reserved atomically; states prepared, broadcast, mined, finalised, reorged, replaced; reconcile by intent after timeout or restart; crash tests around every durable step | 01:00 | pool launch | +| F13 pool admission bounds | P1 | pool lane (new) | frame size enforced while reading; bounded write queues; one membership per session (re-authorise replaces atomically); nonce dedup after validation with in-flight handling; slow-reader and invalid-share flood tests | 01:00 | pool launch | +| F14 lab fleet control is not the public trust model | P1 | app lane, relay lane | two builds from 2.0.2: the public miner (no remote execution, auto-update a choice at install and honoured, an urgent manifest may pause and notify, never install over "off") and the lab build for our own fleet (remote jobs on, separate signing root); a compromised update key cannot reach the wallet or activate a consensus change | split in 2.0.2, 23:00 | public client, retained operator control | + +Decisions for the founder (defaults apply at the clock unless he names otherwise): + +1. F04: keep recovery (labelled, never called final) rather than pause-only. Default: keep recovery with the label. Clock 22:00. +2. F14: the public miner ships without remote execution; our fleet runs the lab build. Default: yes. Clock 20:30. +3. F06: if the live-dataset census on the frozen object is not green by 00:30, the object is "frozen pending D1 census" and the register says so; the freeze is not called complete. Default: yes. diff --git a/docs/analysis/review-2026-10-08-b/findings.json b/docs/analysis/review-2026-10-08-b/findings.json new file mode 100644 index 000000000..884902589 --- /dev/null +++ b/docs/analysis/review-2026-10-08-b/findings.json @@ -0,0 +1,671 @@ +{ + "date": "2026-10-08", + "status": "SOURCE REVIEW; NOT A COMPLETED ACCEPTANCE AUDIT", + "findings": [ + { + "id": "F01", + "title": "Proof-verdict cache omits the statement being verified", + "priority": "P0 - public/value-bearing release blocker", + "evidence_status": "SOURCE + ISOLATED CONTROL-FLOW REPRODUCTION", + "body": "\nThe native cold verifier checks both the pinned program and the hash of the proof's public values against the carried statement. That is good. However, `ProofPool::verdict_for` returns a cached success keyed only by `proof_hash`, checking only the accepted program ID. It does not compare the carried statement on that path. It also returns cached errors without distinguishing intrinsically invalid proof bytes from an otherwise valid proof queried in the wrong context.\n\nThe isolated model reproduces: a wrong statement is refused with a cold cache; the same wrong statement is accepted after that proof was cached against its correct statement. Querying the wrong statement first can poison the later correct lookup. The zero-ID cache wildcard can also bypass a nonempty accepted-ID list if such an entry has been populated. Host configuration controls that second case's reachability.\n\nThis is NOT an SP1 forgery. It is a failure to bind a cached verification result to its use. `check_record` still checks native execution and signatures, so this finding does not show arbitrary execution roots becoming valid. But proof payment and block-validity decisions consume the cache. Warm/cold or order-dependent decisions are unacceptable there. A malicious block producer need not use the honest producer's template-selection code. Full native reproduction is required to establish exact network impact.\n\nFix: cache immutable verified facts (proof kind, actual verifier/program identity, public-values digest, proof format/security version) and compare the required statement and current permitted IDs on EVERY lookup. Alternatively, key by all relevant context. Do not use zero as an accepted pinned ID. Distinguish context-specific refusal from invalid bytes. The relay-time cache population must store the identity actually verified, not merely a configured host label. Bound cache size and in-flight verification.\n", + "source_ranges": [ + { + "path": "node/igneum/exec/src/proving.rs", + "start": 1046, + "end": 1102 + }, + { + "path": "node/igneum/exec/src/proving.rs", + "start": 1358, + "end": 1379 + }, + { + "path": "node/igneum/exec/src/nativeverify.rs", + "start": 151, + "end": 177 + }, + { + "path": "node/igneum/exec/src/proving.rs", + "start": 471, + "end": 489 + } + ], + "required_regressions": [ + "Valid proof A, warm cache, carried statement B: refuse exactly as a cold node does.", + "Run valid/invalid contexts in both orders, concurrently and across cache eviction/restart.", + "Change payout, network, kind, program ID and activation context; no accepted misbinding.", + "A native two-node test must agree on block validity and payouts despite different cache histories." + ] + }, + { + "id": "F02", + "title": "Proof-rule infrastructure can fail open", + "priority": "P0 - release configuration blocker", + "evidence_status": "STATIC SOURCE", + "body": "\n`check_carried_proofs` has a production-compiled environment bypass (`IGNEUM_TEST_SKIP_PROOF_RULE=1`) and returns success when the oracle is absent. The comments explicitly describe these as harness/unit-test accommodations. This is not evidence an unauthenticated peer can set the environment, nor evidence the normal daemon omits its oracle. It is a configuration failure mode that contradicts mandatory enforcement if accidentally activated.\n\nAfter the rule activates, missing verifier infrastructure should stop startup or validation safely, not silently disable the rule. Restrict deliberately unsafe test switches to test-only builds. Distinguish pending proof bytes (retry) from invalid proof (reject) and unavailable trusted verifier (not accepted).\n", + "source_ranges": [ + { + "path": "node/consensus/src/pipeline/body_processor/body_validation_in_context.rs", + "start": 28, + "end": 79 + } + ], + "required_regressions": [ + "Release build cannot activate test bypass.", + "Missing oracle or pinned keys prevents service readiness after enforcement activation.", + "Missing proof bytes retry without incorrectly marking a valid block permanently invalid." + ] + }, + { + "id": "F03", + "title": "The bundle contains a v6 candidate, not a demonstrated integrated v6 release", + "priority": "P0 - freeze/integration blocker", + "evidence_status": "STATIC SOURCE + ARCHIVE PROVENANCE", + "body": "\nThe v6 freeze contains real candidate flags, a 64-register schedule, address mixing, fold/reweight experiments and non-power-of-two dataset geometry. It is materially newer than the earlier V2/V3/V4-only review. Nevertheless the canonical `ProgramClass` enum in this snapshot ends at V5. The freeze README itself says the D1 object cut and spec re-cut are subsequent work. The node bundle is from a different release branch. Generated packs and relevant vendor/build context are omitted.\n\nThere is a concrete seam: the node's `EpochSeeds` struct requires `shadow_reps`, but the pool constructs that type without the field or a struct-update expression. Those exact files cannot be compiled together as written. This was identified statically, not by running Cargo. It does not establish that another deployed pool or vendor revision has the mismatch.\n\nFix: produce one release manifest that pins node, generator, dataset policy, acceptance, host ABI, miner app, pool, proof guests/keys and activation. Build the pool and all supported workers against it in CI. Include executable packs and their authenticated identity. Never combine measurements from different candidates into a single v6 claim.\n", + "source_ranges": [ + { + "path": "v6/igneum-v6-freeze-tree/README-FREEZE.txt", + "start": 1, + "end": 1 + }, + { + "path": "pow/src/generator.rs", + "start": 249, + "end": 277 + }, + { + "path": "pow/src/generator.rs", + "start": 939, + "end": 954 + }, + { + "path": "node/consensus/pow/src/igneum.rs", + "start": 72, + "end": 95 + }, + { + "path": "mining/pool/src/node.rs", + "start": 47, + "end": 71 + } + ], + "required_regressions": [ + "Clean build from one manifest, including the pool and workers, with no unpublished vendor tree.", + "Same job context produces identical accepted work in node, CPU reference, CUDA, Metal, OpenCL and pool.", + "Cross every scheduled transition with old/new client behavior documented and identical rule identities." + ] + }, + { + "id": "F04", + "title": "Finality v4 recovery deliberately has a weaker safety boundary", + "priority": "P0 - finality guarantee decision", + "evidence_status": "SOURCE + SOURCE-REPORTED SIMULATION + PURE PREDICATE REPRODUCTION", + "body": "\nThe older unconditional table-expiry problem has been addressed: the v4 anchored table does not simply disappear. However, after a full window without a lock, the recovery branch accepts strictly more than half of the anchored weight. The supplied guarantee document openly reports conflicting recovery locks in a prolonged partition when an equivocator both mines and signs on both sides. The 40/40/20 case is explicitly included.\n\nThe isolated predicate confirms two sides each holding 60 of the original 100 pass the recovery threshold after the window, although neither passes the two-thirds threshold before it. This alone is not a full protocol exploit: sliding tables, ancestry, dust eligibility and signed certificates also matter. The team's simulation record supplies additional evidence. The same document says its real-node line at the snapshot is the known-failed v3 case and the v4 line still awaits the node change.\n\nDo not call normal finality and recovery finality the same irreversible guarantee. Prefer a reviewed authority-transition/recovery rule retaining the claimed safety assumptions; otherwise restrict and label the recovery state and dependent wallet/bridge actions explicitly. A timeout does not prove that a missing authority is permanently gone.\n\nThe pure pause-only predicate refuses every post-window checkpoint, even with 100% anchored signatures. This is not proof of permanent network liveness failure: historical-checkpoint backfill may re-anchor first. The reported immediate-heal simulations must be reproduced against the actual implementation, including that path.\n", + "source_ranges": [ + { + "path": "node/consensus/src/processes/finality.rs", + "start": 1111, + "end": 1128 + }, + { + "path": "node/consensus/src/processes/finality.rs", + "start": 2610, + "end": 2642 + }, + { + "path": "dag/igneum-node-dag/spec/docs/spec/finality-guarantees.md", + "start": 111, + "end": 136 + }, + { + "path": "dag/igneum-node-dag/spec/docs/spec/finality-guarantees.md", + "start": 184, + "end": 197 + } + ], + "required_regressions": [ + "Native multi-node 40/40/20 partition, equivocation and dust-valid mining on both sides past the window.", + "Proof that the chosen recovery guarantee matches the public finality claim; two valid contradictory certificates are a hard failure for strong finality.", + "Pause-only resume with historical backfill, missing historical data and all old keys returning.", + "Wallet, receipt and oracle consumers distinguish any weaker recovery state." + ] + }, + { + "id": "F05", + "title": "reg64 address coupling has an exact incremental alternative", + "priority": "P1 - adversarial hardware evaluation", + "evidence_status": "EXACT ALGEBRA + 33,024 RANDOMIZED/EDGE COMPARISONS", + "body": "\nThe full-chain window computes a rotate-XOR fold over the 63 registers other than the source, then XORs the source. This connects every register syntactically, but it does not force a physical implementation to reread and fold all 63 on each load.\n\nDefine a[k] = ROL32(r[k], 63-k), S = XOR of all a[k], and P_s = XOR of a[k] for k < s. Then the exact source expression is:\n\n address_source(s) = r[s] XOR ROR32(P_s, 1) XOR S XOR P_s XOR a[s]\n\nA prefix-XOR tree supports point updates and prefix queries in logarithmic time. The included model checked 33,024 comparisons, including 4,096 state updates, without a mismatch. The algebra follows by distributing the rotation across XOR: values before the excluded source have one fewer subsequent rotation; values after it retain their original exponent.\n\nThis is NOT evidence that the full hash is cheap, that the necessary 64-register state is compressible to one word, or that the extra index state is free. Cached prefix state, port bandwidth and update costs must all be priced. The GPU compiler may already remove some redundant work. It is a concrete alternative the adversarial designer must be allowed, and potentially a byte-preserving implementation experiment for both sides.\n\nDo not respond by adding unmeasured nonlinear work. First implement the cheapest alternatives, remeasure GPU cost, then compare complete hardware designs. A one-register perturbation test cannot establish a minimum circuit or storage cost.\n", + "source_ranges": [ + { + "path": "pow/src/verify.rs", + "start": 674, + "end": 692 + }, + { + "path": "pow/src/generator.rs", + "start": 258, + "end": 277 + } + ], + "required_regressions": [ + "Native reference-vs-incremental expression equivalence across all source registers and real instruction updates.", + "Full-kernel output equivalence, registers, spills, wall power and accepted throughput across target GPUs.", + "Adversary physical design includes prefix caching/recomputation cost; no assumed full 63-read cost on every load." + ] + }, + { + "id": "F06", + "title": "The v6 acceptance and census gates are not complete for the final execution", + "priority": "P1 - freeze blocker", + "evidence_status": "STATIC SOURCE + SOURCE-REPORTED RESULTS", + "body": "\nThe new code explicitly executes the V4 shadow in its acceptance interpreter, includes repeated-source and index-concentration checks, and contains a regression test for shadow agreement. That improves on the older review. The v6 comments nevertheless say reg64's draw/acceptance are the underlying class's, while the liveness check is a separate research check not wired into canonical acceptance. Its sampling checks influence, not a formal unavoidable-state lower bound.\n\nThe census is similarly candid: rw2 fails its F8 line; fold plus rw1 is stronger on the reported controls; reg64/all results are based on the full tracing path with closed-form data, and the live-dataset point remains owed. These source results must not be promoted into an unconditional full-v6 pass.\n\nFreeze one agreed acceptance rule for the actual scheduled 64-register execution, and specify safe deterministic fallback behavior when a candidate fails. Re-run known-bad seeds, unseen seeds, real datasets, bound headers/nonces, the combined intended width/geometry and all family transitions. Retain rw2 as a rejected control unless new evidence changes the decision.\n", + "source_ranges": [ + { + "path": "pow/src/accept.rs", + "start": 114, + "end": 119 + }, + { + "path": "pow/src/accept.rs", + "start": 625, + "end": 637 + }, + { + "path": "pow/src/accept.rs", + "start": 867, + "end": 871 + }, + { + "path": "pow/src/generator.rs", + "start": 258, + "end": 264 + }, + { + "path": "v6/igneum-v6-freeze-tree/docs/analysis/class-v6/census-packs.md", + "start": 8, + "end": 22 + }, + { + "path": "v6/igneum-v6-freeze-tree/docs/analysis/class-v6/census-packs.md", + "start": 40, + "end": 49 + } + ], + "required_regressions": [ + "Acceptance/reference/emitter evaluate the same activated schedule.", + "Full live-dataset census on unseen seeds and the complete v6 pack.", + "A failed experimental rule does not silently exhaust generation or bypass the intended resource requirement." + ] + }, + { + "id": "F07", + "title": "VRAM admission and proving deadlines need one operator-level scheduler", + "priority": "P1 - miner economics and reliability", + "evidence_status": "SOURCE-REPORTED HARDWARE RESULTS + STATIC INTEGRATION REVIEW", + "body": "\nThe coexistence records report successful standalone compressed proofs: 13.2 seconds on a 3060 12 GB and 8.2 seconds on a 4060 8 GB after correcting the packaged prover server. They also report that both fail when run beside the 5.5 GiB dataset miner, with device allocation failures while mining continues. These are team measurements, not measurements made for this review. The registered reg64 rows use a different kit configuration and must not be combined with ds55 rows as one benchmark.\n\nThe right product path is explicit modes: simultaneous execution only on tested configurations with headroom; time-sharing on smaller cards with actual dataset eviction/release and confirmed memory availability; mining-only where a complete paid proof job cannot fit. Merely pausing kernel dispatch does not establish that GPU allocations were released.\n\nUse per-device identity and a memory reservation/lease state machine across miner, prover, aggregation, benchmark and next-epoch preparation. Include time to evict/rebuild datasets, WSL process startup, aggregation and payment deadlines in job admission. Do not market an isolated successful shard as proof that the card can finish the economically relevant segment.\n", + "source_ranges": [ + { + "path": "v6/igneum-v6-freeze-tree/docs/analysis/class-v6/coexist-rows.md", + "start": 17, + "end": 37 + }, + { + "path": "v6/igneum-v6-freeze-tree/docs/analysis/class-v6/coexist-rows.md", + "start": 60, + "end": 86 + } + ], + "required_regressions": [ + "Same final v6 configuration: mine -> evict -> prove -> aggregate -> submit -> rebuild -> resume; no leaked reservations.", + "OOM, process crash and stale work recover without losing wallet state or silently consuming power.", + "16 GB+ simultaneous mode only after measured peak plus next-epoch headroom; smaller-card modes labelled separately." + ] + }, + { + "id": "F08", + "title": "The proving pipeline reports waste and deadline censoring, not sustained capacity", + "priority": "P1 - proving product gate", + "evidence_status": "SOURCE-REPORTED OPERATIONAL DATA; NOT INDEPENDENTLY REPLAYED", + "body": "\nThe supplied pipeline report says the requested two-hour declared-load window does not exist in its record. It describes a class-v5 transition stall/partition and 93 paid segments in the paid interval, not a successful end-to-end hold at 150 transactions per second. Its 3060 tier completed zero paid segments over 313 claims. This does not prove a 3060 can never prove profitably: the same report says its completed submissions occurred after the stall, and the standalone fixture succeeds. It does show the claimed consumer-paid-work experience is not established by this run.\n\nA particularly important measurement issue: the worklist stays around 600 entries because entries expire at a deadline whether proved or not. Therefore bounded queue length does not establish sufficient proving capacity. The report itself discloses this mechanism.\n\nAdd lifecycle accounting: eligible work = completed + active + expired + explicitly cancelled, with definitions preventing double-counting. Publish deadline misses and useful accepted proof throughput, not just queue depth. Attribute failure to protocol partition, packaging, proving, assignment race, aggregation or submission. The source reports 70 wasted card-hours versus roughly 4 paid, dominated by the chain incident, so it would be wrong to call that all a prover-speed failure.\n\nPrioritise feasible job sizing and deadlines, resumable verified shards/checkpoints, early cancellation of obsolete claims, and bounded assignment protection. Protection must prevent slow or malicious claimants from monopolising work; do not simply promise no competing completion can ever occur. Run the two-hour workload test on the pinned release, then a longer independent soak.\n", + "source_ranges": [ + { + "path": "proving/igneum-proving/docs/analysis/proving-pipeline-2026-10-08.md", + "start": 7, + "end": 16 + }, + { + "path": "proving/igneum-proving/docs/analysis/proving-pipeline-2026-10-08.md", + "start": 35, + "end": 84 + }, + { + "path": "proving/igneum-proving/docs/analysis/proving-pipeline-2026-10-08.md", + "start": 86, + "end": 121 + } + ], + "required_regressions": [ + "Report every eligible job outcome, including expired work; a bounded list cannot hide losses.", + "Consumer tiers complete and receive payment for declared jobs before claims about income.", + "Sustained real workload across class transitions, with restart/retry and no publisher intervention." + ] + }, + { + "id": "F09", + "title": "The economic model explicitly retains a failed specialist case", + "priority": "P1 - founding claim not yet earned", + "evidence_status": "SOURCE-REPORTED MODEL + DISPLAYED-ARITHMETIC CHECK", + "body": "\nThe coexistence model states that the desired competitiveness statement does not hold for its modeled N2 SRAM die after development is sunk, and that the remaining deterrent is the investment decision. That is not proof the proposed die is manufacturable at the stated cost or throughput. It is also not evidence that the fundamental gate has passed.\n\nThere are at least two items to repair before using the model as certification. Its condition (c) requires fleet cost to exceed a year's mining revenue but labels USD 18M against USD 40-80M a pass; those displayed numbers do not satisfy that inequality. The table or criterion may be mistaken. Condition (g) also treats proving income as a business the specialised supplier cannot enter because its hash engine cannot prove. A company can own companion GPUs or buy proofs; the single-device limitation does not exclude the operator.\n\nReproduce the model from raw inputs, price the SRAM/recomputation design at physical board boundaries, include uncertainty and independent hardware critique, and test a hybrid operator. Treat unknown feasibility as unknown, not either a proven attack or an automatic pass. No retirement credit without a demonstrated adaptation penalty.\n", + "source_ranges": [ + { + "path": "v6/igneum-v6-freeze-tree/docs/analysis/class-v6/coexistence-model.md", + "start": 299, + "end": 321 + } + ], + "required_regressions": [ + "Generate all pass/fail cells directly from the declared inequalities and inputs.", + "Independent feasibility and cost evaluation of the strongest modeled SRAM/hybrid opponent.", + "GPU owners and entrants remain viable under the approved scenario envelope without assuming chip absence or death." + ] + }, + { + "id": "F10", + "title": "CUDA production readback has an existing OpenCL optimization to borrow", + "priority": "P2 - byte-preserving performance experiment", + "evidence_status": "STATIC SOURCE; SPEEDUP NOT MEASURED", + "body": "\nThe CUDA serving loop synchronises and copies one 64-bit result per nonce to the CPU, then scans it while holding its GPU mutex. For 2^24 nonces that is 128 MiB of result data per batch. Its benchmark times the kernel/synchronisation but reads the full result only for warm-up, so the benchmark does not include the same per-batch production cost.\n\nOpenCL already contains a select kernel that returns matching nonce/hash pairs plus sentinel words, with a counter and full-read fallback when more than 256 hits occur. This is not missing everywhere: it is a cross-backend parity opportunity. Port the proven shape to CUDA first as a separate selection pass, retaining correctness sentinels and overflow handling. Then test fusion/asynchronous overlap if justified. Metal still scans shared output on the CPU; unified memory means it is not the same PCIe-copy problem, so profile it separately.\n\nBenchmark accepted shares per wall-joule in serving mode, including setup, stale cancellation, readback, host power and pool submission. No percentage gain is claimed here.\n", + "source_ranges": [ + { + "path": "mining/proto-cuda/nvrtc/worker.cpp", + "start": 1254, + "end": 1295 + }, + { + "path": "mining/proto-cuda/nvrtc/worker.cpp", + "start": 1318, + "end": 1341 + }, + { + "path": "mining/proto-opencl/host.c", + "start": 1652, + "end": 1705 + }, + { + "path": "mining/proto-opencl/host.c", + "start": 1882, + "end": 1899 + }, + { + "path": "mining/proto-metal/main.swift", + "start": 3344, + "end": 3370 + } + ], + "required_regressions": [ + "Compare exact found nonce/hash sets with full-read mode, including all-hit overflow and zero-hit cases.", + "Test stale jobs, high-32 rollover, tail batches and asynchronous buffer reuse.", + "Compare production throughput and wall energy, not only the isolated kernel timer." + ] + }, + { + "id": "F11", + "title": "Ember needs workload-aware identity and objective-aware search", + "priority": "P2 - product performance", + "evidence_status": "STATIC SOURCE + REACHABILITY EXAMPLE", + "body": "\nEmber has useful thermal/fault checks, power and clock controls, calibration rows and fleet priors. Its shipped tier tests pass in this review. Those tests do not measure actual safe tuning or globally optimal settings.\n\nThe five-step hill climb proposes memory-up, core-down or both, even for MaxRate (which changes the score to MH/s). Starting from a low-clock efficiency prior, this search cannot propose a higher core clock or change the fixed power cap to escape that starting regime. A full sweep can choose a new start, so the finding is a limitation of this climb, not evidence every MaxRate setting is wrong.\n\nThe prior key uses card model, driver major and class. The workload class helper is based on load/wide-load counts. This is insufficient as a validated measurement identity for changes in memory geometry, compiler, reg64 schedule or concurrent proving. A prior may remain a useful starting hint; it should not be treated as a current certified optimum.\n\nUse a two-sided, bounded search appropriate to the chosen objective; rebase when switching goals; fingerprint the actual workload/backend and retain per-device calibration separately from fleet hints. Use paired A/B/A samples, a stability soak and rejected-work checks. Support separate mining-only, proving-only and hybrid profiles, coordinated by the device scheduler. Optimise accepted work or transparent net-return estimates, not raw displayed MH/s alone.\n", + "source_ranges": [ + { + "path": "mining/app/igneum-app/src/ember.rs", + "start": 211, + "end": 280 + }, + { + "path": "mining/app/igneum-app/src/ember.rs", + "start": 493, + "end": 520 + }, + { + "path": "mining/app/igneum-app/src/ember.rs", + "start": 606, + "end": 619 + } + ], + "required_regressions": [ + "Goal switch from an efficiency prior can explore higher core/power when policy allows.", + "Driver, workload, dataset, compiler and device changes invalidate certification while retaining optional hints.", + "Thermal/error/late-share events revert safely, including process or machine crash." + ] + }, + { + "id": "F12", + "title": "Pool payouts have a broadcast-before-durable-intent window", + "priority": "P1 - payment integrity", + "evidence_status": "STATIC SOURCE + ISOLATED CRASH SEQUENCE", + "body": "\nThe payout loop broadcasts first, then updates in-memory balances and records; disk snapshots are a separate periodic loop. If a transaction succeeds externally and the process dies before the debit is durable, restart can load the old payable balance and send again using a later nonce. A lost RPC response creates a related uncertain-outcome problem. The isolated model shows 100 units due becoming 200 externally paid under those assumptions; no actual transaction was sent.\n\nReceipt checking does exist, and failed receipts re-credit balances. Do not describe this as a pool with no receipt logic. However, a receipt is marked confirmed immediately and the loop subsequently visits only sent records; the supplied path does not establish finality-aware reorg handling.\n\nPersist an idempotent payment intent and exact signed transaction/hash BEFORE broadcast, reserve the balance atomically, reconcile the same intent after timeout/restart, and finalise against the chain's declared finality. Use explicit prepared/broadcast/mined/finalised/reorged/replaced states. Test crashes around every durable step.\n", + "source_ranges": [ + { + "path": "mining/pool/src/payout.rs", + "start": 228, + "end": 261 + }, + { + "path": "mining/pool/src/payout.rs", + "start": 265, + "end": 296 + }, + { + "path": "mining/pool/src/main.rs", + "start": 138, + "end": 149 + } + ], + "required_regressions": [ + "Crash before/after broadcast, response timeout, restart before snapshot: no duplicate payment or silent debt loss.", + "Same signed transaction retried, fee replacement reconciled by intent, not a new payment.", + "Receipt reorg and finality pause leave correct pending obligations." + ] + }, + { + "id": "F13", + "title": "Pool admission and membership need bounded resources", + "priority": "P1 - service resilience", + "evidence_status": "STATIC SOURCE + ISOLATED MEMBERSHIP MODEL", + "body": "\nThe server checks MAX_LINE after reading a full line, uses an unbounded outgoing channel, and inserts a nonce into a job's seen set before validation. Repeated Authorize requests create fresh members without removing or rejecting the prior one, while disconnect removes only the latest member. The small state-machine reproduction leaves two members after three authorizations on one connection and disconnect.\n\nThese are resource-control issues, not demonstrated remote exploits against a running pool. The verifier semaphore is a useful existing control but does not bound every queue or allocation.\n\nEnforce frame size while reading, bounded write queues, connection/request budgets, a single authenticated membership per session, cheap target/context prefilters, and bounded deduplication with in-flight handling. Test slow readers and malformed/invalid share floods without expensive network attacks. Member vote keys are already committed into the template; preserve that improvement.\n", + "source_ranges": [ + { + "path": "mining/pool/src/server.rs", + "start": 62, + "end": 99 + }, + { + "path": "mining/pool/src/server.rs", + "start": 115, + "end": 175 + }, + { + "path": "mining/pool/src/server.rs", + "start": 234, + "end": 255 + }, + { + "path": "mining/pool/src/server.rs", + "start": 281, + "end": 299 + }, + { + "path": "mining/pool/src/node.rs", + "start": 41, + "end": 48 + } + ], + "required_regressions": [ + "Repeated authorization rejected or atomically replaces and cleans prior state.", + "Oversized unterminated frame rejected within fixed memory/time budget.", + "Slow clients and invalid share floods cannot grow unbounded member, nonce or outgoing state." + ] + }, + { + "id": "F14", + "title": "Developer fleet control must not silently become the public client trust model", + "priority": "P1 - public client/independence gate", + "evidence_status": "STATIC SOURCE + BOOLEAN GUARD REPRODUCTION", + "body": "\nThe supplied settings explicitly call remote jobs default-on for the devnet build. Jobs are signed and have a visible disable switch; disabling aborts work. This is not a hidden unauthenticated backdoor. It is still powerful publisher control: run-script, fetch, collect, restart and update-now jobs share the OTA signing key, and some jobs run elevated or as WSL root.\n\nThe updater's auto-off guard is bypassed for an urgent release (unsupported version or nearby fork). This is intentional in the supplied policy, not a signature bypass. The remaining staging/safety checks still apply. An operator who disabled automatic updates should not unknowingly grant an urgency label permission to install arbitrary future software.\n\nSeparate a controlled lab/developer build from the public miner; remove arbitrary remote execution from the public default or use narrow explicit per-capability consent and a separate trust root. Keep user update acceptance distinct from consensus activation. Emergency safety notifications may pause unsupported operations; they should not silently override the user's installation choice. Review any manifest-delivered consensus overrides under the same rule.\n", + "source_ranges": [ + { + "path": "mining/app/igneum-app/src/config.rs", + "start": 75, + "end": 81 + }, + { + "path": "mining/app/igneum-app/src/config.rs", + "start": 135, + "end": 151 + }, + { + "path": "mining/app/igneum-app/src/jobrun.rs", + "start": 1, + "end": 19 + }, + { + "path": "mining/app/igneum-app/src/ota.rs", + "start": 540, + "end": 555 + }, + { + "path": "mining/app/igneum-app/src/ota.rs", + "start": 589, + "end": 595 + } + ], + "required_regressions": [ + "Public build has no default arbitrary remote execution and a documented least-privilege boundary.", + "Automatic updates off remains off for urgent manifests until explicit action.", + "A compromised fleet/update signing key cannot silently acquire wallet access or activate a consensus change." + ] + } + ], + "probe_results": { + "review": "Igneum updated-stack source review", + "date": "2026-10-08", + "native_rust_executed": false, + "gpu_executed": false, + "sp1_executed": false, + "probe_count": 13, + "probes": [ + { + "probe": "cache_wrong_statement_after_success", + "classification": "ISOLATED_CONTROL_FLOW_REPRODUCTION", + "cold_wrong_context": "Invalid", + "warm_wrong_context": "Verified", + "limitation": "No SP1 proof constructed; models the supplied early-return cache logic." + }, + { + "probe": "cache_negative_context_poisoning", + "classification": "ISOLATED_CONTROL_FLOW_REPRODUCTION", + "valid_context_after_bad_context": "Invalid", + "fresh_valid_context": "Verified" + }, + { + "probe": "cache_zero_id_accepts_pinned_epoch", + "classification": "ISOLATED_CONTROL_FLOW_REPRODUCTION", + "result": "Verified", + "accepted_ids": [ + "vk-B" + ], + "cached_id": "ZERO", + "limitation": "Reachability depends on host configuration and how the cache entry was populated." + }, + { + "probe": "reg64_rotate_xor_prefix_equivalence", + "classification": "EXACT_ALGEBRA_WITH_RANDOMIZED_CHECKS", + "comparisons": 33024, + "point_updates": 4096, + "mismatches": 0, + "equation": "a[k]=ROL(r[k],63-k); P=XOR(a[k],k512*1024*1024: + raise SystemExit('Unexpected archive size') + for i in z.infolist(): + pp=PurePosixPath(i.filename) + if pp.is_absolute() or '..' in pp.parts or '\\' in i.filename or any(':' in x for x in pp.parts): + raise SystemExit('Unsafe archive path: '+i.filename) + if stat.S_ISLNK(i.external_attr>>16): + raise SystemExit('Symlink in archive: '+i.filename) + z.extract(i,dest) +for alias,src in { + 'pow':'v6/igneum-v6-freeze-tree/igneum-pow', + 'node':'dag/igneum-node-dag/node', + 'mining':'workers/igneum-mining-workers', + 'prove':'proving/igneum-proving/proving/igneum-prove', +}.items(): + shutil.copytree(a.out/src,a.out/alias) +print('Prepared '+str(a.out.resolve())) diff --git a/docs/analysis/review-2026-10-08-b/reproductions/recorded-results/ember-tier-tests.log b/docs/analysis/review-2026-10-08-b/reproductions/recorded-results/ember-tier-tests.log new file mode 100644 index 000000000..1d71d6879 --- /dev/null +++ b/docs/analysis/review-2026-10-08-b/reproductions/recorded-results/ember-tier-tests.log @@ -0,0 +1,70 @@ +TAP version 13 +# Subtest: the shipped table validates with no faults +ok 1 - the shipped table validates with no faults + --- + duration_ms: 3.627802 + type: 'test' + ... +# Subtest: the measured rows are the record's (the 5090 at its 1,300 MHz knee, the 5080 at 1,100, the 4070 at its tune, the 9070 XT grid, the M5 Max meter) +ok 2 - the measured rows are the record's (the 5090 at its 1,300 MHz knee, the 5080 at 1,100, the 4070 at its tune, the 9070 XT grid, the M5 Max meter) + --- + duration_ms: 0.573775 + type: 'test' + ... +# Subtest: every entry carries the three tiers where the card has a lever, and only max where it has none +ok 3 - every entry carries the three tiers where the card has a lever, and only max where it has none + --- + duration_ms: 1.437987 + type: 'test' + ... +# Subtest: the match rule takes the longer name: a 5070 Ti is not a 5070, a 4060 Ti is not a 4060, a 9060 XT is not a 9070 XT +ok 4 - the match rule takes the longer name: a 5070 Ti is not a 5070, a 4060 Ti is not a 4060, a 9060 XT is not a 9070 XT + --- + duration_ms: 0.395181 + type: 'test' + ... +# Subtest: a class flip reads stale exactly as src/ember.rs tiers_stale does +ok 5 - a class flip reads stale exactly as src/ember.rs tiers_stale does + --- + duration_ms: 0.461529 + type: 'test' + ... +# Subtest: known-failed: a power rung under 50 is refused +ok 6 - known-failed: a power rung under 50 is refused + --- + duration_ms: 1.007864 + type: 'test' + ... +# Subtest: known-failed: a row missing a field tier_from_json reads is refused +ok 7 - known-failed: a row missing a field tier_from_json reads is refused + --- + duration_ms: 1.01198 + type: 'test' + ... +# Subtest: known-failed: a tuned row dearer per hash than stock is refused, and a max tier that is not stock +ok 8 - known-failed: a tuned row dearer per hash than stock is refused, and a max tier that is not stock + --- + duration_ms: 1.432099 + type: 'test' + ... +# Subtest: known-failed: a card class of the brief left out is refused, and a stale uj (not w over mhs) is refused +ok 9 - known-failed: a card class of the brief left out is refused, and a stale uj (not w over mhs) is refused + --- + duration_ms: 2.615351 + type: 'test' + ... +# Subtest: known-failed: a table under another class is refused +ok 10 - known-failed: a table under another class is refused + --- + duration_ms: 0.914827 + type: 'test' + ... +1..10 +# tests 10 +# suites 0 +# pass 10 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 87.189946 diff --git a/docs/analysis/review-2026-10-08-b/reproductions/recorded-results/environment.json b/docs/analysis/review-2026-10-08-b/reproductions/recorded-results/environment.json new file mode 100644 index 000000000..7e95ac4ce --- /dev/null +++ b/docs/analysis/review-2026-10-08-b/reproductions/recorded-results/environment.json @@ -0,0 +1,8 @@ +{ + "rustc": null, + "cargo": null, + "nvcc": null, + "nvidia_smi": null, + "python": "Python 3.13.5", + "node": "v22.16.0" +} diff --git a/docs/analysis/review-2026-10-08-b/reproductions/recorded-results/portable-reproductions.json b/docs/analysis/review-2026-10-08-b/reproductions/recorded-results/portable-reproductions.json new file mode 100644 index 000000000..7614715e9 --- /dev/null +++ b/docs/analysis/review-2026-10-08-b/reproductions/recorded-results/portable-reproductions.json @@ -0,0 +1,158 @@ +{ + "review": "Igneum updated-stack source review", + "date": "2026-10-08", + "native_rust_executed": false, + "gpu_executed": false, + "sp1_executed": false, + "probe_count": 13, + "probes": [ + { + "probe": "cache_wrong_statement_after_success", + "classification": "ISOLATED_CONTROL_FLOW_REPRODUCTION", + "cold_wrong_context": "Invalid", + "warm_wrong_context": "Verified", + "limitation": "No SP1 proof constructed; models the supplied early-return cache logic." + }, + { + "probe": "cache_negative_context_poisoning", + "classification": "ISOLATED_CONTROL_FLOW_REPRODUCTION", + "valid_context_after_bad_context": "Invalid", + "fresh_valid_context": "Verified" + }, + { + "probe": "cache_zero_id_accepts_pinned_epoch", + "classification": "ISOLATED_CONTROL_FLOW_REPRODUCTION", + "result": "Verified", + "accepted_ids": [ + "vk-B" + ], + "cached_id": "ZERO", + "limitation": "Reachability depends on host configuration and how the cache entry was populated." + }, + { + "probe": "reg64_rotate_xor_prefix_equivalence", + "classification": "EXACT_ALGEBRA_WITH_RANDOMIZED_CHECKS", + "comparisons": 33024, + "point_updates": 4096, + "mismatches": 0, + "equation": "a[k]=ROL(r[k],63-k); P=XOR(a[k],k str: + p = root / path + text = p.read_text() + for needle in needles: + if needle not in text: + raise AssertionError(f'Source changed: {path}: missing {needle!r}') + checked[path] = hashlib.sha256(p.read_bytes()).hexdigest() + return text + +def record(name, classification, data): + results.append({'probe':name, 'classification':classification, **data}) + +# Source-matched cache control flow. Signature/public-value verification is a stub. +read('node/igneum/exec/src/proving.rs', + 'inner.verdicts.get(&c.proof_hash)', + 'Ok(id) if accepted.is_empty() || *id == B256::ZERO || accepted.contains(id)', + 'verdicts.insert(c.proof_hash, r.clone())') +read('node/igneum/exec/src/nativeverify.rs', 'if got.as_slice() != statement') +class CacheModel: + def __init__(self): + self.cache = {} + self.held = {'proof-A': ('Shard', 'statement-A', 'vk-A')} + def verdict(self, digest, kind, statement, accepted): + if digest in self.cache: + ok, value = self.cache[digest] + if not ok: + return 'Invalid' + return 'Verified' if not accepted or value == 'ZERO' or value in accepted else 'Invalid' + if digest not in self.held: + return 'Missing' + actual_kind, actual_statement, actual_vk = self.held[digest] + if kind != actual_kind: + return 'Invalid' + ok = statement == actual_statement and (not accepted or actual_vk in accepted) + self.cache[digest] = (ok, actual_vk if ok else 'context mismatch') + return 'Verified' if ok else 'Invalid' + +cold = CacheModel().verdict('proof-A','Shard','statement-B',{'vk-A'}) +warm = CacheModel() +assert warm.verdict('proof-A','Shard','statement-A',{'vk-A'}) == 'Verified' +warm_wrong = warm.verdict('proof-A','Shard','statement-B',{'vk-A'}) +assert cold == 'Invalid' and warm_wrong == 'Verified' +record('cache_wrong_statement_after_success','ISOLATED_CONTROL_FLOW_REPRODUCTION', + {'cold_wrong_context':cold,'warm_wrong_context':warm_wrong, + 'limitation':'No SP1 proof constructed; models the supplied early-return cache logic.'}) +poison = CacheModel() +assert poison.verdict('proof-A','Shard','statement-B',{'vk-A'}) == 'Invalid' +wrong_first = poison.verdict('proof-A','Shard','statement-A',{'vk-A'}) +assert wrong_first == 'Invalid' +record('cache_negative_context_poisoning','ISOLATED_CONTROL_FLOW_REPRODUCTION', + {'valid_context_after_bad_context':wrong_first,'fresh_valid_context':CacheModel().verdict('proof-A','Shard','statement-A',{'vk-A'})}) +z = CacheModel() +z.cache['proof-A'] = (True,'ZERO') +assert z.verdict('proof-A','Shard','statement-A',{'vk-B'}) == 'Verified' +record('cache_zero_id_accepts_pinned_epoch','ISOLATED_CONTROL_FLOW_REPRODUCTION', + {'result':'Verified','accepted_ids':['vk-B'],'cached_id':'ZERO', + 'limitation':'Reachability depends on host configuration and how the cache entry was populated.'}) + +# Exact algebraic equivalence of the reg64 address mixer. No hash or GPU benchmark. +read('pow/src/verify.rs', 'm = if started { m.rotate_left(1) ^ r[k][lane] } else { r[k][lane] };', 'r[a][lane] ^ m') +MASK = (1 << 32) - 1 + +def rol(x,n): + n %= 32 + return ((x << n) | (x >> ((32-n) % 32))) & MASK + +def naive(r,s): + m = None + for k,x in enumerate(r): + if k == s: + continue + m = x if m is None else rol(m,1) ^ x + return r[s] ^ m + +class IndexedMixer: + def __init__(self, values): + self.r = [0]*64 + self.a = [0]*64 + self.tree = [0]*65 + self.total = 0 + for i,x in enumerate(values): + self.set(i,x) + def set(self,i,x): + a = rol(x,63-i) + delta = a ^ self.a[i] + self.a[i], self.r[i] = a,x + self.total ^= delta + j = i+1 + while j <= 64: + self.tree[j] ^= delta + j += j & -j + def prefix(self,s): + p = 0 + while s: + p ^= self.tree[s] + s -= s & -s + return p + def address(self,s): + p = self.prefix(s) + return self.r[s] ^ rol(p,31) ^ self.total ^ p ^ self.a[s] + +rng = random.Random(0x1A6E20261008) +comparisons = 0 +for v in [[0]*64,[MASK]*64,list(range(64)),[1 << (i%32) for i in range(64)]]: + m = IndexedMixer(v) + for s in range(64): + assert m.address(s) == naive(v,s) + comparisons += 1 +m = IndexedMixer([rng.getrandbits(32) for _ in range(64)]) +for _ in range(4096): + m.set(rng.randrange(64), rng.getrandbits(32)) + for s in rng.sample(range(64),8): + assert m.address(s) == naive(m.r,s) + comparisons += 1 +record('reg64_rotate_xor_prefix_equivalence','EXACT_ALGEBRA_WITH_RANDOMIZED_CHECKS', + {'comparisons':comparisons,'point_updates':4096,'mismatches':0, + 'equation':'a[k]=ROL(r[k],63-k); P=XOR(a[k],k total_f as u128, past)') +def anchored(v4,recovery,lock,checkpoint,window,signed,total): + past = checkpoint >= min((1<<64)-1,lock+window) + if total == 0: + return False,past + if not past: + return 3*signed >= 2*total,past + if v4 and recovery: + return 2*signed > total,past + return False,past +before = [anchored(True,True,0,99,100,60,100)[0] for _ in range(2)] +after = [anchored(True,True,0,100,100,60,100)[0] for _ in range(2)] +pause100 = anchored(True,False,0,100,100,100,100)[0] +assert before == [False,False] and after == [True,True] and not pause100 +record('anchored_recovery_threshold','PURE_PREDICATE_REPRODUCTION', + {'old_table_weights':'40 honest A + 40 honest B + 20 equivocating in both', + 'both_sides_before_window':before,'both_sides_after_window':after, + 'pause_only_post_window_100_of_100':pause100, + 'limitation':'Both branches must satisfy sliding-table, ancestry and dust rules too. Source simulations separately report this case. No real-node conflict produced. Historical-checkpoint backfill may affect healing.'}) + +# Source-contract seam, not a cargo build. +node = read('node/consensus/pow/src/igneum.rs','pub shadow_reps: u16,') +pool = read('mining/pool/src/node.rs','let seeds = EpochSeeds { epoch: info.epoch_seed') +literal = pool.split('let seeds = EpochSeeds {',1)[1].split('};',1)[0] +assert 'shadow_reps' not in literal and '..' not in literal +record('pool_epochseeds_schema_mismatch','STATIC_SOURCE_ASSERTION', + {'required_field':'shadow_reps','present_in_supplied_pool_initializer':False, + 'limitation':'Would fail against the supplied node definition; cargo was not available and the pool vendor tree is absent.'}) +gen = read('pow/src/generator.rs','pub enum ProgramClass {','pub reg64_chain: bool,') +enum = gen.split('pub enum ProgramClass {',1)[1].split('}',1)[0] +assert 'V6' not in enum +record('v6_research_vs_canonical_enum','STATIC_SOURCE_ASSERTION', + {'reg64_chain_exists':True,'canonical_enum_v6_exists':False, + 'limitation':'Not a claim that v6 work is absent. Release manifest and end-to-end activation are not demonstrated by these snapshots.'}) + +# Crash point between externally accepted transfer and durable local state. +read('mining/pool/src/payout.rs','match self.send(&a, wei, nonce).await {','s.dirty = true;') +read('mining/pool/src/main.rs','pool.cfg.snapshot_interval_s.max(1)','s.save(&path)') +disk_balance = 100 +network_paid = 0 +nonce = 0 +network_paid += disk_balance; nonce += 1 # Accepted externally, process dies before local debit/save. +restarted_due = disk_balance +network_paid += restarted_due; nonce += 1 +assert network_paid == 200 +record('pool_send_before_journal_crash','ISOLATED_FAILURE_SEQUENCE', + {'original_due':100,'paid_after_crash_and_retry':network_paid,'transaction_nonces':[0,1], + 'assumptions':['First send accepted on network','Pool has enough funds for retry','Restart loads pre-send snapshot','No external recovery process absent from supplied path'], + 'limitation':'No actual transaction broadcast. Models the missing durable-intent window.'}) + +# Repeated authentication creates stale members in the supplied connection state machine. +server = read('mining/pool/src/server.rs','Msg::Authorize { pubkey, pop, label, payout, .. }','remove(&m.id)') +members = {} +current = None +for i in range(1,4): + members[i] = {'connection':'same'} + current = i +members.pop(current) +assert list(members) == [1,2] +record('pool_reauthorize_cleanup','ISOLATED_CONTROL_FLOW_REPRODUCTION', + {'authorizations_on_one_connection':3,'members_left_after_disconnect':len(members), + 'limitation':'No pool server was run. Static handler contains no prior-member guard/removal.'}) + +# Tuner reachability, not a performance model. +read('mining/app/igneum-app/src/ember.rs', 'let core_down = |p: Point|', 'vec![mem_up(best.point), core_down(best.point)') +start = 1300 +reachable = [start - 100*i for i in range(5)] +assert 1800 not in reachable and max(reachable) == start +record('ember_climb_direction','ALGORITHMIC_REACHABILITY_EXAMPLE', + {'illustrative_start_core_mhz':start,'illustrative_faster_core_mhz':1800, + 'can_climb_propose_higher_core_from_start':False, + 'limitation':'A full sweep can choose a new starting point; no claim the illustrative higher clock is faster on an actual GPU.'}) +ota = read('mining/app/igneum-app/src/ota.rs','if !self.auto && !urgent && !self.install_asked {') +def waits(auto,urgent,asked): return not auto and not urgent and not asked +assert waits(False,False,False) and not waits(False,True,False) +record('ota_disabled_but_urgent','BOOLEAN_GUARD_REPRODUCTION', + {'auto_update':False,'urgent':True,'install_asked':False,'consent_guard_blocks':False, + 'limitation':'Actual application also requires a valid staged release and safe_to_apply; applies to supplied devnet policy.'}) + +# Positive old finding regression: serving API enforces aligned warp group and splitting at high32 rollover. +w = read('mining/proto-cuda/nvrtc/worker.cpp','nonce_start must be 32-aligned') +# Modeled aligned partitioning mirrors the intent; not C++/GPU execution. +start = (1<<32)-32 +remaining = 96 +parts=[] +while remaining: + room = (1<<32)-(start & 0xffffffff) + take = min(remaining,room) + parts.append({'high32':start>>32,'low32':start&0xffffffff,'count':take}) + start += take + remaining -= take +assert [p['count'] for p in parts] == [32,64] +record('cuda_high32_boundary_positive','SOURCE_ASSERTION_AND_ARITHMETIC_CHECK', + {'parts':parts,'alignment_guard_present':True, + 'limitation':'No GPU execution; confirms the earlier host-level alignment omission is covered in this archive.'}) + +# Contradictory economic-table inequality: check only the displayed values, no economic model execution. +model = read('v6/igneum-v6-freeze-tree/docs/analysis/class-v6/coexistence-model.md', + 'USD 18 M of boards against USD 40 to 80 M: PASSES') +assert not (18 > 40) and not (18 > 80) +record('economic_table_displayed_inequality','ARITHMETIC_CHECK', + {'condition':'fleet cost exceeds annual miner revenue','displayed_fleet_musd':18, + 'displayed_revenue_musd':[40,80],'displayed_pass_consistent':False, + 'limitation':'The table or condition may contain a typo; underlying economic workbook was not reproduced.'}) + +output = {'review':'Igneum updated-stack source review','date':'2026-10-08', + 'native_rust_executed':False,'gpu_executed':False,'sp1_executed':False, + 'probe_count':len(results),'probes':results,'source_sha256':checked} +args.out.parent.mkdir(parents=True,exist_ok=True) +args.out.write_text(json.dumps(output,indent=2)+'\n') +print(json.dumps({'probes_completed':len(results),'output':str(args.out),'reg64_comparisons':comparisons},indent=2)) diff --git a/docs/analysis/review-2026-10-08-b/review.md b/docs/analysis/review-2026-10-08-b/review.md new file mode 100644 index 000000000..3ecd1cac2 --- /dev/null +++ b/docs/analysis/review-2026-10-08-b/review.md @@ -0,0 +1,3534 @@ +# IGNEUM 2.0 - Updated full-stack source review + +## Review snapshot: 8 October 2026 + +**Verdict:** There is substantial room for improvement and a credible engineering programme. This is a more complete system than the earlier algorithm pack, but it is not yet an independently validated, integrated release suitable for the leadership claim. Prioritise proof-cache correctness, exact release integration, finality guarantees, payment durability and useful paid-work scheduling before adding algorithm complexity. + +This report reviews the five unique uploaded bundles. Duplicate `(1)` uploads were byte-identical to their counterparts. It traces selected critical paths across the generator, node/DAG, proving guests/host, CUDA/Metal/OpenCL worker hosts, Ember/engine and pool; it is not a line-by-line audit of every file. Source names and branch boundaries are preserved. No source was modified or deployed. + +**What ran:** 13 isolated source/math/control-flow probes; within them, 33,024 reg64 address-expression comparisons with 4,096 point updates; 10 shipped JavaScript tier tests; SHA-256 matching of two supplied proof guest ELFs and two verifying-key files against their manifest. All these checks completed as described. A probe reproducing a defect is NOT an acceptance pass. + +**What did not run:** native Rust tests (no Rust/Cargo toolchain), actual SP1 proving/verification, GPU execution, live nodes, the pool's payment flow, physical ASIC design or the economic workbook. Generated packs and parts of the build/vendor context are omitted from the uploads. Therefore modeled behavior and source assertions are not represented as end-to-end exploit or performance measurements. + +**Priority labels:** P0 is a release-blocking correctness/integration/guarantee decision in this review, not a formal CVSS score. P1 is a serious reliability, security, economic or public-product gate. P2 is an optimisation/product experiment requiring measurement. + + +## Improvements over the earlier review + +- Real v6 research paths exist: register windows, address folding/reweighting, tracing and geometry work. Do not repeat the old statement that the archive has no such implementation. +- Acceptance now executes the shadow for the relevant path, with an explicit regression test. New source/index concentration checks exist. +- CUDA and OpenCL serving interfaces enforce 32-nonce alignment and handle high-word transitions; Metal also partitions work at the boundary. The earlier missing-host concern is partly closed. +- Proof verification is wired into body/payout paths behind activation, with pinned-key and succession logic. F01/F02 explain why that still needs repair. +- OpenCL already performs hit selection with sentinels and overflow fallback. This is an implementation to reuse, not a missing feature across all workers. +- Pool template generation commits member voting identities. Ember has real protection and rollback-related machinery rather than just a preset table. +- The research records negative results and operational failures. That is useful evidence, not a reason to suppress them. + + +## Finding register + +| ID | Priority | Finding | Evidence | +|---|---|---|---| + +| F01 | P0 - public/value-bearing release blocker | Proof-verdict cache omits the statement being verified | SOURCE + ISOLATED CONTROL-FLOW REPRODUCTION | + +| F02 | P0 - release configuration blocker | Proof-rule infrastructure can fail open | STATIC SOURCE | + +| F03 | P0 - freeze/integration blocker | The bundle contains a v6 candidate, not a demonstrated integrated v6 release | STATIC SOURCE + ARCHIVE PROVENANCE | + +| F04 | P0 - finality guarantee decision | Finality v4 recovery deliberately has a weaker safety boundary | SOURCE + SOURCE-REPORTED SIMULATION + PURE PREDICATE REPRODUCTION | + +| F05 | P1 - adversarial hardware evaluation | reg64 address coupling has an exact incremental alternative | EXACT ALGEBRA + 33,024 RANDOMIZED/EDGE COMPARISONS | + +| F06 | P1 - freeze blocker | The v6 acceptance and census gates are not complete for the final execution | STATIC SOURCE + SOURCE-REPORTED RESULTS | + +| F07 | P1 - miner economics and reliability | VRAM admission and proving deadlines need one operator-level scheduler | SOURCE-REPORTED HARDWARE RESULTS + STATIC INTEGRATION REVIEW | + +| F08 | P1 - proving product gate | The proving pipeline reports waste and deadline censoring, not sustained capacity | SOURCE-REPORTED OPERATIONAL DATA; NOT INDEPENDENTLY REPLAYED | + +| F09 | P1 - founding claim not yet earned | The economic model explicitly retains a failed specialist case | SOURCE-REPORTED MODEL + DISPLAYED-ARITHMETIC CHECK | + +| F10 | P2 - byte-preserving performance experiment | CUDA production readback has an existing OpenCL optimization to borrow | STATIC SOURCE; SPEEDUP NOT MEASURED | + +| F11 | P2 - product performance | Ember needs workload-aware identity and objective-aware search | STATIC SOURCE + REACHABILITY EXAMPLE | + +| F12 | P1 - payment integrity | Pool payouts have a broadcast-before-durable-intent window | STATIC SOURCE + ISOLATED CRASH SEQUENCE | + +| F13 | P1 - service resilience | Pool admission and membership need bounded resources | STATIC SOURCE + ISOLATED MEMBERSHIP MODEL | + +| F14 | P1 - public client/independence gate | Developer fleet control must not silently become the public client trust model | STATIC SOURCE + BOOLEAN GUARD REPRODUCTION | + + + +## F01 - Proof-verdict cache omits the statement being verified + +**Priority:** P0 - public/value-bearing release blocker +**Evidence:** SOURCE + ISOLATED CONTROL-FLOW REPRODUCTION + +The native cold verifier checks both the pinned program and the hash of the proof's public values against the carried statement. That is good. However, `ProofPool::verdict_for` returns a cached success keyed only by `proof_hash`, checking only the accepted program ID. It does not compare the carried statement on that path. It also returns cached errors without distinguishing intrinsically invalid proof bytes from an otherwise valid proof queried in the wrong context. + +The isolated model reproduces: a wrong statement is refused with a cold cache; the same wrong statement is accepted after that proof was cached against its correct statement. Querying the wrong statement first can poison the later correct lookup. The zero-ID cache wildcard can also bypass a nonempty accepted-ID list if such an entry has been populated. Host configuration controls that second case's reachability. + +This is NOT an SP1 forgery. It is a failure to bind a cached verification result to its use. `check_record` still checks native execution and signatures, so this finding does not show arbitrary execution roots becoming valid. But proof payment and block-validity decisions consume the cache. Warm/cold or order-dependent decisions are unacceptable there. A malicious block producer need not use the honest producer's template-selection code. Full native reproduction is required to establish exact network impact. + +Fix: cache immutable verified facts (proof kind, actual verifier/program identity, public-values digest, proof format/security version) and compare the required statement and current permitted IDs on EVERY lookup. Alternatively, key by all relevant context. Do not use zero as an accepted pinned ID. Distinguish context-specific refusal from invalid bytes. The relay-time cache population must store the identity actually verified, not merely a configured host label. Bound cache size and in-flight verification. + +### Required closure tests + +- Valid proof A, warm cache, carried statement B: refuse exactly as a cold node does. + +- Run valid/invalid contexts in both orders, concurrently and across cache eviction/restart. + +- Change payout, network, kind, program ID and activation context; no accepted misbinding. + +- A native two-node test must agree on block validity and payouts despite different cache histories. + +### Exact source locations + +- `node/igneum/exec/src/proving.rs:1046-1102` + +- `node/igneum/exec/src/proving.rs:1358-1379` + +- `node/igneum/exec/src/nativeverify.rs:151-177` + +- `node/igneum/exec/src/proving.rs:471-489` + + + +## F02 - Proof-rule infrastructure can fail open + +**Priority:** P0 - release configuration blocker +**Evidence:** STATIC SOURCE + +`check_carried_proofs` has a production-compiled environment bypass (`IGNEUM_TEST_SKIP_PROOF_RULE=1`) and returns success when the oracle is absent. The comments explicitly describe these as harness/unit-test accommodations. This is not evidence an unauthenticated peer can set the environment, nor evidence the normal daemon omits its oracle. It is a configuration failure mode that contradicts mandatory enforcement if accidentally activated. + +After the rule activates, missing verifier infrastructure should stop startup or validation safely, not silently disable the rule. Restrict deliberately unsafe test switches to test-only builds. Distinguish pending proof bytes (retry) from invalid proof (reject) and unavailable trusted verifier (not accepted). + +### Required closure tests + +- Release build cannot activate test bypass. + +- Missing oracle or pinned keys prevents service readiness after enforcement activation. + +- Missing proof bytes retry without incorrectly marking a valid block permanently invalid. + +### Exact source locations + +- `node/consensus/src/pipeline/body_processor/body_validation_in_context.rs:28-79` + + + +## F03 - The bundle contains a v6 candidate, not a demonstrated integrated v6 release + +**Priority:** P0 - freeze/integration blocker +**Evidence:** STATIC SOURCE + ARCHIVE PROVENANCE + +The v6 freeze contains real candidate flags, a 64-register schedule, address mixing, fold/reweight experiments and non-power-of-two dataset geometry. It is materially newer than the earlier V2/V3/V4-only review. Nevertheless the canonical `ProgramClass` enum in this snapshot ends at V5. The freeze README itself says the D1 object cut and spec re-cut are subsequent work. The node bundle is from a different release branch. Generated packs and relevant vendor/build context are omitted. + +There is a concrete seam: the node's `EpochSeeds` struct requires `shadow_reps`, but the pool constructs that type without the field or a struct-update expression. Those exact files cannot be compiled together as written. This was identified statically, not by running Cargo. It does not establish that another deployed pool or vendor revision has the mismatch. + +Fix: produce one release manifest that pins node, generator, dataset policy, acceptance, host ABI, miner app, pool, proof guests/keys and activation. Build the pool and all supported workers against it in CI. Include executable packs and their authenticated identity. Never combine measurements from different candidates into a single v6 claim. + +### Required closure tests + +- Clean build from one manifest, including the pool and workers, with no unpublished vendor tree. + +- Same job context produces identical accepted work in node, CPU reference, CUDA, Metal, OpenCL and pool. + +- Cross every scheduled transition with old/new client behavior documented and identical rule identities. + +### Exact source locations + +- `v6/igneum-v6-freeze-tree/README-FREEZE.txt:1-1` + +- `pow/src/generator.rs:249-277` + +- `pow/src/generator.rs:939-954` + +- `node/consensus/pow/src/igneum.rs:72-95` + +- `mining/pool/src/node.rs:47-71` + + + +## F04 - Finality v4 recovery deliberately has a weaker safety boundary + +**Priority:** P0 - finality guarantee decision +**Evidence:** SOURCE + SOURCE-REPORTED SIMULATION + PURE PREDICATE REPRODUCTION + +The older unconditional table-expiry problem has been addressed: the v4 anchored table does not simply disappear. However, after a full window without a lock, the recovery branch accepts strictly more than half of the anchored weight. The supplied guarantee document openly reports conflicting recovery locks in a prolonged partition when an equivocator both mines and signs on both sides. The 40/40/20 case is explicitly included. + +The isolated predicate confirms two sides each holding 60 of the original 100 pass the recovery threshold after the window, although neither passes the two-thirds threshold before it. This alone is not a full protocol exploit: sliding tables, ancestry, dust eligibility and signed certificates also matter. The team's simulation record supplies additional evidence. The same document says its real-node line at the snapshot is the known-failed v3 case and the v4 line still awaits the node change. + +Do not call normal finality and recovery finality the same irreversible guarantee. Prefer a reviewed authority-transition/recovery rule retaining the claimed safety assumptions; otherwise restrict and label the recovery state and dependent wallet/bridge actions explicitly. A timeout does not prove that a missing authority is permanently gone. + +The pure pause-only predicate refuses every post-window checkpoint, even with 100% anchored signatures. This is not proof of permanent network liveness failure: historical-checkpoint backfill may re-anchor first. The reported immediate-heal simulations must be reproduced against the actual implementation, including that path. + +### Required closure tests + +- Native multi-node 40/40/20 partition, equivocation and dust-valid mining on both sides past the window. + +- Proof that the chosen recovery guarantee matches the public finality claim; two valid contradictory certificates are a hard failure for strong finality. + +- Pause-only resume with historical backfill, missing historical data and all old keys returning. + +- Wallet, receipt and oracle consumers distinguish any weaker recovery state. + +### Exact source locations + +- `node/consensus/src/processes/finality.rs:1111-1128` + +- `node/consensus/src/processes/finality.rs:2610-2642` + +- `dag/igneum-node-dag/spec/docs/spec/finality-guarantees.md:111-136` + +- `dag/igneum-node-dag/spec/docs/spec/finality-guarantees.md:184-197` + + + +## F05 - reg64 address coupling has an exact incremental alternative + +**Priority:** P1 - adversarial hardware evaluation +**Evidence:** EXACT ALGEBRA + 33,024 RANDOMIZED/EDGE COMPARISONS + +The full-chain window computes a rotate-XOR fold over the 63 registers other than the source, then XORs the source. This connects every register syntactically, but it does not force a physical implementation to reread and fold all 63 on each load. + +Define a[k] = ROL32(r[k], 63-k), S = XOR of all a[k], and P_s = XOR of a[k] for k < s. Then the exact source expression is: + + address_source(s) = r[s] XOR ROR32(P_s, 1) XOR S XOR P_s XOR a[s] + +A prefix-XOR tree supports point updates and prefix queries in logarithmic time. The included model checked 33,024 comparisons, including 4,096 state updates, without a mismatch. The algebra follows by distributing the rotation across XOR: values before the excluded source have one fewer subsequent rotation; values after it retain their original exponent. + +This is NOT evidence that the full hash is cheap, that the necessary 64-register state is compressible to one word, or that the extra index state is free. Cached prefix state, port bandwidth and update costs must all be priced. The GPU compiler may already remove some redundant work. It is a concrete alternative the adversarial designer must be allowed, and potentially a byte-preserving implementation experiment for both sides. + +Do not respond by adding unmeasured nonlinear work. First implement the cheapest alternatives, remeasure GPU cost, then compare complete hardware designs. A one-register perturbation test cannot establish a minimum circuit or storage cost. + +### Required closure tests + +- Native reference-vs-incremental expression equivalence across all source registers and real instruction updates. + +- Full-kernel output equivalence, registers, spills, wall power and accepted throughput across target GPUs. + +- Adversary physical design includes prefix caching/recomputation cost; no assumed full 63-read cost on every load. + +### Exact source locations + +- `pow/src/verify.rs:674-692` + +- `pow/src/generator.rs:258-277` + + + +## F06 - The v6 acceptance and census gates are not complete for the final execution + +**Priority:** P1 - freeze blocker +**Evidence:** STATIC SOURCE + SOURCE-REPORTED RESULTS + +The new code explicitly executes the V4 shadow in its acceptance interpreter, includes repeated-source and index-concentration checks, and contains a regression test for shadow agreement. That improves on the older review. The v6 comments nevertheless say reg64's draw/acceptance are the underlying class's, while the liveness check is a separate research check not wired into canonical acceptance. Its sampling checks influence, not a formal unavoidable-state lower bound. + +The census is similarly candid: rw2 fails its F8 line; fold plus rw1 is stronger on the reported controls; reg64/all results are based on the full tracing path with closed-form data, and the live-dataset point remains owed. These source results must not be promoted into an unconditional full-v6 pass. + +Freeze one agreed acceptance rule for the actual scheduled 64-register execution, and specify safe deterministic fallback behavior when a candidate fails. Re-run known-bad seeds, unseen seeds, real datasets, bound headers/nonces, the combined intended width/geometry and all family transitions. Retain rw2 as a rejected control unless new evidence changes the decision. + +### Required closure tests + +- Acceptance/reference/emitter evaluate the same activated schedule. + +- Full live-dataset census on unseen seeds and the complete v6 pack. + +- A failed experimental rule does not silently exhaust generation or bypass the intended resource requirement. + +### Exact source locations + +- `pow/src/accept.rs:114-119` + +- `pow/src/accept.rs:625-637` + +- `pow/src/accept.rs:867-871` + +- `pow/src/generator.rs:258-264` + +- `v6/igneum-v6-freeze-tree/docs/analysis/class-v6/census-packs.md:8-22` + +- `v6/igneum-v6-freeze-tree/docs/analysis/class-v6/census-packs.md:40-49` + + + +## F07 - VRAM admission and proving deadlines need one operator-level scheduler + +**Priority:** P1 - miner economics and reliability +**Evidence:** SOURCE-REPORTED HARDWARE RESULTS + STATIC INTEGRATION REVIEW + +The coexistence records report successful standalone compressed proofs: 13.2 seconds on a 3060 12 GB and 8.2 seconds on a 4060 8 GB after correcting the packaged prover server. They also report that both fail when run beside the 5.5 GiB dataset miner, with device allocation failures while mining continues. These are team measurements, not measurements made for this review. The registered reg64 rows use a different kit configuration and must not be combined with ds55 rows as one benchmark. + +The right product path is explicit modes: simultaneous execution only on tested configurations with headroom; time-sharing on smaller cards with actual dataset eviction/release and confirmed memory availability; mining-only where a complete paid proof job cannot fit. Merely pausing kernel dispatch does not establish that GPU allocations were released. + +Use per-device identity and a memory reservation/lease state machine across miner, prover, aggregation, benchmark and next-epoch preparation. Include time to evict/rebuild datasets, WSL process startup, aggregation and payment deadlines in job admission. Do not market an isolated successful shard as proof that the card can finish the economically relevant segment. + +### Required closure tests + +- Same final v6 configuration: mine -> evict -> prove -> aggregate -> submit -> rebuild -> resume; no leaked reservations. + +- OOM, process crash and stale work recover without losing wallet state or silently consuming power. + +- 16 GB+ simultaneous mode only after measured peak plus next-epoch headroom; smaller-card modes labelled separately. + +### Exact source locations + +- `v6/igneum-v6-freeze-tree/docs/analysis/class-v6/coexist-rows.md:17-37` + +- `v6/igneum-v6-freeze-tree/docs/analysis/class-v6/coexist-rows.md:60-86` + + + +## F08 - The proving pipeline reports waste and deadline censoring, not sustained capacity + +**Priority:** P1 - proving product gate +**Evidence:** SOURCE-REPORTED OPERATIONAL DATA; NOT INDEPENDENTLY REPLAYED + +The supplied pipeline report says the requested two-hour declared-load window does not exist in its record. It describes a class-v5 transition stall/partition and 93 paid segments in the paid interval, not a successful end-to-end hold at 150 transactions per second. Its 3060 tier completed zero paid segments over 313 claims. This does not prove a 3060 can never prove profitably: the same report says its completed submissions occurred after the stall, and the standalone fixture succeeds. It does show the claimed consumer-paid-work experience is not established by this run. + +A particularly important measurement issue: the worklist stays around 600 entries because entries expire at a deadline whether proved or not. Therefore bounded queue length does not establish sufficient proving capacity. The report itself discloses this mechanism. + +Add lifecycle accounting: eligible work = completed + active + expired + explicitly cancelled, with definitions preventing double-counting. Publish deadline misses and useful accepted proof throughput, not just queue depth. Attribute failure to protocol partition, packaging, proving, assignment race, aggregation or submission. The source reports 70 wasted card-hours versus roughly 4 paid, dominated by the chain incident, so it would be wrong to call that all a prover-speed failure. + +Prioritise feasible job sizing and deadlines, resumable verified shards/checkpoints, early cancellation of obsolete claims, and bounded assignment protection. Protection must prevent slow or malicious claimants from monopolising work; do not simply promise no competing completion can ever occur. Run the two-hour workload test on the pinned release, then a longer independent soak. + +### Required closure tests + +- Report every eligible job outcome, including expired work; a bounded list cannot hide losses. + +- Consumer tiers complete and receive payment for declared jobs before claims about income. + +- Sustained real workload across class transitions, with restart/retry and no publisher intervention. + +### Exact source locations + +- `proving/igneum-proving/docs/analysis/proving-pipeline-2026-10-08.md:7-16` + +- `proving/igneum-proving/docs/analysis/proving-pipeline-2026-10-08.md:35-84` + +- `proving/igneum-proving/docs/analysis/proving-pipeline-2026-10-08.md:86-121` + + + +## F09 - The economic model explicitly retains a failed specialist case + +**Priority:** P1 - founding claim not yet earned +**Evidence:** SOURCE-REPORTED MODEL + DISPLAYED-ARITHMETIC CHECK + +The coexistence model states that the desired competitiveness statement does not hold for its modeled N2 SRAM die after development is sunk, and that the remaining deterrent is the investment decision. That is not proof the proposed die is manufacturable at the stated cost or throughput. It is also not evidence that the fundamental gate has passed. + +There are at least two items to repair before using the model as certification. Its condition (c) requires fleet cost to exceed a year's mining revenue but labels USD 18M against USD 40-80M a pass; those displayed numbers do not satisfy that inequality. The table or criterion may be mistaken. Condition (g) also treats proving income as a business the specialised supplier cannot enter because its hash engine cannot prove. A company can own companion GPUs or buy proofs; the single-device limitation does not exclude the operator. + +Reproduce the model from raw inputs, price the SRAM/recomputation design at physical board boundaries, include uncertainty and independent hardware critique, and test a hybrid operator. Treat unknown feasibility as unknown, not either a proven attack or an automatic pass. No retirement credit without a demonstrated adaptation penalty. + +### Required closure tests + +- Generate all pass/fail cells directly from the declared inequalities and inputs. + +- Independent feasibility and cost evaluation of the strongest modeled SRAM/hybrid opponent. + +- GPU owners and entrants remain viable under the approved scenario envelope without assuming chip absence or death. + +### Exact source locations + +- `v6/igneum-v6-freeze-tree/docs/analysis/class-v6/coexistence-model.md:299-321` + + + +## F10 - CUDA production readback has an existing OpenCL optimization to borrow + +**Priority:** P2 - byte-preserving performance experiment +**Evidence:** STATIC SOURCE; SPEEDUP NOT MEASURED + +The CUDA serving loop synchronises and copies one 64-bit result per nonce to the CPU, then scans it while holding its GPU mutex. For 2^24 nonces that is 128 MiB of result data per batch. Its benchmark times the kernel/synchronisation but reads the full result only for warm-up, so the benchmark does not include the same per-batch production cost. + +OpenCL already contains a select kernel that returns matching nonce/hash pairs plus sentinel words, with a counter and full-read fallback when more than 256 hits occur. This is not missing everywhere: it is a cross-backend parity opportunity. Port the proven shape to CUDA first as a separate selection pass, retaining correctness sentinels and overflow handling. Then test fusion/asynchronous overlap if justified. Metal still scans shared output on the CPU; unified memory means it is not the same PCIe-copy problem, so profile it separately. + +Benchmark accepted shares per wall-joule in serving mode, including setup, stale cancellation, readback, host power and pool submission. No percentage gain is claimed here. + +### Required closure tests + +- Compare exact found nonce/hash sets with full-read mode, including all-hit overflow and zero-hit cases. + +- Test stale jobs, high-32 rollover, tail batches and asynchronous buffer reuse. + +- Compare production throughput and wall energy, not only the isolated kernel timer. + +### Exact source locations + +- `mining/proto-cuda/nvrtc/worker.cpp:1254-1295` + +- `mining/proto-cuda/nvrtc/worker.cpp:1318-1341` + +- `mining/proto-opencl/host.c:1652-1705` + +- `mining/proto-opencl/host.c:1882-1899` + +- `mining/proto-metal/main.swift:3344-3370` + + + +## F11 - Ember needs workload-aware identity and objective-aware search + +**Priority:** P2 - product performance +**Evidence:** STATIC SOURCE + REACHABILITY EXAMPLE + +Ember has useful thermal/fault checks, power and clock controls, calibration rows and fleet priors. Its shipped tier tests pass in this review. Those tests do not measure actual safe tuning or globally optimal settings. + +The five-step hill climb proposes memory-up, core-down or both, even for MaxRate (which changes the score to MH/s). Starting from a low-clock efficiency prior, this search cannot propose a higher core clock or change the fixed power cap to escape that starting regime. A full sweep can choose a new start, so the finding is a limitation of this climb, not evidence every MaxRate setting is wrong. + +The prior key uses card model, driver major and class. The workload class helper is based on load/wide-load counts. This is insufficient as a validated measurement identity for changes in memory geometry, compiler, reg64 schedule or concurrent proving. A prior may remain a useful starting hint; it should not be treated as a current certified optimum. + +Use a two-sided, bounded search appropriate to the chosen objective; rebase when switching goals; fingerprint the actual workload/backend and retain per-device calibration separately from fleet hints. Use paired A/B/A samples, a stability soak and rejected-work checks. Support separate mining-only, proving-only and hybrid profiles, coordinated by the device scheduler. Optimise accepted work or transparent net-return estimates, not raw displayed MH/s alone. + +### Required closure tests + +- Goal switch from an efficiency prior can explore higher core/power when policy allows. + +- Driver, workload, dataset, compiler and device changes invalidate certification while retaining optional hints. + +- Thermal/error/late-share events revert safely, including process or machine crash. + +### Exact source locations + +- `mining/app/igneum-app/src/ember.rs:211-280` + +- `mining/app/igneum-app/src/ember.rs:493-520` + +- `mining/app/igneum-app/src/ember.rs:606-619` + + + +## F12 - Pool payouts have a broadcast-before-durable-intent window + +**Priority:** P1 - payment integrity +**Evidence:** STATIC SOURCE + ISOLATED CRASH SEQUENCE + +The payout loop broadcasts first, then updates in-memory balances and records; disk snapshots are a separate periodic loop. If a transaction succeeds externally and the process dies before the debit is durable, restart can load the old payable balance and send again using a later nonce. A lost RPC response creates a related uncertain-outcome problem. The isolated model shows 100 units due becoming 200 externally paid under those assumptions; no actual transaction was sent. + +Receipt checking does exist, and failed receipts re-credit balances. Do not describe this as a pool with no receipt logic. However, a receipt is marked confirmed immediately and the loop subsequently visits only sent records; the supplied path does not establish finality-aware reorg handling. + +Persist an idempotent payment intent and exact signed transaction/hash BEFORE broadcast, reserve the balance atomically, reconcile the same intent after timeout/restart, and finalise against the chain's declared finality. Use explicit prepared/broadcast/mined/finalised/reorged/replaced states. Test crashes around every durable step. + +### Required closure tests + +- Crash before/after broadcast, response timeout, restart before snapshot: no duplicate payment or silent debt loss. + +- Same signed transaction retried, fee replacement reconciled by intent, not a new payment. + +- Receipt reorg and finality pause leave correct pending obligations. + +### Exact source locations + +- `mining/pool/src/payout.rs:228-261` + +- `mining/pool/src/payout.rs:265-296` + +- `mining/pool/src/main.rs:138-149` + + + +## F13 - Pool admission and membership need bounded resources + +**Priority:** P1 - service resilience +**Evidence:** STATIC SOURCE + ISOLATED MEMBERSHIP MODEL + +The server checks MAX_LINE after reading a full line, uses an unbounded outgoing channel, and inserts a nonce into a job's seen set before validation. Repeated Authorize requests create fresh members without removing or rejecting the prior one, while disconnect removes only the latest member. The small state-machine reproduction leaves two members after three authorizations on one connection and disconnect. + +These are resource-control issues, not demonstrated remote exploits against a running pool. The verifier semaphore is a useful existing control but does not bound every queue or allocation. + +Enforce frame size while reading, bounded write queues, connection/request budgets, a single authenticated membership per session, cheap target/context prefilters, and bounded deduplication with in-flight handling. Test slow readers and malformed/invalid share floods without expensive network attacks. Member vote keys are already committed into the template; preserve that improvement. + +### Required closure tests + +- Repeated authorization rejected or atomically replaces and cleans prior state. + +- Oversized unterminated frame rejected within fixed memory/time budget. + +- Slow clients and invalid share floods cannot grow unbounded member, nonce or outgoing state. + +### Exact source locations + +- `mining/pool/src/server.rs:62-99` + +- `mining/pool/src/server.rs:115-175` + +- `mining/pool/src/server.rs:234-255` + +- `mining/pool/src/server.rs:281-299` + +- `mining/pool/src/node.rs:41-48` + + + +## F14 - Developer fleet control must not silently become the public client trust model + +**Priority:** P1 - public client/independence gate +**Evidence:** STATIC SOURCE + BOOLEAN GUARD REPRODUCTION + +The supplied settings explicitly call remote jobs default-on for the devnet build. Jobs are signed and have a visible disable switch; disabling aborts work. This is not a hidden unauthenticated backdoor. It is still powerful publisher control: run-script, fetch, collect, restart and update-now jobs share the OTA signing key, and some jobs run elevated or as WSL root. + +The updater's auto-off guard is bypassed for an urgent release (unsupported version or nearby fork). This is intentional in the supplied policy, not a signature bypass. The remaining staging/safety checks still apply. An operator who disabled automatic updates should not unknowingly grant an urgency label permission to install arbitrary future software. + +Separate a controlled lab/developer build from the public miner; remove arbitrary remote execution from the public default or use narrow explicit per-capability consent and a separate trust root. Keep user update acceptance distinct from consensus activation. Emergency safety notifications may pause unsupported operations; they should not silently override the user's installation choice. Review any manifest-delivered consensus overrides under the same rule. + +### Required closure tests + +- Public build has no default arbitrary remote execution and a documented least-privilege boundary. + +- Automatic updates off remains off for urgent manifests until explicit action. + +- A compromised fleet/update signing key cannot silently acquire wallet access or activate a consensus change. + +### Exact source locations + +- `mining/app/igneum-app/src/config.rs:75-81` + +- `mining/app/igneum-app/src/config.rs:135-151` + +- `mining/app/igneum-app/src/jobrun.rs:1-19` + +- `mining/app/igneum-app/src/ota.rs:540-555` + +- `mining/app/igneum-app/src/ota.rs:589-595` + + + +## Work sequence + +### 1. Establish a coherent, enforced release +Fix F01/F02 and the F03 release seam. Pin source, parameters, guests, keys, worker contracts and generated artifacts. Reproduce cache-order independence and exact node/worker/pool agreement natively. Resolve the finality guarantee in F04 before calling recovery irreversible. + +### 2. Protect operator money and control +Implement the payment journal, bound the pool, separate public/developer controls, and reproduce crash/restart behavior. Keep member voting ownership intact. + +### 3. Make proving economically useful on the declared hardware +Implement coordinated GPU memory admission and deadline-aware work selection. Re-run a declared sustained workload across the actual transition boundary; report paid completions, expiry and wasted work, not just worklist size or shard speed. + +### 4. Improve honest execution and attack the same candidate +Port the OpenCL result-selection pattern to CUDA. Evaluate objective-aware Ember tuning and exact reg64 alternatives. Re-run full live-dataset acceptance and physical adversarial cost analysis against the identical candidate. + +### 5. Earn the competitive claim +Close the economic-model failures or explicitly narrow the approved scenario envelope. Run the existing 2.0 acceptance programme on the pinned release, including independent review, repeat external buyers, operator retention, current comparisons and sustained no-founder operation. None of the probes here establishes a numerical market ranking. + + +## What this means for the number-one ambition + +The project has a concrete basis for continued engineering, and several improvements can be made without inventing another mining primitive. The most valuable product is not a nominally complicated hash: it is correctly enforced work, competitive complete-system cost, reliable payment, independent control and repeat demand. + +The founding coexistence statement is not yet established by these uploads. In particular, the source's own SRAM-die model is a failed case, its full-v6 live-dataset census is unfinished, its pipeline hold was interrupted, and its finality recovery deliberately trades safety for recovery. These are not grounds for declaring the ambition impossible. They are specific proof obligations that must be closed rather than averaged away. + +Passing the repaired technical and economic gates could support a credible leadership-contender case. It cannot guarantee adoption, all future hardware behavior, or a number-one ranking. + + +## Reproduction results + +```json + +{ + "review": "Igneum updated-stack source review", + "date": "2026-10-08", + "native_rust_executed": false, + "gpu_executed": false, + "sp1_executed": false, + "probe_count": 13, + "probes": [ + { + "probe": "cache_wrong_statement_after_success", + "classification": "ISOLATED_CONTROL_FLOW_REPRODUCTION", + "cold_wrong_context": "Invalid", + "warm_wrong_context": "Verified", + "limitation": "No SP1 proof constructed; models the supplied early-return cache logic." + }, + { + "probe": "cache_negative_context_poisoning", + "classification": "ISOLATED_CONTROL_FLOW_REPRODUCTION", + "valid_context_after_bad_context": "Invalid", + "fresh_valid_context": "Verified" + }, + { + "probe": "cache_zero_id_accepts_pinned_epoch", + "classification": "ISOLATED_CONTROL_FLOW_REPRODUCTION", + "result": "Verified", + "accepted_ids": [ + "vk-B" + ], + "cached_id": "ZERO", + "limitation": "Reachability depends on host configuration and how the cache entry was populated." + }, + { + "probe": "reg64_rotate_xor_prefix_equivalence", + "classification": "EXACT_ALGEBRA_WITH_RANDOMIZED_CHECKS", + "comparisons": 33024, + "point_updates": 4096, + "mismatches": 0, + "equation": "a[k]=ROL(r[k],63-k); P=XOR(a[k],k kaspa_consensus_core::proving::ProofVerdict { + +1049: use kaspa_consensus_core::proving::{ProofKind, ProofVerdict}; + +1050: let accepted = self.cfg.accepted_ids(c.kind, c.carrier_daa); + +1051: let held = { + +1052: let inner = self.inner.lock(); + +1053: if let Some(v) = inner.verdicts.get(&c.proof_hash) { + +1054: return match v { + +1055: Ok(id) if accepted.is_empty() || *id == B256::ZERO || accepted.contains(id) => ProofVerdict::Verified, + +1056: Ok(id) => ProofVerdict::Invalid(format!("{}: verified under program id {id}, which the carrier's epoch at DAA {} does not accept (accepted: {})", c.label, c.carrier_daa, accepted.iter().map(|i| i.to_string()).collect::>().join(", "))), + +1057: Err(e) => ProofVerdict::Invalid(e.clone()), + +1058: }; + +1059: } + +1060: inner.proofs.get(&c.proof_hash).map(|(k, b)| (*k, b.clone())) + +1061: }; + +1062: let Some((kind, bytes)) = held else { return ProofVerdict::Missing }; + +1063: if kind != c.kind { + +1064: return ProofVerdict::Invalid(format!("the proof is held as a {kind:?} proof and the record carries it as a {:?} record", c.kind)); + +1065: } + +1066: let started = std::time::Instant::now(); + +1067: // in-process on Unix; through the installed host on Windows (sp1-jit does not build there) + +1068: let r = if crate::nativeverify::IN_PROCESS { + +1069: crate::nativeverify::verify_kind(keys, c.kind, &bytes, &c.statement, &c.label, &accepted) + +1070: } else if let VerifyMode::Command(host) = &self.cfg.verify { + +1071: // the host pins one pair, the one it names at start (`cfg.shard_program_id` / `aggregator_id`, zero when unnamed) + +1072: crate::nativeverify::verify_via_host(host, c.kind, &bytes, &c.statement, &c.label).map(|()| self.host_pair_id(c.kind)) + +1073: } else { + +1074: Err(format!("{}: this build has no in-process verifier and no verifier host is configured", c.label)) + +1075: }; + +1076: info!("[igneum-exec] consensus verify of {}: {} in {:.3} s", c.label, if r.is_ok() { "VERIFIED" } else { "REFUSED" }, started.elapsed().as_secs_f64()); + +1077: self.inner.lock().verdicts.insert(c.proof_hash, r.clone()); + +1078: match r { + +1079: Ok(_) => ProofVerdict::Verified, + +1080: Err(e) => ProofVerdict::Invalid(e), + +1081: } + +1082: } + +1083: + +1084: /// The pair the installed host verifies under: the ids named at start (the object's or the host's `--mode id`), + +1085: /// zero when unnamed (then any epoch without pinned ids accepts it). + +1086: fn host_pair_id(&self, kind: kaspa_consensus_core::proving::ProofKind) -> B256 { + +1087: match kind { + +1088: kaspa_consensus_core::proving::ProofKind::Shard => self.cfg.shard_program_id.unwrap_or(B256::ZERO), + +1089: kaspa_consensus_core::proving::ProofKind::Segment => self.cfg.aggregator_id.unwrap_or(B256::ZERO), + +1090: } + +1091: } + +1092: + +1093: /// Enforced proving: whether a carried proof is VERIFIED by this node (the cached verdict of the body rule or the + +1094: /// relay-time verifier, else a verify now when the bytes are held and keys exist). Missing bytes, no keys, a wrong + +1095: /// proof, another program id: false, and the record pays nothing. + +1096: pub fn verified_for_payment(&self, c: &kaspa_consensus_core::proving::CarriedProof, keys: Option<&crate::nativeverify::Keys>) -> bool { + +1097: match keys { + +1098: Some(k) => matches!(self.verdict_for(c, k), kaspa_consensus_core::proving::ProofVerdict::Verified), + +1099: None => { + +1100: let accepted = self.cfg.accepted_ids(c.kind, c.carrier_daa); + +1101: matches!(self.inner.lock().verdicts.get(&c.proof_hash), Some(Ok(id)) if accepted.is_empty() || *id == B256::ZERO || accepted.contains(id)) + +1102: } + +``` + +**`node/igneum/exec/src/proving.rs:1358-1379`** +SHA-256: `92302ee089fca720f2ee6ac0756c0054a01c995db4c997d358683a2a08f3401b` + +```text + +1358: fn set_verified(&self, key: (Hash, u32, Hash), ok: bool, note: String) { + +1359: let host_id = self.host_pair_id(kaspa_consensus_core::proving::ProofKind::Shard); + +1360: let mut inner = self.inner.lock(); + +1361: if let Some(h) = inner.entries.get(&key).map(|e| e.record.proof_hash) { + +1362: inner.verdicts.insert(h, if ok { Ok(host_id) } else { Err(note.clone()) }); + +1363: } + +1364: if ok { + +1365: inner.verified_count += 1; + +1366: } else { + +1367: inner.failed_count += 1; + +1368: } + +1369: if let Some(e) = inner.entries.get_mut(&key) { + +1370: e.verified = Some(ok); + +1371: e.note = note; + +1372: } + +1373: } + +1374: + +1375: fn set_segment_verified(&self, key: (u64, Hash), ok: bool, note: String) { + +1376: let host_id = self.host_pair_id(kaspa_consensus_core::proving::ProofKind::Segment); + +1377: let mut inner = self.inner.lock(); + +1378: if let Some(h) = inner.segments.get(&key).map(|e| e.record.proof_hash) { + +1379: inner.verdicts.insert(h, if ok { Ok(host_id) } else { Err(note.clone()) }); + +``` + +**`node/igneum/exec/src/nativeverify.rs:151-177`** +SHA-256: `bbb11e72495c04f44172c2ec6f230919f8ff96b8b45847e03efacd063f94da96` + +```text + +151: pub fn verify_kind(keys: &Keys, kind: ProofKind, bytes: &[u8], statement: &[u8; 32], what: &str, accepted: &[B256]) -> Result { + +152: let proof: SP1ProofWithPublicValues = bincode::deserialize(bytes).map_err(|e| format!("{what}: the proof bytes are not a bincode SP1 proof: {e}"))?; + +153: let Some(claimed) = claimed_program_id(&proof) else { return Err(format!("{what}: the proof is not a compressed SP1 proof")) }; + +154: if !accepted.is_empty() && !accepted.contains(&claimed) { + +155: return Err(format!("{what}: the proof claims program id {claimed}, which the carrier's epoch does not accept (accepted: {})", accepted.iter().map(|i| i.to_string()).collect::>().join(", "))); + +156: } + +157: let Some((id, vk)) = keys.pairs(kind).into_iter().find(|(id, _)| *id == claimed) else { + +158: return Err(format!("{what}: the proof claims program id {claimed}, which this node does not embed (embedded: {})", keys.ids(kind).iter().map(|i| i.to_string()).collect::>().join(", "))); + +159: }; + +160: verify(bytes, statement, vk, id, what).map(|()| id) + +161: } + +162: + +163: /// The verifier itself: the proof bytes (bincode `SP1ProofWithPublicValues`) must claim the pinned id, carry + +164: /// public values whose keccak-256 is `statement`, and verify under `vk`. + +165: pub fn verify(bytes: &[u8], statement: &[u8; 32], vk: &SP1VerifyingKey, pinned_id: B256, what: &str) -> Result<(), String> { + +166: let proof: SP1ProofWithPublicValues = bincode::deserialize(bytes).map_err(|e| format!("{what}: the proof bytes are not a bincode SP1 proof: {e}"))?; + +167: let got = alloy_primitives::keccak256(proof.public_values.as_slice()); + +168: if got.as_slice() != statement { + +169: return Err(format!("{what}: the proof's public values hash to {got}, the record's statement is 0x{}", hex::encode(statement))); + +170: } + +171: match claimed_program_id(&proof) { + +172: Some(id) if id == pinned_id => {} + +173: Some(id) => return Err(format!("{what}: the proof claims program id {id}, the pinned id is {pinned_id}")), + +174: None => return Err(format!("{what}: the proof is not a compressed SP1 proof")), + +175: } + +176: let verifier = LightProver::new(); + +177: verifier.verify(&proof, vk, None).map_err(|e| format!("{what}: the SP1 verifier refuses the proof: {e}")) + +``` + +**`node/igneum/exec/src/proving.rs:471-489`** +SHA-256: `92302ee089fca720f2ee6ac0756c0054a01c995db4c997d358683a2a08f3401b` + +```text + +471: pub fn carried_payouts(state: &ExecState, cfg: &ProvingConfig, carrier_number: u64, carrier_daa: u64, carried: Vec<(Hash, ProofRecord)>, paid: &mut HashMap<(Hash, u32), PaidShard>, verified: &dyn Fn(&kaspa_consensus_core::proving::CarriedProof) -> bool) -> (Vec<(Address, U256)>, Vec) { + +472: let mut payouts = Vec::new(); + +473: let mut out = Vec::with_capacity(carried.len()); + +474: let active = cfg.active_at(carrier_daa); + +475: let enforced = carrier_daa >= cfg.verified_payout_from; + +476: for (carrier, record) in carried { + +477: let key = record.shard_key(); + +478: let outcome = match check_record(state, cfg, &record, carrier_number, carrier_daa) { + +479: Err(e) => CarriedRecord { record, carrier, rejected: e, paid_wei: 0 }, + +480: Ok(_) if paid.contains_key(&key) => CarriedRecord { record, carrier, rejected: "shard already paid".into(), paid_wei: 0 }, + +481: Ok(_) if !active => CarriedRecord { record, carrier, rejected: format!("before activation (DAA {} of {})", carrier_daa, cfg.activation_daa), paid_wei: 0 }, + +482: Ok(_) if enforced && !verified(&carried_proof_of(&record, carrier_daa)) => CarriedRecord { record, carrier, rejected: "proof not verified by this node (enforced proving): no payment".into(), paid_wei: 0 }, + +483: Ok(c) => { + +484: let wei = shard_parts(cfg, c.segment_daa, c.pool_credit_wei, c.shards)[record.shard as usize]; + +485: let payout = Address::from(record.payout); + +486: if wei > 0 { + +487: payouts.push((payout, U256::from(wei))); + +488: } + +489: paid.insert(key, PaidShard { carrier_number, key_hash: record.key_hash(), payout, wei }); + +``` + +### F02 excerpts + +**`node/consensus/src/pipeline/body_processor/body_validation_in_context.rs:28-79`** +SHA-256: `80de37d434290030d6e17ce71a6cf11e6ed9ddd011288acc1cc6004d3312d5f5` + +```text + +28: /// `Params::proof_rule_active_from()` (block zero under the named switch `verifier_in_consensus`, else the DAA floor + +29: /// `proving_consensus_verify_daa`), every proof record the coinbase carries + +30: /// must come with a proof the node holds and that verifies for the record's statement under the pinned program + +31: /// id. A failing proof makes the block invalid (a producer paid for fake proofs in its own blocks was the one + +32: /// attack line where a majority earned more than it spent); a proof not held yet is `IgneumProofMissing`, which + +33: /// is retried, not marked, since the relay delivers proof bytes beside their records a moment before or after + +34: /// the carrying block. The verifier is the execution layer's (`set_proof_oracle`); the verdicts are cached by + +35: /// proof hash, so a proof verified at relay time costs nothing here. No oracle (unit tests) = the rule is off. + +36: fn check_carried_proofs(self: &Arc, block: &Block) -> BlockProcessResult<()> { + +37: if block.header.daa_score < self.proof_rule_active_from { + +38: return Ok(()); + +39: } + +40: // the attacker's shape for the harness only: a node with the rule switched off carries fake proofs; every + +41: // honest peer refuses its blocks (IGNEUM_TEST_SKIP_PROOF_RULE=1, never set on a live node) + +42: static SKIP: std::sync::OnceLock = std::sync::OnceLock::new(); + +43: if *SKIP.get_or_init(|| std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").map(|v| v == "1").unwrap_or(false)) { + +44: return Ok(()); + +45: } + +46: let Some(oracle) = kaspa_consensus_core::proving::proof_oracle() else { return Ok(()) }; + +47: // the rule applies from the floor only (never on the live Devnet 3 object): below it a carried + +48: // record is what the record flows and the pool make of it, as on 0.3.17 + +49: if !kaspa_consensus_core::proving::proof_rule_applies(block.header.daa_score, oracle.active_from()) { + +50: return Ok(()); + +51: } + +52: let Some(coinbase) = block.transactions.first() else { return Ok(()) }; + +53: let carried = kaspa_consensus_core::proving::carried_proofs(&coinbase.payload, block.header.daa_score); + +54: if carried.is_empty() { + +55: return Ok(()); + +56: } + +57: // every proof in parallel: a cold verify is 0.26 to 0.53 s a proof on one core (M5 Max to a 2019 Zen 2) + +58: let verdicts: Vec<_> = self.thread_pool.install(|| { + +59: use rayon::prelude::*; + +60: carried.par_iter().map(|c| (c, oracle.verdict(c))).collect() + +61: }); + +62: let mut missing = Vec::new(); + +63: let mut missing_hashes = Vec::new(); + +64: for (c, v) in verdicts { + +65: match v { + +66: kaspa_consensus_core::proving::ProofVerdict::Verified => {} + +67: kaspa_consensus_core::proving::ProofVerdict::Invalid(why) => { + +68: return Err(RuleError::IgneumInvalidProofRecord(format!("{} (proof {}): {why}", c.label, faster_hex::hex_string(&c.proof_hash[..8])))); + +69: } + +70: kaspa_consensus_core::proving::ProofVerdict::Missing => { + +71: missing.push(format!("{} (proof {})", c.label, faster_hex::hex_string(&c.proof_hash[..8]))); + +72: missing_hashes.push(Hash::from_bytes(c.proof_hash)); + +73: } + +74: } + +75: } + +76: if !missing.is_empty() { + +77: return Err(RuleError::IgneumProofMissing(missing_hashes, missing.join("; "))); + +78: } + +79: Ok(()) + +``` + +### F03 excerpts + +**`v6/igneum-v6-freeze-tree/README-FREEZE.txt:1-1`** +SHA-256: `26e77e724331420c066449bc4ee5e8ff6782a516b5329cb71a6b0fc66bdf8207` + +```text + +1: Frozen generator tree for class v6: master 2e9b3e73 on 8 Oct 2026 (igneum-pow at the freeze sha c245d50b9, fingerprint a65e4c5a; the D1 object cut lands 23:30 BST with the five digests and the spec re-cut). Spec section 01 here is still version 0.2 until that re-cut. + +``` + +**`pow/src/generator.rs:249-277`** +SHA-256: `751f39aeedb2987856343e2a67c7aeca7daa47ddafe5192c50a19b1d425e3a8a` + +```text + +249: /// Class v6 lane 1 (`docs/design/class-v6-rotating-family.md` section 2, the index fold; a research class, + +250: /// 8 October 2026): `true` folds the product's low bits in every era load address before the stride rotation + +251: /// (`verify::load_index`: `y = x * M; y ^= y >> 16; y = rotl(y, R)`), so no era's R lands a biased product bit + +252: /// on an address bit. The class's era carries the same bit ([`EraParams::fold`]). `false` for every other class. + +253: pub fold: bool, + +254: /// Class v6 lane 1, the op-mix re-weight behind the fold: 0 is the plain table [`NONLOAD_WEIGHTS`]; 1 the k lane's + +255: /// optimiser split [`NONLOAD_WEIGHTS_RW`] (sum 83, `or` never drawn); 2 the census lane's neighbouring table + +256: /// [`NONLOAD_WEIGHTS_RW2`] (sum 75). The draw rolls against the table's own sum ([`LoadClass::nonload_weights`]). + +257: pub rw: u8, + +258: /// The 64-register window (the hash lane's reg64 measurement, 8 October 2026, a research class behind `+reg64` + +259: /// and `--reg64`): each lane holds 64 live 32-bit registers. r0..r7 are seeded as today, r8..r63 derived from them + +260: /// (`r[k] = r[k & 7] * 0x9E3779B9 + k`); the 64 drawn instructions run twice per iteration in an interleaved + +261: /// order, instruction i on window 0 (register field + 8 * (i % 4), registers 0..31) then the same instruction on + +262: /// window 1 (+32, registers 32..63); the windows fold into r0..r7 by xor before the hash fold + +263: /// ([`Program::scheduled`], [`crate::verify`], the emitters). The draw, the acceptance rule and the dataset are the + +264: /// class's without the flag. `false` for every other class. + +265: pub reg64: bool, + +266: /// reg64, the full chain (the coordinator's amendment of 8 October 2026, 15:1x UK, class suffix `+reg64c`, + +267: /// `--reg64-chain`): the address of every load consumes all 64 registers: address source `src ^ m`, `m` the + +268: /// rotate-xor chain (`m = first; m = rotl(m, 1) ^ next`) over the 63 registers other than `src` in index order + +269: /// (the same text in the verifier and the emitters), so an in-flight hash holds 64 independently necessary + +270: /// values for the length of the dependent memory chain. The source stays out of the chain: inside it, r31 and + +271: /// r63 land at rotation 0 mod 32 and cancel their own direct term (found by the liveness rule on the pinned draw). + +272: /// Init rule: r0..r7 from the seed words as every class, `r[k] = r[k & 7] * 0x9E3779B9 + k` for k in 8..63. + +273: /// Output rule: `r[k] ^= r[k + 8] ^ r[k + 16] ^ ... ^ r[k + 56]` for k in 0..7, then the class's hash fold. + +274: /// Liveness rule (`accept::check_window_liveness`): xoring any one register with either of two seed-derived + +275: /// probe words at the start of an iteration moves that iteration's first load address and the final hash, in + +276: /// every lane (the complement and a single bit are the patterns a linear fold loses). Requires `reg64`. + +277: pub reg64_chain: bool, + +``` + +**`pow/src/generator.rs:939-954`** +SHA-256: `751f39aeedb2987856343e2a67c7aeca7daa47ddafe5192c50a19b1d425e3a8a` + +```text + +939: /// The program class of an epoch (Counter ASIC 2.0, 5 October 2026, `docs/plans/counter-asic-2-rollout.md`): one + +940: /// height switch in the node, `program_class_v3_activation_daa`, rounded up to an epoch boundary, decides which + +941: /// class an epoch's program is drawn from. V2 is the lottery hash as adopted on 4 October 2026, byte for byte. + +942: /// V3 is generator version 3: its program id carries `generator = 3` and its load class is [`V3_CLASS`]. + +943: /// V4 (Counter ASIC 3.0, 6 October 2026, the candidate `mx8+sh256x27` behind `program_class_v4_activation_daa`) is + +944: /// generator version 4: its program id carries `generator = 4` and its load class is [`V4_CLASS`]. + +945: #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] + +946: pub enum ProgramClass { + +947: #[default] + +948: V2, + +949: V3, + +950: V4, + +951: /// Class v5 (`docs/design/class-v5-stored-state.md`, behind `program_class_v5_activation_daa`): class v4's program + +952: /// over a dataset whose every item is keyed by the window's execution state ([`V5_CLASS`]), generator 5. + +953: V5, + +954: } + +``` + +**`node/consensus/pow/src/igneum.rs:72-95`** +SHA-256: `8264d300db90ee5e4c0f568f3e4f847e9c2c1028312827f38e8458d2c13bedfd` + +```text + +72: /// The chain-derived inputs of the lottery hash for one header. + +73: #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + +74: pub struct EpochSeeds { + +75: /// Seed of the program for this header's epoch + +76: pub epoch: Hash, + +77: /// Seed of the memory-hard cache: days since the Unix epoch of the header timestamp + +78: pub day: u64, + +79: /// The program class of the header's epoch (Counter ASIC 2.0, 5 October 2026): v2, or v3 from the first epoch + +80: /// at or above `Params::program_class_v3_activation_daa`, or v4 (Counter ASIC 3.0) from the first epoch at or + +81: /// above `Params::program_class_v4_activation_daa` (`igneum::program_class_for_epoch`) + +82: pub class: ProgramClass, + +83: /// The era seed of the header's era (`E_n`; the devnet stand-in: genesis for era 0, else the last selected-chain + +84: /// block below `pow_era_seed_score`). Read by class v3 programs only; `ZERO_HASH` where a v2 caller has none. + +85: pub era: Hash, + +86: /// The latency ladder's shadow pass count of the header's epoch (`docs/design/latency-ladder.md`): the rung the + +87: /// chain's step gives the epoch, 0 for the class's own (before the ladder, every class but v4, a caller that knows + +88: /// no rung). Part of the key: two rungs of one epoch are two programs. + +89: pub shadow_reps: u16, + +90: } + +91: + +92: impl EpochSeeds { + +93: /// Class v2 seeds with no era: the shape of every caller before Counter ASIC 2.0 (tests, the legacy seed walk). + +94: pub fn v2(epoch: Hash, day: u64) -> Self { + +95: Self { epoch, day, class: ProgramClass::V2, era: kaspa_hashes::ZERO_HASH, shadow_reps: 0 } + +``` + +**`mining/pool/src/node.rs:47-71`** +SHA-256: `54e52ee3ca45d2b482b2acf066d1ba613fa7fc7aaf7075fc5919d61257a126d4` + +```text + +47: raw.header.vote_key_hash = member.key_hash; + +48: let block: Block = raw.clone().try_into().map_err(|e| format!("block convert: {e}"))?; + +49: let header: Header = block.header.as_ref().clone(); + +50: let info = tmpl.pow_epoch.as_ref().ok_or("the node reports no pow_epoch; a devnet-v4 line node is needed")?; + +51: // The node's PoW schedule, genesis day and dataset size, and class v3 activation are the pool's (what the solo + +52: // miner's `template()` installs): the share verifier's day cache and program must be the node's exactly + +53: let wanted = PowSchedule::clamped(info.epoch_blocks, info.epoch_lead, info.day_ms); + +54: if wanted != pow_schedule() { + +55: install_pow_schedule(wanted); + +56: eprintln!("{} node PoW schedule: {} DAA per epoch, lead {}, day {} ms", now(), wanted.epoch_blocks, wanted.epoch_lead, wanted.day_ms); + +57: } + +58: if (info.genesis_day_index, info.genesis_dataset_log2) != (pow_genesis_day_index(), pow_genesis_dataset_log2()) { + +59: install_pow_genesis(info.genesis_day_index, info.genesis_dataset_log2); + +60: eprintln!("{} node genesis day index {} and genesis dataset 2^{} words: the day cache follows them", now(), info.genesis_day_index, info.genesis_dataset_log2); + +61: } + +62: if info.program_class_v3_activation_daa != program_class_v3_activation_daa() { + +63: install_program_class_v3_activation(info.program_class_v3_activation_daa); + +64: eprintln!("{} node program class v3 activation: {} (epoch {} is class {}, the next epoch class {})", now(), info.program_class_v3_activation_daa, info.epoch_index, info.class().name(), info.next_class().name()); + +65: } + +66: // Counter ASIC 2.0: the class and the era seed of the epoch ride with the template; the verifier hashes the + +67: // program they name, the same one the members' workers compile (6 October 2026: a fixed class here or on the + +68: // member refused every GPU share of the fleet run) + +69: let seeds = EpochSeeds { epoch: info.epoch_seed, day: day_index(header.timestamp), class: info.class(), era: info.era_seed.unwrap_or(kaspa_hashes::ZERO_HASH) }; + +70: let engine = pool.engine.clone(); + +71: let epoch = tokio::task::spawn_blocking(move || engine.epoch_for(&seeds)).await.map_err(|e| e.to_string())?; + +``` + +### F04 excerpts + +**`node/consensus/src/processes/finality.rs:1111-1128`** +SHA-256: `6194a96a80e5ec5a4daeb63afa8fda55a8c7a2f959d5becab60ea733790f5bf4` + +```text + +1111: /// The anchored test of a certificate's signers against the table frozen at lock `C_f` (docs/spec/finality- + +1112: /// guarantees.md 6.2): inside one weight window of the lock, two thirds of `T_f` (rule v3's Q5 and rule v4's + +1113: /// item 1); past the window, under rule v4 with the recovery, strictly more than half of `T_f` (item 2, the + +1114: /// majority-continuity recovery: `2 x signed_f > total_f`); under rule v4 without the recovery (6.5, the pause), + +1115: /// nothing passes past the window; under rule v3 the table has expired by then (`frozen_table` returns None) and + +1116: /// the sliding table alone decides. Pure: (passes, signed_f, total_f, past_the_window). + +1117: pub fn anchored_test(v4: bool, recovery: bool, lock_daa: u64, checkpoint_daa: u64, window: u64, signed_f: u64, total_f: u64) -> (bool, bool) { + +1118: let past = checkpoint_daa >= lock_daa.saturating_add(window); + +1119: if total_f == 0 { + +1120: return (false, past); + +1121: } + +1122: if !past { + +1123: return (FinalityParams::floor_met(signed_f, total_f), past); + +1124: } + +1125: if v4 && recovery { + +1126: return ((signed_f as u128) * 2 > total_f as u128, past); + +1127: } + +1128: (false, past) + +``` + +**`node/consensus/src/processes/finality.rs:2610-2642`** +SHA-256: `6194a96a80e5ec5a4daeb63afa8fda55a8c7a2f959d5becab60ea733790f5bf4` + +```text + +2610: // Rule v3 (F21): the signers also need two thirds of the table frozen at the last locked checkpoint on + +2611: // C_i's chain, at that table's weights, while it stands (less than one window old) + +2612: let v3 = self.v3_active(cp.daa_score); + +2613: let frozen = if v3 { self.frozen_table_with_daa(state, index, cp.hash, cp.daa_score) } else { None }; + +2614: // W7: keys that have left at this checkpoint are out of the frozen denominator too (`frozen_floor`); the + +2615: // sliding table (`voters_at`) already excludes them + +2616: let gone = if frozen.is_some() { self.leaves_at(state, cp.hash, cp.daa_score) } else { HashMap::new() }; + +2617: // rule v4 (6.2): past the window the anchored test is the majority-continuity recovery (with the flag) or + +2618: // nothing (the pause); a lock that passes it past the window is a recovery lock, reported for one window + +2619: let v4 = self.v4_active(cp.daa_score); + +2620: let recovery_window = frozen.as_ref().map(|(_, lock_daa, _)| cp.daa_score >= lock_daa.saturating_add(self.params.weight_window)).unwrap_or(false); + +2621: let frozen_test = |keys: &[Hash]| -> bool { + +2622: match &frozen { + +2623: Some((_, lock_daa, f)) => { + +2624: let (fs, ft) = Self::frozen_floor(f, keys, &gone); + +2625: Self::anchored_test(v4, self.v4_recovery, *lock_daa, cp.daa_score, self.params.weight_window, fs, ft).0 + +2626: } + +2627: None => true, + +2628: } + +2629: }; + +2630: // Ledger C4: a checkpoint locks here only on the chain through the locks this node holds. Fork choice keeps + +2631: // every other chain out of candidacy, so a record on another chain (the node's own determination before a + +2632: // certificate-driven move, or a lock it adopted at a higher index) is determined again after the move (F24), + +2633: // never locked where it is. Without this a side that out-works the certified chain locks it alone once its + +2634: // own last lock is a window old. + +2635: let through_locks = locked || self.off_lock_chain(state, index, cp.hash).is_none(); + +2636: if !through_locks && self.lock_test(signed, active_num, p, table.total) { + +2637: debug!( + +2638: "Finality: checkpoint {} on {} meets the quorum but is not on the chain through this node's locks: not locked here (ledger C4); determined again once the chain moves", + +2639: index, cp.hash + +2640: ); + +2641: } + +2642: let passes = through_locks && self.lock_test(signed, active_num, p, table.total) && frozen_test(&signers); + +``` + +**`dag/igneum-node-dag/spec/docs/spec/finality-guarantees.md:111-136`** +SHA-256: `bd9f640eb0396ec05c22dbb50ecb0446fb9a7397d205e792615374367797776a` + +```text + +111: ### 6.5 The cost, stated, and the one-line alternative + +112: + +113: The recovery certificate's safety bound is weaker than one third. Two conflicting recovery locks need a partition that has lasted a full window with no certificate on either side AND equivocating keys that (i) hold a share of `T_f` exceeding the split's imbalance and (ii) are still in BOTH sides' canonical voter lists at the recovery index, which means they mined at least dust (100 blue blocks in the window, W3) on each side: a certificate's bitmap is over the voter list at `C_i` (C3), so a key that mined on one side only is, after a full window, not a voter on the other side whatever its anchored weight. Each side of a 50/50 split with an equivocator at `a` that mines on both holds `(1 - a) / 2 + a` of `T_f`, more than half for any `a > 0`; in a 60/40 split the 40 side needs `a > 0.2`. Measured (N1b, N2b, five rows each over two seeds): an equivocator mining on one side only never produces a recovery conflict (one side recovers at day 30.00, the other never, 0 conflicts, at 10 and 20 percent across 50/50 and in the 40/40 case); one mining on both sides makes both sides recover at day 30.00 and the heal shows 2,800 to 2,889 conflicting locks under `v4 recovery` and 0 under `v4 pause`; at 34 percent both sides lock from minute 0 under every rule (the one-third bound). Every pre-heal lock kept and the heal locks at 0 minutes in every row. In every such case the equivocator is stripped at the heal (3.6), the history through `C_f` is untouched (item 3), and the pair is handled as 3.11.4 says. The one-third bound of section 4 is intact for every certificate formed within a window of the last one, which is every certificate of a connected network. + +114: + +115: The alternative the founder can choose by deleting item 2 is the **indefinite pause** (`v4 pause` in the simulator, `P.recovery = False`; in the node, the recovery test left out). Its guarantees are strictly stronger: no certificate ever forms with less than two thirds of the last certified table, so the one-third bound holds for every certificate for ever. Its costs, measured: one purchase of keys worth a third of a window is a permanent veto on finality (N4: never in 31 days, against day 30 with the recovery), a sudden honest loss of a third of weight (a pool folding with its keys) pauses finality permanently absent succession or an operator's trusted certificate (N6: never, against day 30), and a 60/40 partition that outlasts a window pauses the 60 side until the heal instead of recovering at day 30 (N1). + +116: + +117: **This document recommends the recovery (items 1 to 5 as written)**, because its failure needs an attacker, a month-long partition and equivocation that the chain then punishes, while the pause's failure needs no attacker and has no exit inside the protocol; and because the recovery never touches certified history, which is what the acceptance line protects. The founder chose "the pause over the fork" on 4 October 2026 (ledger F21) against a fork that needed no attacker; this recommendation keeps that choice (the honest 31-day partition never forks under either variant) and adds a bounded, disclosed exit. The decision is the founder's at the 18:00 UK report; the simulator and the node carry both as one switch. + +118: + +119: ### 6.6 How it composes with the rest of the rule + +120: + +121: - **No certified checkpoint is ever reversed (3.11.4)** stands unchanged: a recovery lock adds a certificate, it never withdraws one; a node holding two valid certificates at one index keeps the first, reports `conflict`, and the protocol does not pick. + +122: - **The certificate-driven reorg (3.5, ledger C4)** carries recovery certificates: a node on the other side of a healed 60/40 partition, holding no lock since `C_f`, verifies the 60 side's recovery certificate against `T_f` and `T(i)` from the block's own past and moves to it. Measured: every pre-heal lock kept, first lock after the heal 0 minutes, 0 post-heal stalls (N1). + +123: - **Succession (W5) and the strip (3.6)** are the two in-protocol ways the anchored table changes without a certificate, both functions of the chain: a miner that retires hands its weight to a successor (which then counts in `T_f` at the old key's weight), and the owner of a compromised key equivocates with it once to remove it from every table (N4: finality resumes the day the evidence is carried). + +124: - **The trusted certificate (F5)** is the operator's exit for the cases no rule covers (half or more of `T_f` gone for good). It gains one check: a configured trusted certificate MUST lie on the chain through every lock the node holds, else it is refused; an operator cannot be handed a certificate that overrides certified history. + +125: - **The exchange guidance (3.9)** gains one row: `finality_reason` `recovered` means a lock formed under 6.2 item 2 within the last window; an operator who prefers the pause-only reading treats it as `paused` for that window. The pause row itself is unchanged: a pause is proof of work, the reorg bound is the finality depth in median time. + +126: - **Layer 4 of class rotation** (the emergency miner vote, `docs/design/class-rotation-four-layers.md`): no flip vote counts while finality is paused, and a recovery lock counts as a lock; the schedule stands throughout (8). + +127: + +128: ### 6.7 The node change + +129: + +130: One function and one switch, for the node lane; nothing here lands on any network until the founder sets the height. `frozen_table` (the Q5 row of 3.10) keeps its reference and loses its drop; `evaluate` and `ingest_off_chain` test `floor_met(frozen_signed, frozen.total)` while `daa(C_i) < daa(C_f) + weight_window` and `continuity_met(frozen_signed, frozen.total)` (`2 x signed > total`, a pure function with its own inclusive-boundary unit test) after, provided no lock exists between; a lock that passed by `continuity_met` is stamped `recovered` in the record and `getFinalityCheckpoints` reports `finality_reason` `recovered` with `anchored_index` and `anchored_signed_share` for one window. Switch `finality_v4_activation_daa` (never on every network until set; the digest arm entered only when set, so a binary carrying the field peers with one that does not, the rule of every switch since 0.3.20). Unit tests, known-failed first: the M3 shape (A at 60 percent and B at 40 percent lock together, B leaves, A alone must not lock under v4 pause ever and must lock under v4 recovery only once the last lock is one window old; under v3 it locks at the window, the known-failed line); and a 50/50 shape that never locks under either v4. The fast-time harness row is `tools/finality-attacks/v3.mjs split50` extended past the window (`SPLIT` above 120 DAA at 60x), where v3 must conflict and v4 must not; the pass line per variant: pause-only, no lock on either side during the split, locking resumed after the heal (a heal window of at least one weight window after the reconnect), 0 conflicting certificates, 0 disagreeing locks; recovery, at most one side locks during the split (the side above half of the anchored table by weight, which block-count jitter decides in a 3/3 split), 0 conflicting certificates, 0 disagreeing locks, every node on the recovering side's chain after the heal. Measured on the 2.0.0 node line on 8 October 2026 (the node lane's rows in `sim/results_v2.md`, "Rule v4"). + +131: + +132: ## 7. What is NOT guaranteed + +133: + +134: 1. **At or above one third of equivocating weight** two valid certificates can exist at one index; the rule reports and does not resolve (3.11.4). + +135: 2. **A recovery lock** is bounded as 6.5 states, not by one third: a month-long partition plus an equivocator outweighing the split's imbalance can produce two recovery locks after `C_f`. The history through `C_f` is never touched. + +136: 3. **A loss of half or more of the last certified table at once** has no in-protocol exit: finality pauses until the weight returns, hands over or is stripped, or an operator configures a trusted certificate on the chain through the last lock (F5 with 6.6's check). The chain runs on proof of work meanwhile. + +``` + +**`dag/igneum-node-dag/spec/docs/spec/finality-guarantees.md:184-197`** +SHA-256: `bd9f640eb0396ec05c22dbb50ecb0446fb9a7397d205e792615374367797776a` + +```text + +184: ## 10. Test table + +185: + +186: Each claim, the scenario, the known-failed line first where there is one, and the result. Simulator rows are `sim/finality_v2.py --scenarios N --seeds 7,11` at the 2/3 floor, each side retargeting and counting only its own blocks, run on build-3 under the lease tool; the tables are in `sim/results_v2.md`, "Rule v4". Where a row says "pending" the run had not landed when this version was written; the 20:00 UK version carries the numbers. + +187: + +188: | Claim | Scenario | Known-failed line | Result | + +189: |---|---|---|---| + +190: | (a) The 31-day partition at the window: no conflicting locks under the chosen rule | N1: 50/50, 60/40, 55/45 for 31 days, v3 against v4 pause and v4 recovery | v3: both sides lock alone at day 30.00, 2,679 to 2,870 conflicts (M3, re-run in N1, reproduced to the checkpoint) | **measured**: v4 pause: no side locks in 31 days, 0 conflicts, every pre-heal lock kept, first lock 0 minutes after the heal, 0 post-heal stalls, every split; v4 recovery: 50/50 the same; 60/40 and 55/45 the larger side recovery-locks once at day 30.00 and then locks normally, the smaller side never, 0 conflicts, kept, heal 0 minutes | + +191: | (a) The recovery bound | N1b: 50/50 for 31 days with a 10, 20, 34 percent equivocator, mining on one side and on both | 34 percent: conflicts from minute 0 under every rule (the one-third bound: 87,428 to 87,915 conflicts in 31 days) | **measured** (build-4, 16:06 to 16:16 UK): on one side only, 0 conflicts (one side recovers at day 30.00, the other never); on both sides, both recover at day 30.00 and 2,800 to 2,889 conflicts under v4 recovery, 0 under v4 pause; every pre-heal lock kept, heal 0 minutes | + +192: | (b) 40/40/20 under the active-set rules | N2: honest three-way for 150 minutes and 31 days; 40/40 with a 20 percent equivocator reaching both for 31 days; N2b the same equivocator mining on both sides | the 60/60 case with the equivocator mining on both sides under v4 recovery at day 30 (6.5) | **measured** (N2): honest three-way: no side locks for 150 minutes or 31 days under either v4, 0 conflicts, kept, heal 0 minutes; 60/60 with the equivocator mining on one side: v4 pause never, v4 recovery one side at day 30.00, the other never, 0 conflicts; N2b **measured**: the same equivocator mining on both sides: v4 pause 0 conflicts, v4 recovery both sides at day 30.00 with 2,835 to 2,860 conflicts (the bound of 6.5) | + +193: | (c) Signing stops while mining continues | N3: 34, 40, 45 percent for 24 h under v4 recovery; 34 percent for 31 days under both v4 | none expected (as J) | **measured**: 24 h: every checkpoint stalled (2,880 to 2,889), longest gap 1,440 minutes, first lock 0 minutes after the resume, 0 post-resume stalls, 0 conflicts, 0 recovery locks; 31 days at 34 percent: no lock under either v4 for the whole silence (89,286 to 89,318 stalled), first lock 0 minutes after the resume, 0 conflicts; under v4 recovery that first post-resume lock passed by the majority test (the anchored checkpoint was a window old), so a node would report `recovered` for one window after a resume that follows a pause longer than a window, a reporting fact and not a weakening | + +194: | (d) Old keys compromised against fresh hashrate | N4: keys worth 40 percent withhold, holder at 30 percent of hashrate, 31 days, v2, v3, v4 pause, v4 recovery; the self-strip on day 1 | v4 pause: never (the permanent veto, 6.5) | **measured**: first lock v2 day 20.9, v3 day 30.00, v4 pause never in 31 days (89,262 to 89,373 stalled), v4 recovery day 30.00 (one recovery lock); the holder's share decays to 0.300 under every rule; self-strip: the first lock on day 0 (571 to 736 stalled checkpoints before the evidence is carried), 0 conflicts | + +195: | (e) Finality paused across an epoch boundary: seeds advance, mining continues, certified history intact | derived from N1 and N3: blocks and checkpoints continue through the pause (every stalled checkpoint in the tables is a block the chain mined), 744 hourly boundaries in a 31-day pause, each seeded by its reference block (8); the fast-time harness row (11) | none | derived; harness run owed | + +196: | (f) The heal: a deterministic path that never reverses a lock | N1, N2, N6: every pre-heal lock kept, first lock after the heal, post-heal stalls; the certificate-driven reorg (3.11.7, `c4.mjs`) | none | **measured**: every pre-heal lock kept in every row of N1 and N2 (24 runs), first lock 0 minutes after every heal, 0 post-heal stalls, 0 conflicts under both v4 rules; a sudden departure of 35 percent: v3 and v4 recovery lock at day 30.00, v4 pause never (N6); at 50 percent the recovery is a knife edge (one seed day 30.23, one never) | + +197: | The harness line on real nodes: the known-failed v3 partition past the window | `tools/finality-attacks/v3.mjs split50`, 0.3.25 node pair, 60x file, SPLIT 420 s (the frozen table expires 240 s after the last lock) | both sides lock alone, conflicting certificates at the heal, disagreeing locks | **measured** (build-4, 17:06 to 17:20 UK): both sides locked alone 192 to 198 s after the cut, 7 new locks each; 4/8/8 conflicting certificates logged, 8 disagreeing locked indices after the heal, locking resumed on both forks: FAIL by the scenario's criterion, the known-failed line; the v4 line waits on the node change (6.7) | + +``` + +### F05 excerpts + +**`pow/src/verify.rs:674-692`** +SHA-256: `033ae9f2ccd32e1170e7ff4f259b206b26e78ddc9ebf68d7b26933794aa1f345` + +```text + +674: // reg64 full chain: a load's address source is src ^ m, m the rotate-xor chain over the 63 other registers in + +675: // index order (m = first; m = rotl(m, 1) ^ next). The source itself stays out of the chain: with it inside, a + +676: // register whose term lands at rotation 0 mod 32 (r31, r63) cancels its own direct term, a dead register the + +677: // liveness rule found on the pinned draw (first load src r31, 8 October 2026, 15:5x UK). + +678: let addr_src = |r: &[[u32; LANES]], lane: usize| -> u32 { + +679: if !address_mix { + +680: return r[a][lane]; + +681: } + +682: let mut m = 0u32; + +683: let mut started = false; + +684: for k in 0..r.len() { + +685: if k == a { + +686: continue; + +687: } + +688: m = if started { m.rotate_left(1) ^ r[k][lane] } else { r[k][lane] }; + +689: started = true; + +690: } + +691: r[a][lane] ^ m + +692: }; + +``` + +**`pow/src/generator.rs:258-277`** +SHA-256: `751f39aeedb2987856343e2a67c7aeca7daa47ddafe5192c50a19b1d425e3a8a` + +```text + +258: /// The 64-register window (the hash lane's reg64 measurement, 8 October 2026, a research class behind `+reg64` + +259: /// and `--reg64`): each lane holds 64 live 32-bit registers. r0..r7 are seeded as today, r8..r63 derived from them + +260: /// (`r[k] = r[k & 7] * 0x9E3779B9 + k`); the 64 drawn instructions run twice per iteration in an interleaved + +261: /// order, instruction i on window 0 (register field + 8 * (i % 4), registers 0..31) then the same instruction on + +262: /// window 1 (+32, registers 32..63); the windows fold into r0..r7 by xor before the hash fold + +263: /// ([`Program::scheduled`], [`crate::verify`], the emitters). The draw, the acceptance rule and the dataset are the + +264: /// class's without the flag. `false` for every other class. + +265: pub reg64: bool, + +266: /// reg64, the full chain (the coordinator's amendment of 8 October 2026, 15:1x UK, class suffix `+reg64c`, + +267: /// `--reg64-chain`): the address of every load consumes all 64 registers: address source `src ^ m`, `m` the + +268: /// rotate-xor chain (`m = first; m = rotl(m, 1) ^ next`) over the 63 registers other than `src` in index order + +269: /// (the same text in the verifier and the emitters), so an in-flight hash holds 64 independently necessary + +270: /// values for the length of the dependent memory chain. The source stays out of the chain: inside it, r31 and + +271: /// r63 land at rotation 0 mod 32 and cancel their own direct term (found by the liveness rule on the pinned draw). + +272: /// Init rule: r0..r7 from the seed words as every class, `r[k] = r[k & 7] * 0x9E3779B9 + k` for k in 8..63. + +273: /// Output rule: `r[k] ^= r[k + 8] ^ r[k + 16] ^ ... ^ r[k + 56]` for k in 0..7, then the class's hash fold. + +274: /// Liveness rule (`accept::check_window_liveness`): xoring any one register with either of two seed-derived + +275: /// probe words at the start of an iteration moves that iteration's first load address and the final hash, in + +276: /// every lane (the complement and a single bit are the patterns a linear fold loses). Requires `reg64`. + +277: pub reg64_chain: bool, + +``` + +### F06 excerpts + +**`pow/src/accept.rs:114-119`** +SHA-256: `a4baf9286508e73406dd05b9f89bc6660baf017f107c650ce09bda504cfc92f9` + +```text + +114: /// The reg64 window's liveness rule (the coordinator's spec of 8 October 2026, `check_window_liveness`, a research + +115: /// class, not wired into the acceptance): xoring register `reg` of lane `lane` with a probe word at the start of iteration 0 + +116: /// left the iteration's first load address unchanged (`address_changed` false) or the final hash unchanged + +117: /// (`result_changed` false). A window whose address fold reads a subset of the registers is refused here. + +118: DeadWindowRegister { reg: u8, lane: u8, address_changed: bool, result_changed: bool }, + +119: /// `check_window_liveness` on a program without the reg64 window. + +``` + +**`pow/src/accept.rs:625-637`** +SHA-256: `a4baf9286508e73406dd05b9f89bc6660baf017f107c650ce09bda504cfc92f9` + +```text + +625: // Class v4 sub-version 3 (AP-F8-3, 7 October 2026): the acceptance interpreter runs the latency-shadow block + +626: // after instruction 63 of every iteration, `reps` times with the iteration's `sel`, exactly as the hash does + +627: // (verify.rs). Until this commit it ran the 64 base instructions only, so every dynamic test (c) judged a class v4 + +628: // program the chain never hashes. The shadow block holds no load, so its instructions take the same arms. + +629: let shadow_reps = p.shadow_reps(); + +630: for it in 0..ITERATIONS { + +631: let sel = r[0]; + +632: let shadow_pass = (0..shadow_reps).flat_map(|_| p.shadow.iter().enumerate().map(|(k, i)| (INSTR_COUNT + k, i))); + +633: for (k, ins) in p.instrs.iter().enumerate().chain(shadow_pass) { + +634: let d = ins.dst as usize; + +635: let a = ins.src as usize; + +636: match ins.op { + +637: Op::Scratch => { + +``` + +**`pow/src/accept.rs:867-871`** +SHA-256: `a4baf9286508e73406dd05b9f89bc6660baf017f107c650ce09bda504cfc92f9` + +```text + +867: /// The whole rule: (a), (b), then (c). + +868: pub fn check(p: &Program) -> Result { + +869: check_static(p)?; + +870: check_dynamic(p) + +871: } + +``` + +**`pow/src/generator.rs:258-264`** +SHA-256: `751f39aeedb2987856343e2a67c7aeca7daa47ddafe5192c50a19b1d425e3a8a` + +```text + +258: /// The 64-register window (the hash lane's reg64 measurement, 8 October 2026, a research class behind `+reg64` + +259: /// and `--reg64`): each lane holds 64 live 32-bit registers. r0..r7 are seeded as today, r8..r63 derived from them + +260: /// (`r[k] = r[k & 7] * 0x9E3779B9 + k`); the 64 drawn instructions run twice per iteration in an interleaved + +261: /// order, instruction i on window 0 (register field + 8 * (i % 4), registers 0..31) then the same instruction on + +262: /// window 1 (+32, registers 32..63); the windows fold into r0..r7 by xor before the hash fold + +263: /// ([`Program::scheduled`], [`crate::verify`], the emitters). The draw, the acceptance rule and the dataset are the + +264: /// class's without the flag. `false` for every other class. + +``` + +**`v6/igneum-v6-freeze-tree/docs/analysis/class-v6/census-packs.md:8-22`** +SHA-256: `1aa41baecf0eb2683dbd7392fcc827dc69aa1cc72f1f751803081bf1c9b21885` + +```text + +8: |---|---|---|---|---|---|---|---| + +9: | hl-v6-fold (the index fold alone, W = 4, base mix) | 482dc0dad937135b, attempt 1 | class-v6-fold 7880bc96 | class-v6-census-fold 5b3486f0 | accepted; min site ratio 0.99986; bucket +5.25 sigma; worst free bit 2.84 sigma; no site over 6 sigma (class v5's own program on the same state: -448 sigma at one site) | 256 of 256, 0 exhausted, r 0.701, mean attempt 2.34, max 14, (c''') 0.35 percent | 16 seeds p18 to p33 at 2^22: 16 PASS, at most 1.0455x of the window model (the class v5 control on the same seeds: 5 of 16 flagged on the 6-sigma bucket); the known-failed set at 2^24: p4 1.0057x (from 1.2163x), p8 1.1663x (+6.6 sigma bucket; from 1.3787x), p10 1.1868x (from 1.5052x), p15 PASS, p212 1.0411x (+27 sigma; from 1.1917x), p225 1.2414x BEYOND (from 1.2457x: the value-level class, unmoved), p34 1.0486x with a +11.9 sigma bucket (from +5.06: the one regression) | **PASS** | + +10: | hl-v6-rw (the k lane's table rw1: 16,14,4,12,4,11,10,2,10,0 in draw order, sum 83, `or` never drawn) | 30628f8adcf6035e, attempt 0 | class-v6-fold 7880bc96 | the same | accepted; min ratio 0.99990; bucket +5.5; worst bit 2.89; no site over 6 sigma | 256 of 256, 0 exhausted, r 0.117, mean attempt 0.13, max 2, (c''') 0.34 percent | 16 seeds: ratio at most 1.1526x (within), 4 of 16 flagged on the 6-sigma bucket (the control's own rate is 5 of 16); known-failed under this table's draw: p4, p34, p225 PASS, p8 +22 sigma, p10 +14, p15 +8.8, p212 +37 sigma buckets at ratios 1.00 to 1.15x | **PASS** | + +11: | hl-v6-foldrw (fold and rw1) | 605d06cabc489f94, attempt 0 | class-v6-fold 7880bc96 | the same | accepted; min ratio 0.99993; bucket +5.75; worst bit 3.49; no site over 6 sigma | 256 of 256, 0 exhausted, r 0.117, mean 0.13, max 2, (c''') 0.34 percent | 16 seeds: 15 PASS, 1 flagged (p18), at most 1.0932x; known-failed: every ratio within (p4 1.0002x, p8 1.0138x, p10 1.0100x, p15 1.0088x, p34 1.0121x, p212 1.1111x, p225 1.0000x), buckets flagged on p15 (+9.3) and p212 (+24.5) | **PASS** | + +12: | hl-v6-rw2 (the census lane's table 13,11,6,10,8,8,7,2,6,4, sum 75) | 09e91b0dcd458c77, attempt 1 | class-v6-fold 7880bc96 | the same | accepted; min ratio 0.99990; two sites with the stride bit at -64 sigma (no fold) | 256 of 256, 0 exhausted, r 0.410, mean 0.70, max 8, (c''') 1.15 percent | 16 seeds: seed p30 1.3111x over the window model (the control 1.0004x; hl-v6-rw's worst 1.1526x), BEYOND the 1.2x gate; known-failed under its draw: p8 1.2507x (+42.8), p10 1.5044x (+100.9), p225 1.4049x beyond, p34 +70.7 and p15 +13.2 sigma buckets, p4 and p212 PASS | **FAIL** on the F8 line | + +13: | hl-v6-win (the 64-register full-chain window alone, `+reg64c`) | f42d4a743ce7d4fa, attempt 0 (the v5-dn3-epoch0 seeds and state) | reg64-v5 198d171d | class-v6-census-reg64 b29e690d2 | accepted (with the liveness probe); min ratio 0.99923; bucket +5.5; the base program's stride-bit site at -448 sigma (no fold) | 256 of 256, 0 exhausted, r 0.716, mean 2.52, max 15, (c''') 1.66 percent (the window does not enter the draw: class v5's rows) | the window's own address stream through the interpreter's tracing probe, 32 sites of the interleaved schedule, 16 seeds at 2^22, the era laid over, closed form: per-site distinct ratio 1.0026 to 1.0027 on every site (the (c'') form; the control 0.9946 to 1.0027); the item histogram's top-0.1-percent share 1.0645 to 1.5624x of a flat control against the control's 1.0738 to 1.5074x, paired by seed 0.968 to 1.036x of the control. The attack-f8 mirror and the known-failed set do not apply (eight registers) | **PASS** (the live-dataset point owed) | + +14: | hl-v6-all (window, fold and rw1) | 9d40978601a7df2a, attempt 0 | class-v6 3f25a8305 (the texts at c245d50b9, the verifier unchanged) | class-v6-census-all 2d54a4633 | accepted; min ratio 0.99993; bucket +5.75; worst bit 3.49; no site over 6 sigma | 256 of 256, 0 exhausted, r 0.117, mean 0.13, max 2, (c''') 0.34 percent | the trace on 32 sites: per-site distinct ratio 1.0026 to 1.0027; item share 1.0443 to 1.5132x flat against the foldrw control's 1.0398 to 1.4616x, paired 1.004 to 1.035x | **PASS** (the live-dataset point owed) | + +15: + +16: The controls: the freeze's class v5 pack v5-dn3-epoch0 (e5a4ac5978462156) through the whole harness at 15:53 UK (the dry run: the known-failed set reproduces the record to three places, p4 1.2163x, p8 1.3787x, p10 1.5052x, p212 1.1917x, p225 1.2457x) and class v5 on the node1 state over the same 16 seeds at 2^22 (16 of 16 within 1.2x, worst 1.0698x; 5 of 16 flagged on the 6-sigma windowed bucket: the statistic's own rate on the family at 2^22, so a pack's 4 or 5 flags is the control's and a pack's 0 of 16 is a gain). + +17: + +18: What the sheet means. The index fold does what it was drawn for: the stride-bit bias is gone from every program it ships, the F8 tails of 1.22 to 1.50x come inside the gate (1.01 to 1.19x) and the bucket concentration at narrow-window sites falls from the control's 5 of 16 seeds to 0 of 16; p225's value-level class is untouched and p34 gains one bucket statistic. The k lane's table (rw1, `or` never drawn, `mul` at 4) reads clean and gives the lowest rejection rate measured on the family (0.117); the census lane's table (rw2, `or` 4, `mul` 6) keeps the lossy writers the tails come from and fails the F8 line, so the re-weight is sound in the rw1 form only. The window changes nothing the rule or the F8 form sees at 2^22 on the closed form and carries the fold's and the table's rows unchanged; its value is the chip-side cost the adversary lane prices. + +19: + +20: ## 1. The harness + +21: + +22: The class v5 crate of each pack's branch plus lane D's family-gate harness diff (`docs/analysis/class-v6/logs/harness-family-gate-v5-3dc3117c.diff`: `IGNEUM_FAMILY_GATE` widens the class v4 rules to the family's shapes, with every new class flag set aside in `is_family_shape`: state, fold, rw, wide8, reg64, reg64_chain), plus: a `sitestats` command (the whole rule with (c'''), then per site over 2^20 evaluations the distinct ratio against the window model, the largest 256-item bucket in sigma, the largest index-bit excess in sigma over the free bits); the `accept` walk at the class's own cap under the flag; the attack-pass lane's `attack-f8` with `--load-class ` (the program drawn from a class string with the spec's era laid over it) and `--dataset-words N` (the ds55 geometry through `load_index_geom`; not exercised: a state class refuses the non-power-of-two count); the uniform trace tool `tools/attack/v6-census/uniform` on `verify::Probe { trace_loads }` (every load's index per lane through the interpreter itself). Binaries pinned per crate under `/srv/builds/v6-census/bin//` with sha256: fold-5b3486f0 (igneum-pow 50903d30, attack-f8 9ba2210b), reg64-1b676975 (v6census-uniform b2214265 after the fixes), all-d7d441be (igneum-pow 5a221e56, v6census-uniform 2ebd92c1). + +``` + +**`v6/igneum-v6-freeze-tree/docs/analysis/class-v6/census-packs.md:40-49`** +SHA-256: `1aa41baecf0eb2683dbd7392fcc827dc69aa1cc72f1f751803081bf1c9b21885` + +```text + +40: ## 4. Faults found and fixed on the way (the record, so no one repeats them) + +41: + +42: - The uniform tool's distinct sets as HashSet took 16 GB a thread at 2^22 nonces and were killed by the lease's memory cap; bit vectors over the dataset's words now. + +43: - The reg64 interleaved schedule runs 32 load rows per iteration (instruction k on window 0 then window 1); the first read mapped them onto 16 sites (doubling N, ratios 1.35 to 1.80), the second mapped site s to load s mod 16 (the wrong window's expectation, ratios 0.84); site s is load instruction s / 2. + +44: - The all pack's attack-f8 chain died at start on the fold-base tool (`+reg64c` unparsed); the class-v6 tool parses it but its mirror interprets eight registers, so the mirror is not the window's instrument. + +45: - The lease pool is a race, not a queue; `env VAR="a b"` through it splits on spaces. + +46: + +47: ## 5. Owed + +48: + +49: The live-dataset F8 point at 2^24 for the two window packs (the mirror would need the two-window interpreter: four to six hours); the 64 x 2^24 point per pack (45 to 100 core-hours each); hl-v6-rw's files (on build-3, unreachable); the exact `or = 0` attempts rows are lane D's. + +``` + +### F07 excerpts + +**`v6/igneum-v6-freeze-tree/docs/analysis/class-v6/coexist-rows.md:17-37`** +SHA-256: `1c3751990121cd258dc0d816c0d4339af9d872487687899258629944e897dc9f` + +```text + +17: RESULT idle mem_mib=1 power_w=21.32 + +18: RESULT cmd miner_alone: /root/coex/kit/bin/linux/igneum-worker-cuda --bench --pack /root/coex/kit/packs/ds55 --batches 100 --batch-log2 24 --block-warps 1 --device 0 + +19: RESULT miner_alone rc=0 wall_s=67 peak_mib=6129 check=PASS fingerprint=23ced07a4d28b465 mhs=26.824 self-test PASS + +20: RESULT cmd proof_alone: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=67108864 timeout 900 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode compressed --shard 0 --out /root/coex/proof-alone.json + +21: RESULT proof_alone rc=0 wall_s=31 peak_mib=7525 prove_s=13.2 verified=1 err="" + +22: RESULT proof_alone_line RESULT compressed shard 0: prove 13.2 s, proof 1272897 bytes, verify 0.110 s, VERIFIED; statement 0x6adcc7fab21512b58cfa3778756718fd37aea6cccb311e12d2ca33b5f4bf10c0 proof sha256 0x57e133da05ea5f6ecbcbd731b1b26c9bed2f10c5a1aa1bd04a5a5e3dab1e + +23: RESULT cmd miner_beside_comp26: /root/coex/kit/bin/linux/igneum-worker-cuda --bench --pack /root/coex/kit/packs/ds55 --batches 400 --batch-log2 24 --block-warps 1 --device 0 + +24: RESULT cmd proof_beside_comp26: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=67108864 timeout 600 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode compressed --shard 0 --out /root/coex/proof-beside-comp26.json (started 1s+10s after the miner, miner resident mem_mib=6129) + +25: RESULT proof_beside_comp26 mode=compressed thr=67108864 rc=1 wall_s=34 proof_window=1791472334..1791472368 miner_start=1791472323 prove_s= verified=0 miner_alive_after=yes err="thread 'tokio-rt-worker' (1790) panicked at slop/crates/alloc/src/raw_buffer.rs:271:9:" + +26: RESULT miner_beside_comp26 rc=0 wall_s=256 peak_mib=11893 check=PASS fingerprint=23ced07a4d28b465 mhs=26.512 self-test PASS + +27: RESULT beside_fallback compressed 2^26 beside the miner did not verify; core 2^25 beside the miner next + +28: RESULT cmd proof_beside_core25: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=33554432 timeout 600 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode core --shard 0 --out /root/coex/proof-beside-core25.json (started 3s+10s after the miner, miner resident mem_mib=6129) + +29: RESULT proof_beside_core25 mode=core thr=33554432 rc=1 wall_s=18 proof_window=1791472598..1791472616 miner_start=1791472585 prove_s= verified=0 miner_alive_after=yes err="Error: unknown mode core" + +30: RESULT miner_beside_core25 rc=0 wall_s=254 peak_mib=11013 check=PASS fingerprint=23ced07a4d28b465 mhs=26.775 self-test PASS + +31: RESULT beside_failed core 2^25 beside the miner did not verify either + +32: RESULT idle_after mem_mib=1 + +33: RESULT window_hl-reg64c rc=0 wall_s=130 peak_mib=1521 regs=87 blocks_per_sm=16 check=PASS fingerprint=4e7cc25967eba280 mhs=13.467 self-test PASS + +34: RESULT window_hl-reg64 rc=0 wall_s=128 peak_mib=1521 regs=104 blocks_per_sm=16 check=PASS fingerprint=70e786af1a457653 mhs=13.476 self-test PASS + +35: ``` + +36: + +37: Watts from the samples (busy mean of power.draw from 8 s in): miner alone 117.4 W; proof alone 122.2 W over the 9 busy seconds; miner with the failed compressed attempt beside it 118.6 W; hl-reg64c 120.8 W; hl-reg64 119.3 W. The card touched its 170 W limit on none of them. + +``` + +**`v6/igneum-v6-freeze-tree/docs/analysis/class-v6/coexist-rows.md:60-86`** +SHA-256: `1c3751990121cd258dc0d816c0d4339af9d872487687899258629944e897dc9f` + +```text + +60: The rerun on the patched server (run2.log): + +61: + +62: ``` + +63: RESULT start 2026-10-08T15:31:00Z card=NVIDIAGeForceRTX4060 total_mib=8188 driver=570.211.01 power_limit_w=115.00 + +64: RESULT bins worker=d43be4625b78baf7 host=71bc2438856bb141 server=882bd34f7f69dc97 fixture=20a108f159c61ff9 + +65: RESULT cmd proof_alone: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=67108864 timeout 900 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode compressed --shard 0 --out /root/coex/proof-alone-rerun.json + +66: RESULT proof_alone_rerun rc=0 wall_s=15 peak_mib=7532 prove_s=8.2 verified=1 err="" + +67: RESULT proof_alone_rerun_line RESULT compressed shard 0: prove 8.2 s, proof 1272897 bytes, verify 0.036 s, VERIFIED; statement 0x6adcc7fab21512b58cfa3778756718fd37aea6cccb311e12d2ca33b5f4bf10c0 proof sha256 0x7b887af9426039e61345f11d2c9faf10d4b4d0d16e4850d10ee08c5fa7002 + +68: RESULT cmd proof_beside_comp26: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=67108864 timeout 600 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode compressed --shard 0 --out /root/coex/proof-beside-comp26-rerun.json (started 3s+10s after the miner, miner resident mem_mib=6116) + +69: RESULT proof_beside_comp26 mode=compressed thr=67108864 rc=1 wall_s=5 proof_window=1791473509..1791473514 miner_start=1791473495 prove_s= verified=0 miner_alive_after=yes err="thread 'tokio-rt-worker' (10627) panicked at slop/crates/tensor/src/inner.rs:51:51:" + +70: RESULT miner_beside_comp26 rc=0 wall_s=359 peak_mib=7811 check=PASS fingerprint=23ced07a4d28b465 mhs=18.833 self-test PASS + +71: RESULT beside_fallback compressed 2^26 beside the miner did not verify (the core 2^25 beside row is in run.log) + +72: RESULT idle_after mem_mib=2 + +73: ``` + +74: + +75: ## The table + +76: + +77: | Card | Miner alone (ds55) | Proof alone (compressed 2^26) | Together | Register windows (v5 kit worker) | + +78: |---|---|---|---|---| + +79: | RTX 3060 12 GB | 26.82 MH/s at 117.4 W, 6,129 MiB resident, fingerprint 23ced07a4d28b465, PASS | 13.2 s, VERIFIED, peak 7,525 MiB, 122 W | 6,129 + 7,525 = 13,654 MiB against 12,288: TIME-SHARING NEEDED. Live attempt: the card filled to 11,893 MiB and the prover died in a device allocation (raw_buffer.rs:271) after 34 s; the miner held 26.51 MH/s through it (1.2 percent under alone). Proof with the miner paused = the proof-alone row | hl-reg64c 13.47 MH/s, 87 regs, 16 of 24 blocks per SM, fingerprint 4e7cc25967eba280 MATCH, 120.8 W; hl-reg64 13.48 MH/s, 104 regs, 16 of 24, 70e786af1a457653 MATCH, 119.3 W | + +80: | RTX 4060 8 GB | 18.84 MH/s, 6,116 MiB resident, fingerprint 23ced07a4d28b465, PASS (no watts: host sensor N/A) | 8.2 s, VERIFIED, peak 7,532 MiB | 6,116 + 7,532 = 13,648 MiB against 8,188: TIME-SHARING NEEDED. Live attempt: the server died in a tensor allocation (inner.rs:51) at 7,811 MiB after 5 s; the miner held 18.83 MH/s | hl-reg64c 9.51 MH/s, 87 regs, 20 of 24 blocks per SM, fingerprint MATCH; hl-reg64 9.57 MH/s, 104 regs, 16 of 24, MATCH | + +81: + +82: Not measured and why: a core-only proof beside the miner (igneum-prove-host-0317 has no `--mode core`: "Error: unknown mode core"; its modes are native, execute, shard, compressed, block, all); watts on the 4060 (the host's power sensor reads N/A). + +83: + +84: ## Reconciliation with the served row + +85: + +86: The served sentence (litepaper, "Proving", from `docs/analysis/prover-tiers-real-cards.md`, 6 October 2026) says an RTX 3060 (12 GB) "mines at 23.78 MH/s and proves the v1 shard beside its miner at an 8.9 GB peak in 37.5 s". Today's row on the same card tier reads a 7.5 GiB compressed peak that kills the prover beside a 6.1 GiB miner. The two are not in conflict; they measured different things. The 6 October row is the matrix's `miner-comp-26-v1` point (`tools/fleet/box-matrix.sh` section 7): the patched server at threshold 2^26 in compressed mode, driven by the matrix host (the segment host build at `/opt/igneum-segal/.../igneum-prove-host`, which also carries `--mode core`; the matrix's core rows come from it), beside `igneum-miner mine` on the 1 GiB class v3 pack, whose resident set was 1.4 GB: 1.4 + 7.5 = 8.9 GB, inside 12 GB, proof in 37.5 s with the miner running. Today's row is igneum-prove-host-0317 in compressed mode at the same threshold 2^26 (the same 7.5 GiB own footprint, 7,525 to 7,532 MiB peak alone), beside the ds55 miner on the 5.5 GiB dataset of the genesis floor, whose resident set is 6.1 GB: 6.1 + 7.5 = 13.6 GB, outside 12 GB and 8 GB alike, so the prover's allocation fails while the miner keeps mining. What changed between the rows is the miner's dataset (1 GiB then, 5.5 GiB now), not the prover. The rule that follows: at the 5.5 GiB floor a compressed shard proof beside a running miner needs a 16 GB card (13.6 GB together; the 16 GB tier's own peak is 18 GB on the stock sizes and 7.8 GB patched, so 16 GB holds both with about 2 GB spare); 8 GB and 12 GB cards time-share, proving with the miner paused (13.2 s on the 3060, 8.2 s on the 4060) and mining otherwise. The 6 October beside rows stand only for the 1 GiB dataset; the 6 October core-only beside rows (5.6 GB own on the 3060, 27.2 s) stand only for core mode on the segment host, which the 0317 host does not expose, and are not a coexistence claim at the floor. The served sentence is read as a 1 GiB-dataset row until the site lane rewrites it against this record. + +``` + +### F08 excerpts + +**`proving/igneum-proving/docs/analysis/proving-pipeline-2026-10-08.md:7-16`** +SHA-256: `9f96b76b32e6647b83fa8f751d695f581020b603bedadd2d515578937fb2a7fa` + +```text + +7: ## The window and its limit + +8: + +9: The measurement window is the whole of 8 October's proving on the rented fleet, 07:00:26Z (first claim) to 14:27:15Z (last claim), read + +10: from every prover box's own log after Devnet 3 was turned off at 15:34Z. Paid work exists only between 07:46:55Z and 10:43:10Z: 93 paid + +11: segments, 172.88 IGN. From 11:45Z the chain stalled at the class v5 crossing and then partitioned (every solo branch carried old-object + +12: blocks, the network restarted from the stall sink at 15:27Z and was turned off at 15:34Z), so every segment claimed after 11:45Z was + +13: submitted into a chain that never paid it. The hold's declared workload (150 tx/s) ran 11:42Z to 12:23Z with inclusion, then without, so + +14: no paid shard carries a hold transaction: the stage columns below are the fleet's proving of the chain's own blocks, under the pre-stall + +15: load (the DEX and faucet lanes, the hold's earlier steps), on the 0.3.24 node (5b673577). The window asked for, two hours under the hold, + +16: does not exist in the record; this is the honest substitute, and the instrument is in place for the next chain. + +``` + +**`proving/igneum-proving/docs/analysis/proving-pipeline-2026-10-08.md:35-84`** +SHA-256: `9f96b76b32e6647b83fa8f751d695f581020b603bedadd2d515578937fb2a7fa` + +```text + +35: ## Stage columns, seconds, every segment that reached the stage + +36: + +37: | stage | n | median | slowest 5 % | slowest 1 % | max | + +38: |---|---|---|---|---|---| + +39: | inputs (claim to export and cuts done) | 2,447 | 2.4 | 7.5 | 10.1 | 14.7 | + +40: | proving (shard proofs) | 2,453 | 26.8 | 216.1 | 364.7 | 1,104.7 | + +41: | aggregation (segment chain) | 2,453 | 22.8 | 44.2 | 96.4 | 156.6 | + +42: | verification and shard-record submission | 2,453 | 0.0 | 2.0 | 2.0 | 10.0 | + +43: | segment-record submission | 1,909 | 0.0 | 1.0 | 1.0 | 1.0 | + +44: | claim to submitted (end to end) | 1,909 | 75.1 | 230.3 | 301.6 | 497.4 | + +45: | inclusion and payment (submitted to paid) | 93 | 171.0 | 543.0 | 31,397 | 31,470 | + +46: | claim to paid | 91 | 336.0 | 720.0 | 1,098 | 1,240 | + +47: | peak GPU memory during the chain, MiB | 2,453 | 11,948 | 21,174 | 25,788 | 26,210 | + +48: + +49: The two 31,000-second payments are segments submitted before the 02:4xZ pause and paid when the chain resumed; without them the + +50: inclusion-and-payment p99 is 902 s. The assignment wait is not in the table (see the instrument row). + +51: + +52: ## Paid segments per tier + +53: + +54: | tier | paid segments | IGN | proving median s | aggregation median s | payment median s | payment p95 s | + +55: |---|---|---|---|---|---|---| + +56: | RTX 4090 | 48 | 89.33 | 117.3 | 20.1 | 177.5 | 649 | + +57: | RTX 3090 | 34 | 59.66 | 69.8 | 75.0 | 166.0 | 543 | + +58: | L40S | 10 | 21.86 | 67.0 | 18.7 | 125.0 | 370 | + +59: | RTX 6000 Ada | 1 | 2.03 | 20.2 | 18.4 | 116.0 | 116 | + +60: | RTX 3060 (12 GB) | 0 | 0 | | | | | + +61: + +62: The 3090's aggregation median (75 s) is three times the 4090's: the segment chain is memory-bound and the 3090 pays for it. The 4090's + +63: proving median on paid segments (117 s) is above the all-segment median (27 s) because paid segments are the long ones (the short ones + +64: were taken by a faster claimant, below). + +65: + +66: ## Outcomes per tier and wasted work + +67: + +68: | tier | claimed | paid | stolen | refused | submitted, never paid | claimed, never submitted | work s paid | work s wasted | + +69: |---|---|---|---|---|---|---|---|---| + +70: | RTX 4090 | 2,515 | 48 | 98 | 93 | 1,322 | 959 | 7,025 | 183,524 | + +71: | RTX 3060 12 GB | 313 | 0 | 0 | 0 | 34 | 280 | 0 | 6,765 | + +72: | L40S | 273 | 10 | 25 | 28 | 147 | 63 | 1,196 | 26,026 | + +73: | RTX 3090 | 263 | 34 | 8 | 30 | 152 | 41 | 6,484 | 34,855 | + +74: | RTX 6000 Ada | 45 | 1 | 6 | 0 | 26 | 12 | 57 | 3,055 | + +75: + +76: - "submitted, never paid" (1,681 segments) is the partition: records accepted into a chain that never settled them after 11:45Z. It is + +77: the day's largest waste and is not a pipeline fault; it is the fault of the afternoon (the fleet record). + +78: - "claimed, never submitted" (1,355): the chain step failed or the claim was abandoned. The failures are counted below. + +79: - The 3060 tier completed no paid segment in 313 claims: at 12 GB the chain runs out of margin (its proving median on completed chains + +80: is above the 4090's by the card's ratio, and a 4090 claimant finishes the same segment first), so the 12 GB tier is a miner, not a + +81: prover, on this segment size. The 3060's 34 submitted segments were all after 11:45Z (never paid for the partition's reason). + +82: - Wasted work is the end-to-end seconds of every claimed segment that was not paid; the fleet spent 254,000 card-seconds (70 card-hours) + +83: on segments that did not pay against 14,800 (4.1 card-hours) that did. Before the stall the ratio was about 3 to 1 (the steals and + +84: refusals below); after it, everything was waste. + +``` + +**`proving/igneum-proving/docs/analysis/proving-pipeline-2026-10-08.md:86-121`** +SHA-256: `9f96b76b32e6647b83fa8f751d695f581020b603bedadd2d515578937fb2a7fa` + +```text + +86: ## Failures and retries + +87: + +88: - `RESULT seg N chain FAILED`: 900, median wall 2.7 s. 629 "NotFound: No such file or directory": the sm_89 floor tarball's + +89: `igneum-prove-host` was a dangling link until the real host was served at 10:50Z (08:00 to 10:59Z, the fleet record's floor fault); + +90: 264 "invalid string length" (the host's proof-bytes string on the 48 GB cards' larger segments); 4 OutOfMemory (12 GB cards). After + +91: 10:50Z the NotFound class ended; the string-length class remains open for the node lane. + +92: - `RESULT segment_refused`: 153. 62 "does not chain to segment N..N" (the previous segment's record moved under the claim), 54 "unproven: + +93: the record is carried after the segment's deadline" (the chain step finished too late), 35 "segment already paid" (a faster claimant). + +94: - Retries: 0 lines "record accepted on retry". The prover does not retry a failed chain; it drops the export and claims afresh. + +95: - Failure rate on claims: 900 chain failures plus 153 refusals over 3,421 claims is 30.8 percent; without the floor-link class (fixed) it + +96: is 12.5 percent. + +97: + +98: ## Steals: a faster claimant takes an assigned prover's reward + +99: + +100: `RESULT paid_other`: 137 segments this box had claimed were paid to another key, 4.0 percent of claims (98 on 4090s, 25 on L40S, 8 on + +101: 3090s, 6 on the 6000 Ada). The time from this box's claim to the other key's payment read: median 306 s, p95 9,757 s (the long tail is the + +102: same pause-and-resume as the payment column). The exclusive window (10 DAA seconds) does not hold a slow claimant's segment for it: a + +103: second box that finishes its chain first is paid. The 3060 tier was never the winner and never the victim (it never finished). + +104: + +105: ## Queue depth over time + +106: + +107: The worklist as the provers read it on idle passes, median entries per hour (tip in the line): 02Z 596, 05Z 596, 08Z 596, 11Z 713, 14Z + +108: 594. Bounded at about 600 entries (the 600 DAA unproven window times one entry a DAA) for the whole day; it did not grow, because a + +109: segment leaves the list at its deadline whether proved or not. Growth would show the window itself lengthening; it did not. + +110: + +111: ## What this means + +112: + +113: - With the floor link fixed, the pipeline's own stages are fast: inputs 2 s, verification and submission under 2 s, aggregation 23 s + +114: (75 s on a 3090); the proving stage sets the pace, 27 s median and 216 s at the slowest 5 percent, and payment lands 171 s after + +115: submission (543 s at the slowest 5 percent) when the chain settles. + +116: - The waste is structural, not incidental: 70 card-hours wasted against 4 paid, dominated by the partition, then by the floor fault, + +117: then by steals and late chains (13 percent of claims). Two changes would cut the pre-stall waste: a claim that is honoured for the + +118: window it was granted (the steal rate goes to zero) and a 12 GB tier that claims only segments it can finish (the 3060's 313 claims + +119: earned nothing). + +120: - The next chain (igneum-devnet-4) starts this instrument from block zero; the two-hour window under the hold's declared workload is + +121: the first measurement to run on it, with block timestamps read so the assignment wait becomes a real column. + +``` + +### F09 excerpts + +**`v6/igneum-v6-freeze-tree/docs/analysis/class-v6/coexistence-model.md:299-321`** +SHA-256: `8b2b8964a77bac49c88c3d9c801455f18d9e730859b61ef958ffa3fa79f0de4a` + +```text + +299: ## 12. The conditions under which the success statement holds (the result) + +300: + +301: | Condition | The number on today's rows | DRAM board | SRAM die | + +302: |---|---|---|---| + +303: | (a) the chip's all-in cost per accepted unit at its own electricity within about 1.5x of the best GPU owner's at the GPU's electricity | the owner at 0.12: 263 to 332 micro-USD (5070 Ti, 5080) | 307 at 3 y, 750 at 1 y: PASSES | 72 to 134: FAILS at every life | + +304: | (b) the chip's annualised hardware per MH/s not below about a quarter of the GPU entrant's | the 5080 entrant's hardware 460 micro-USD | 239 to 677: PASSES | 33 to 95: FAILS | + +305: | (c) a fleet above a third of the chain's hash costs more than a year's miner revenue | at IGN 0.10 the chain is 9.6 TH/s, a third 3.2 | USD 18 M of boards against USD 40 to 80 M: PASSES above IGN 0.05 | USD 2.5 M of dies: FAILS at every price under about 3 | + +306: | (d) GPUs keep a resale market and a use outside mining | resale 25 to 55 percent after two years; rental 3x to 5x the mining cost | PASSES (the GPU side's property) | PASSES (the same) | + +307: | (e) the per-joule gap at the honest knee stays under about 3x | Blackwell at the knee 1.70 to 2.06 microjoules | 2.2x to 2.6x: PASSES | 3.7x to 4.5x: FAILS (the bare-lane floor 10x to 12x) | + +308: | (f) the supplier's gross margin is a normal return (under about 70 percent) and does not rise with the halvings | section 8 | 27 to 69 percent, falling with the halvings: PASSES | 85 to 93 percent, rising, or a loss once it is the chain: FAILS | + +309: | (g) a second income (proving) for the commodity side that the specialised supplier cannot enter | section 5: USD 3 to 7 a card-day at launch demand on the 16 GB and larger tiers; 0 for any hash engine | PASSES (the board cannot prove either, which is the GPU's advantage over it) | PASSES (the same) | + +310: + +311: **The result.** The success statement holds for the stored-dataset DRAM-board chip at a 1 to 3 year life on today's + +312: rows, in every price path, at every electricity price on the axis, with the GPU side's own generation curve narrowing + +313: the gap further and proving as a second income the chip cannot enter; its pass needs no small network (it holds at 42 + +314: TH/s and IGN 1.00), no token appreciation (it holds on the flat and shrinking paths) and no scheduled ASIC death (the + +315: life axis, not the rotation, is what the board lives on). The statement does not hold for the N2 SRAM die once that + +316: die exists with its development sunk, at any life, price path or electricity price; a small network makes it worse, + +317: appreciation only delays it, and the rotation does not touch a programmable die. **The model names where it fails: a + +318: sunk SRAM die fleet of USD 10 M or more at any price in the window, and of USD 1 M in a shrinking chain.** The only + +319: condition that holds the die is that nobody pays to build it (section 6: IGN 0.73 for a USD 150 M project at a third + +320: of the chain over three years, 0.22 taking the chain, 0.12 to 0.16 for a revision), which is a statement about an + +321: investor's decision and is carried as such, not as a level the chain stays below. + +``` + +### F10 excerpts + +**`mining/proto-cuda/nvrtc/worker.cpp:1254-1295`** +SHA-256: `faf4782ff0fbfdea594e4635200a75db9084f96c663053d7c074df8e5bebf92f` + +```text + +1254: uint64_t remaining = nonceCount, hashes = 0; + +1255: uint32_t hi = (uint32_t)(nonceStart >> 32), lo = (uint32_t)nonceStart; + +1256: bool failed = false; + +1257: while (remaining > 0) { + +1258: uint64_t room = (uint64_t)(0xffffffffu - lo) + 1ull; + +1259: uint64_t chunk64 = remaining < batch ? remaining : batch; + +1260: if (chunk64 > room) chunk64 = room; + +1261: uint32_t chunk = (uint32_t)chunk64; + +1262: uint32_t iw[8]; + +1263: { + +1264: uint8_t b[49]; + +1265: std::memcpy(b, "igneum-block/", 13); + +1266: std::memcpy(b + 13, prehash, 32); + +1267: b[45] = (uint8_t)hi; b[46] = (uint8_t)(hi >> 8); b[47] = (uint8_t)(hi >> 16); b[48] = (uint8_t)(hi >> 24); + +1268: pf_seed_words_from_bytes(b, 49, iw); + +1269: } + +1270: // A chunk that is not a multiple of the block is finished one 32-lane block at a time. The block is the + +1271: // pair's (its winning variant's). The mutex gives a race its exclusive windows between chunks. + +1272: std::lock_guard hold(gpuMutex); + +1273: uint32_t block = 32u * (uint32_t)cur->blockWarps; + +1274: uint32_t main = chunk - (chunk % block); + +1275: CUresult r = CUDA_SUCCESS; + +1276: if (main > 0 && !launchHash(c, cur, dOut, lo, iw, main, block, nullptr, err)) { emit("error " + jobId + " dispatch failed: " + err); failed = true; break; } + +1277: if (main < chunk) { + +1278: for (uint32_t off = main; off < chunk && !failed; off += 32u) if (!launchHash(c, cur, dOut + (CUdeviceptr)off * 8u, lo + off, iw, 32u, 32u, nullptr, err)) { emit("error " + jobId + " dispatch failed: " + err); failed = true; } + +1279: if (failed) break; + +1280: } + +1281: r = c.drv.streamSynchronize(nullptr); + +1282: if (r == CUDA_SUCCESS) r = c.drv.memcpyDtoH(hOut.data(), dOut, (size_t)chunk * 8u); + +1283: if (r != CUDA_SUCCESS) { emit("error " + jobId + " dispatch failed: " + c.err(r)); failed = true; break; } + +1284: for (uint32_t i = 0; i < chunk; ++i) if (hOut[i] <= target) { + +1285: uint64_t nonce = ((uint64_t)hi << 32) | (uint64_t)(uint32_t)(lo + i); + +1286: std::printf("found %s %llu %016llx\n", jobId.c_str(), (unsigned long long)nonce, (unsigned long long)hOut[i]); + +1287: } + +1288: std::fflush(stdout); + +1289: hashes += chunk; + +1290: remaining -= chunk; + +1291: if (chunk64 == room) { hi += 1u; lo = 0u; } else lo += chunk; + +1292: } + +1293: if (failed) continue; + +1294: emit(fmt("done %s %llu %.2f", jobId.c_str(), (unsigned long long)hashes, wallMs() - t0)); + +1295: if (switched && old) { releasePair(c, old); old = nullptr; info("dropped the previous pair (its program, cache and dataset)"); } + +``` + +**`mining/proto-cuda/nvrtc/worker.cpp:1318-1341`** +SHA-256: `faf4782ff0fbfdea594e4635200a75db9084f96c663053d7c074df8e5bebf92f` + +```text + +1318: // --bench: the pair is built and self-tested (vectors through the bound kernel with the seed words); then a warm-up + +1319: // dispatch at base nonce 0 (fingerprinted) and --batches timed dispatches of 2^B nonces, wall time around + +1320: // cuStreamSynchronize (the driver API path loads no event symbols; a 2^24 dispatch is 60 to 900 ms on the cards here, + +1321: // so the launch overhead is under 1 percent). + +1322: static int runBench(Ctx& c, const Options& o, Pair* p) { + +1323: uint32_t nonces = 1u << o.batchLog2, block = 32u * (uint32_t)c.blockWarps; + +1324: if (p->persistent) { uint32_t unit = 32u * (uint32_t)p->warps; nonces = (nonces / unit) * unit; if (nonces == 0) nonces = unit; } + +1325: CUdeviceptr dOut = 0; + +1326: std::string err; + +1327: if (c.drv.memAlloc(&dOut, (size_t)nonces * 8u) != CUDA_SUCCESS) { std::printf("FAIL: cuMemAlloc out\n"); return 2; } + +1328: std::vector hOut(nonces); + +1329: double sum = 0, warm = 0; + +1330: uint64_t fp = 0; + +1331: for (int b = -1; b < o.batches; ++b) { + +1332: double t0 = wallMs(); + +1333: if (!launchHash(c, p, dOut, (uint32_t)(b + 1) * nonces, p->sw, nonces, block, nullptr, err)) { std::printf("FAIL: %s\n", err.c_str()); return 2; } + +1334: CUresult r = c.drv.streamSynchronize(nullptr); + +1335: if (r != CUDA_SUCCESS) { std::printf("FAIL: dispatch %d: %s\n", b, c.err(r).c_str()); return 2; } + +1336: double ms = wallMs() - t0; + +1337: if (b < 0) { + +1338: warm = ms; + +1339: if (c.drv.memcpyDtoH(hOut.data(), dOut, (size_t)nonces * 8u) != CUDA_SUCCESS) { std::printf("FAIL: read-back\n"); return 2; } + +1340: fp = fnv1a64Bytes(hOut.data(), (size_t)nonces * 8u); + +1341: } else sum += ms; + +``` + +**`mining/proto-opencl/host.c:1652-1705`** +SHA-256: `ac390fe2a6a338e24a4530fe68fd5cab3ea7b3ecf822dcd5c9504bd5ea669b6f` + +```text + +1652: /* The select pass (5 October 2026). Before it every dispatch read back 8 bytes per nonce (16 MiB for a 2^21-nonce + +1653: * job) over the bus and scanned them on the host; on an eGPU over USB4 that is a measurable part of every job. + +1654: * Now a tiny kernel built here (no pack involved) writes the hits (index, hash) behind an atomic counter plus 34 + +1655: * sentinel words (the first 32 outputs, the middle and the last), and the host reads back a few hundred bytes. + +1656: * The fault detectors read the sentinels; the found lines are printed in nonce order from the sorted hits. If a + +1657: * chunk has more hits than the table holds (a target that loose is a test, not a block), the chunk falls back to the + +1658: * full read. --readback full or IGNEUM_READBACK=full keeps the old path for a comparison. */ + +1659: #define IG_MAX_HITS 256u + +1660: cl_program selProg = NULL; + +1661: cl_kernel kSelect = NULL; + +1662: cl_mem dCount = NULL, dHits = NULL, dSentinel = NULL; + +1663: size_t selLocal = di->maxWorkGroup < 256 ? di->maxWorkGroup : 256; + +1664: uint32_t hCount = 0; + +1665: uint64_t hHits[IG_MAX_HITS * 2]; + +1666: uint64_t hSentinel[34]; + +1667: unsigned long long bytesUp = 0, bytesDown = 0; + +1668: double kernelMsSum = 0, selectMsSum = 0, readMsSum = 0, scanMsSum = 0; + +1669: unsigned long fullFallbacks = 0; + +1670: if (!o->readback) { + +1671: static const char* SELECT_SRC = + +1672: "__kernel void igneum_select(__global const ulong* out, uint n, ulong target, volatile __global uint* count,\n" + +1673: " __global ulong* hits, uint maxHits, __global ulong* sentinel) {\n" + +1674: " uint i = (uint)get_global_id(0);\n" + +1675: " if (i < n) {\n" + +1676: " ulong h = out[i];\n" + +1677: " if (h <= target) { uint k = atomic_inc(count); if (k < maxHits) { hits[2u * k] = (ulong)i; hits[2u * k + 1u] = h; } }\n" + +1678: " if (i < 32u) sentinel[i] = h;\n" + +1679: " if (i == 0u) { sentinel[32] = out[n / 2u]; sentinel[33] = out[n - 1u]; }\n" + +1680: " }\n" + +1681: "}\n"; + +1682: size_t selLen = strlen(SELECT_SRC); + +1683: selProg = clCreateProgramWithSource(dv->ctx, 1, &SELECT_SRC, &selLen, &err); CL_CHECK_ERR(err, "clCreateProgramWithSource select"); + +1684: err = clBuildProgram(selProg, 1, &di->device, "-cl-std=CL1.2", NULL, NULL); + +1685: if (err != CL_SUCCESS) { + +1686: size_t logLen = 0; char* log; + +1687: clGetProgramBuildInfo(selProg, di->device, CL_PROGRAM_BUILD_LOG, 0, NULL, &logLen); + +1688: log = (char*)calloc(logLen + 1, 1); + +1689: if (logLen) clGetProgramBuildInfo(selProg, di->device, CL_PROGRAM_BUILD_LOG, logLen, log, NULL); + +1690: printf("info the select pass did not build (%s): %.300s; using the full read-back\n", clErrName(err), log); + +1691: free(log); clReleaseProgram(selProg); selProg = NULL; ((Options*)o)->readback = 1; + +1692: } else { + +1693: kSelect = clCreateKernel(selProg, "igneum_select", &err); CL_CHECK_ERR(err, "clCreateKernel igneum_select"); + +1694: dCount = clCreateBuffer(dv->ctx, CL_MEM_READ_WRITE, 4, NULL, &err); CL_CHECK_ERR(err, "clCreateBuffer count"); + +1695: dHits = clCreateBuffer(dv->ctx, CL_MEM_READ_WRITE, IG_MAX_HITS * 2 * sizeof(uint64_t), NULL, &err); CL_CHECK_ERR(err, "clCreateBuffer hits"); + +1696: dSentinel = clCreateBuffer(dv->ctx, CL_MEM_READ_WRITE, 34 * sizeof(uint64_t), NULL, &err); CL_CHECK_ERR(err, "clCreateBuffer sentinel"); + +1697: gMemCreated += 3; + +1698: { + +1699: size_t wg = 0; + +1700: if (clGetKernelWorkGroupInfo(kSelect, di->device, CL_KERNEL_WORK_GROUP_SIZE, sizeof(wg), &wg, NULL) == CL_SUCCESS && wg && wg < selLocal) selLocal = wg; + +1701: } + +1702: } + +1703: } + +1704: printf("info readback %s (per dispatch of %u nonces: %s)\n", o->readback ? "full" : "select", + +1705: batch, o->readback ? "8 bytes per nonce come back and the host scans them" : "the hits and 34 sentinel words come back; the GPU scans"); + +``` + +**`mining/proto-opencl/host.c:1882-1899`** +SHA-256: `ac390fe2a6a338e24a4530fe68fd5cab3ea7b3ecf822dcd5c9504bd5ea669b6f` + +```text + +1882: r0 = wallMs(); + +1883: if (useSelect) { + +1884: SERVE_CHECK(jobId, clEnqueueReadBuffer(dv->q, dCount, CL_TRUE, 0, 4, &hCount, 0, NULL, NULL)); + +1885: bytesDown += 4; + +1886: nHits = hCount; + +1887: if (nHits > IG_MAX_HITS) { + +1888: /* more hits than the table holds: this chunk takes the full path (the test target case) */ + +1889: ++fullFallbacks; + +1890: useSelect = 0; + +1891: } else { + +1892: if (nHits) { SERVE_CHECK(jobId, clEnqueueReadBuffer(dv->q, dHits, CL_TRUE, 0, (size_t)nHits * 2 * sizeof(uint64_t), hHits, 0, NULL, NULL)); bytesDown += (unsigned long long)nHits * 16; } + +1893: SERVE_CHECK(jobId, clEnqueueReadBuffer(dv->q, dSentinel, CL_TRUE, 0, 34 * sizeof(uint64_t), hSentinel, 0, NULL, NULL)); + +1894: bytesDown += 34 * 8; + +1895: } + +1896: } + +1897: if (!useSelect) { + +1898: SERVE_CHECK(jobId, clEnqueueReadBuffer(dv->q, dOut, CL_TRUE, 0, (size_t)chunk * sizeof(uint64_t), hOut, 0, NULL, NULL)); + +1899: bytesDown += (unsigned long long)chunk * 8; + +``` + +**`mining/proto-metal/main.swift:3344-3370`** +SHA-256: `0f909ca4d20345635765e03442b06c79f34bc8a2ec8bdac22a3e87f6d7e209b5` + +```text + +3344: let outPtr = outBuf.contents().bindMemory(to: UInt64.self, capacity: batch) + +3345: let kernel = program.compiled // the pair's kernel for this job (a race may swap it for the next) + +3346: while remaining > 0 { + +3347: let room = UInt64(UInt32.max - lo) + 1 // lane nonces left before the high word steps + +3348: let chunk = Int(min(min(remaining, UInt64(batch)), room)) + +3349: var initw = blockInitWords(prehash: prehash, nonceHi: hi) + +3350: gpuLock.lock() // a race's exclusive windows fall between chunks + +3351: let cb = gpu.queue.makeCommandBuffer()! + +3352: let enc = cb.makeComputeCommandEncoder()! + +3353: encodeHash(enc, kernel, dataset: dataset.buffer, out: outBuf, base: lo, initw: &initw, count: chunk) + +3354: enc.endEncoding() + +3355: cb.commit(); cb.waitUntilCompleted() + +3356: gpuLock.unlock() + +3357: if let e = cb.error { emit("error \(jobId) dispatch failed: \(e)"); failed = true; break } + +3358: for i in 0.. Plan { + +228: let mem_step = if limits.mem_max_mhz > limits.mem_default_mhz { ((limits.mem_max_mhz - limits.mem_default_mhz) / 20).max(25) } else { 0 }; + +229: let core_step = if limits.clock_max_mhz > 0 { (limits.clock_max_mhz / 20).max(25) } else { 0 }; + +230: let start = Point { clock_mhz: limits.clamp_clock(start.clock_mhz), power_pct: start.power_pct.clamp(50, 100), mem_mhz: limits.clamp_mem(start.mem_mhz) }; + +231: Plan { kind: PlanKind::Climb, limits: limits.clone(), before: start, tolerance_pct: goal.tolerance_pct(tolerance_pct), power: Vec::new(), clock_pcts: Vec::new(), clock_fine: Vec::new(), fixed: Vec::new(), climb: Some(Climb { start, mem_step, core_step, budget: 5, goal }) } + +232: } + +233: + +234: /// The goal's score of a row: MH per watt for efficiency and balanced, the rate for maximum rate. + +235: pub fn score(&self, r: &Row) -> f64 { + +236: match self.climb.as_ref().map(|c| c.goal) { + +237: Some(Goal::MaxRate) => r.mhs, + +238: _ => r.eff, + +239: } + +240: } + +241: + +242: /// The climb's next probe from the rows so far: None when the budget is spent or no move is left. + +243: fn climb_next(&self, rows: &[Row]) -> Option { + +244: let c = self.climb.as_ref()?; + +245: let step = |p: Point| Step { point: p, watts: self.limits.watts_for(p.power_pct), kind: Kind::Climb }; + +246: if rows.is_empty() { + +247: return Some(step(c.start)); + +248: } + +249: if rows.len() >= c.budget { + +250: return None; + +251: } + +252: // the best usable row so far is the hill's top; a knob that produced a marked (refused) row is backed off + +253: let best = rows.iter().filter(|r| r.usable()).max_by(|a, b| self.score(a).partial_cmp(&self.score(b)).unwrap_or(std::cmp::Ordering::Equal))?; + +254: let refused_mem = rows.iter().any(|r| !r.usable() && r.point.mem_mhz > best.point.mem_mhz); + +255: let refused_core = rows.iter().any(|r| !r.usable() && r.point.clock_mhz != 0 && (best.point.clock_mhz == 0 || r.point.clock_mhz < best.point.clock_mhz)); + +256: let last = rows.last()?; + +257: let mem_up = |p: Point| -> Option { + +258: if c.mem_step == 0 || refused_mem { return None; } + +259: let base = if p.mem_mhz == 0 { self.limits.mem_default_mhz } else { p.mem_mhz }; + +260: let m = self.limits.clamp_mem(base + c.mem_step); + +261: (m > 0 && m != p.mem_mhz && m != base).then_some(Point { mem_mhz: m, ..p }) + +262: }; + +263: let core_down = |p: Point| -> Option { + +264: if c.core_step == 0 || refused_core { return None; } + +265: let base = if p.clock_mhz == 0 { self.limits.clock_max_mhz } else { p.clock_mhz }; + +266: let k = self.limits.clamp_clock(base.saturating_sub(c.core_step)); + +267: (k > 0 && k != p.clock_mhz).then_some(Point { clock_mhz: k, ..p }) + +268: }; + +269: let tried = |p: Point| rows.iter().any(|r| r.point == p); + +270: // the last move improved: keep going the same way from the top; else turn: memory first, then core, then both + +271: let last_improved = last.usable() && last.point == best.point && rows.len() > 1; + +272: let last_was_mem = rows.len() > 1 && last.point.mem_mhz != rows[rows.len() - 2].point.mem_mhz; + +273: let candidates: Vec> = if last_improved && last_was_mem { + +274: vec![mem_up(best.point), core_down(best.point)] + +275: } else if last_improved { + +276: vec![core_down(best.point), mem_up(best.point)] + +277: } else { + +278: vec![mem_up(best.point), core_down(best.point), mem_up(best.point).and_then(core_down)] + +279: }; + +280: candidates.into_iter().flatten().find(|p| !tried(*p)).map(step) + +``` + +**`mining/app/igneum-app/src/ember.rs:493-520`** +SHA-256: `e1e9d83e37b3acb47e89027994dba1638d7c4c547a0d1f587b95455cbe3c4873` + +```text + +493: pub fn from_samples(step: &Step, s: &Samples, baseline_mclk: f64) -> Row { + +494: let watts = mean(&s.draws); + +495: let mhs = mean(&s.rates); + +496: let mclk = mean(&s.mclks); + +497: let usable = s.draws.len() >= 3 && !s.rates.is_empty() && watts > 1.0; + +498: let mark = if s.faults > 0 { + +499: Mark::Faulted + +500: } else if s.unapplied { + +501: Mark::Unapplied + +502: } else if !usable { + +503: Mark::NoReadings + +504: } else if s.tmax >= HOT_C { + +505: Mark::Hot + +506: } else if baseline_mclk > 0.0 && mclk > 0.0 && mclk < MCLK_HOLD * baseline_mclk { + +507: Mark::MemoryClock + +508: } else { + +509: Mark::Ok + +510: }; + +511: Row { point: step.point, limit: step.watts, watts, mhs, eff: if usable { mhs / watts } else { 0.0 }, draws: s.draws.len(), rates: s.rates.len(), gclk: mean(&s.gclks), mclk, tmax: s.tmax, faults: s.faults, mark: Some(mark) } + +512: } + +513: pub fn usable(&self) -> bool { + +514: self.eff > 0.0 && self.mark == Some(Mark::Ok) + +515: } + +516: /// `TUNE card=