diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index 9ce975501..8c0c4cec3 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -338,7 +338,7 @@ Status: Closed by rule (3 October 2026). Spec section 7.2: sortition keyed to th Answer: Fair, and the lottery/proving separation does not by itself fix it. If claims are first-come, latency wins. The candidate rule is sortition of shards: a VRF keyed to the block assigns each shard to a set of eligible provers, weighted by past proving or by a lottery, with fallback to open claiming after a timeout. This is a phase 1 specification item and a phase 4 devnet measurement. The 20% proving pool is paid per block as a fixed amount divided by consensus proving cost, so a prover's income is bounded by the shards it is assigned, not by how many it can grab. -Evidence: design doc "Proving speed on consumer GPUs" risk item and Finality v2 "Fees". Rule: not yet. +Evidence: design doc "Proving speed on consumer GPUs" risk item and Finality v2 "Fees". Rule: not yet. Cross-reference (round 3, 3 October 2026): F17 reopens the per-key draw of spec 7.2, which a key-splitter sweeps by count. ### P9. Shard griefing "Claim a shard with a small bond and never prove it. Repeat. Finality waits on you." @@ -1032,3 +1032,252 @@ Add one before sharing the litepaper, and point the FUD ledger's submission line 78. LP "What Igneum does not claim" Add three items: "A memory-hard prototype. Not yet: the current dataset can be shortcut 110x; the 256 MB cache replaces it." "Finality in the first month. Not until the 30-day window has history." "A cryptography team. Not yet; external reviewers are named before gate 3." + +--- + +## Round 3 entries (3 October 2026, evening): the specification and the running code + +Added by `docs/review/round-3-2026-10-03.md`, which holds the full argument for each, the reviewer who raised it, and the five to fix first. Entries are placed under their section letters below and numbered on from the last entry of each section. Existing entries are unchanged; where one is extended the new entry says so. Count after this block: 107 entries (80 plus 27). Of the 27: 0 fatal, 18 serious, 9 minor by the review's ranking (M18, M21, P13, P15, C13, L8, G10, X12 and the text half of E11 are the minor ones). + +### M14. A pulsed rental against the block-count DAA buys weight at a discount +"Bring 50x the hashrate for two minutes once an hour. Kaspa's window keeps the 661 most recent samples, so you mine thousands of blocks at the old target before it moves, and when you leave the honest network crawls for hours at your difficulty. Weight is blocks over a window of blocks. You get a third of the window in a week for the price of a 1.6x average." + +Status: Open, experiment scheduled. + +Answer: Correct in mechanism and unmeasured in size. `sim/results_v2.md` runs every scenario with a perfect retarget (its assumption table) and no finality run has a DAA model (O-3.9). Tonight's honest step on the devnet showed the lag: 5.67 blocks a second in a two-minute bucket after the first retarget, 2,248 blocks in eight minutes against 480 expected, and 1.5 blocks a second an hour later (`docs/review/round-3-2026-10-03.md`, "Tonight's devnet"). The review's estimate is that a pulse buys under two windows of blocks (about 5,000) and the chain crawls after each, so the amplifier over the formula is about 2x at the cost of a visible attack; the estimate is not a simulation. The same lag is the farm operator's hop profit (`sim/difficulty/sim.py` profiles `hop3`, `hop10`, `polluted`), for which no result is in the bench-log. Fix in two parts: the controller (gate 2, O-2.1, with the simulation results logged) and the weight rule (F14). + +Evidence: `node1.log` of 3 October 2026 and `sim/difficulty/devnet-2026-10-03.csv`; `sim/results_v2.md` assumptions. Experiment: `finality_v2.py` with the Kaspa DAA and the two-lane candidate in the loop against a 50x pulsed renter, reporting the day it crosses a third; `sim/difficulty/sim.py` results in the bench-log. Review id R3.1. + +### M15. A header with any past timestamp or any claimed DAA score makes the node build a 256 MiB cache +"Your PoW check runs after GHOSTDAG and before the checks that validate `daa_score` and the past-median timestamp. The engine keys the program on the header's own `daa_score` and the cache on its own `timestamp`, and keeps three entries. I send headers with random past days. Each one costs you a 0.18-second ChaCha12 fill and evicts the honest epoch." + +Status: Open, fix named. + +Answer: Correct. `validate_header` (`consensus/src/pipeline/header_processor/processor.rs:293` to `301`) runs the isolation checks (timestamp against the future only, `pre_ghostdag_validation.rs:48`), GHOSTDAG, `check_pow_and_calc_block_level`, and only then `pre_pow_validation` with `check_difficulty_and_daa_score`. `epoch_seed` (`processor.rs:325`) reads `header.daa_score`; `day_index` reads `header.timestamp`; `IgneumEngine::KEEP` is 3. `docs/fork-divergence.md` flags the ordering as a GHOSTDAG cost; the cache thrash is the sharper form. Fix: run the DAA-score and past-median checks before the PoW check; derive the day from DAA score (spec 1.12) or from the selected parent's window; `KEEP` 4; a per-peer cap on cache builds. + +Evidence: the files and lines above. Fix: review's first of five. Review id R3.26. + +### M16. The 256 MiB cache fits on a die, so the recompute attacker is compute bound +"Your 4.8x-slower shortcut ran with the cache in DRAM behind a chip that cannot hold it. Put 256 MiB of SRAM on a die and the dataset is never needed: 128 items per hash at about 1,170 integer operations and 8 near-free reads each. That is 150,000 operations per hash, and integer operations per dollar is where silicon beats a GPU." + +Status: Open, experiment scheduled. + +Answer: Correct as arithmetic, unmeasured as a device. From spec 1.8.4 and 1.8.5 an item costs 9 mixer applications of about 130 operations and 8 cache reads; under the proposed 16-load rule a hash derives 128 items. A 5090-class integer budget (about 50 T operations a second, approximate) gives about 0.33 Ghash/s against the honest 141 Mhash/s projection, about 2.4x at equal silicon before any chip-versus-GPU efficiency, and a 256 MiB SRAM is about 250 to 300 mm^2 on a current node (approximate, from wafer-scale parts). The cache size was set to beat a GPU's L2 (spec 1.16), not a die. The lever is the cache size and the mixer cost, both prototype values at gate 1. Monero's precedent does not price this (C13). An FPGA does not reach it (review, chip designer, attack 3). + +Evidence: spec 1.8, 1.16; `proto-metal/MEMHARD.md` 2.2 (Apple only). Experiment: `--inline-dataset` on the RTX 5090 at a 64 MiB cache (inside its 96 MiB L2, the SRAM emulation) and at 256 MiB against the honest 1 GiB kernel; the O-1.6 time-memory curve; a CPU fill and verify time at a 1 GiB cache. Decision at gate 1: cache size "exceeds what one die can hold, and grows". Review ids R3.5 and the chip designer's pricing. + +### M17. Every ahead-of-time miner stops at the epoch boundary; whoever compiles in process mines alone +"Your log: last block of epoch 0 at 21:12:14, nothing for the next 91 seconds while the Windows launcher killed eight identities, re-exported, rebuilt two binaries and restarted them. The Metal worker compiles in 129 ms. The first 2.5% of every hour goes to whoever does not use your launcher." + +Status: Open, fix named. + +Answer: Correct for the devnet client. Under the v0 seed rule (the last block of the previous epoch) nobody can compile early and the gap is structural; under the spec's VDF pipeline the seed is known 1,200 DAA seconds ahead (spec 4.3) and an honest client compiles ahead, so the gap is a client defect, not consensus. Fix: in-worker NVRTC and runtime OpenCL compilation (listed in `docs/fork-divergence.md` as open), measured on a mixed rig through 24 epoch changes (O-1.16). Extends M11. + +Evidence: `node1.log` 21:12:14 to 21:13:45; `proto-cuda/windows-miner/start-mining.ps1` (exit 42 handling); bench-log Metal compile figures. Review id R3.7. + +### M18. The per-hash random data path is a one-bit select +"Your `add` picks one of two immediates by a bit of `r0`. A chip computes both and muxes. Calling that a data-dependent path next to ProgPoW is marketing." + +Status: Conceded, wording fix. + +Answer: Correct. Spec 1.4.1: `sel` is `r0` at the top of each iteration and each `add` selects `imm` or `imm2` by one bit of it. It costs a chip nothing and defends nothing; the defence is the random reads. The litepaper's vs RandomX row should drop "random data path" or say what it is. + +Evidence: spec 1.4.1. Fix: `site/litepaper.html` vs RandomX row, "Random program" (review, RandomX author, sentence). Review id R3.8. + +### M19. The census that justifies the generator rule has blank cells, and the spec still carries the free load count +"Section 7.1 of your census has `FRESH2_DIST`, `FRESH2_P` and `FRESH2_CAND` where the proposed generator's numbers go, section 7.2 is the word `FRESH2_ACCEPTED`, section 7.3 is `CF_AGREEMENT`, and the spec text you propose cites a rejection rate called `REJECT-RATE`. Until it is filled and adopted every hour is a different coin." + +Status: Open, measurement in progress. + +Answer: Correct. The current-generator census is complete (100,000 programs, 94.8% with a redundant load, hash rate tracking distinct loads 56 to 152 per hash at the 1st to 99th percentile); the `fixed16-fresh2` run that fixes the proposed rule's own rejection rate and candidate count is not in the document, and section 1.4.2 of the spec still draws the load count freely. Fix: finish the run, fill the cells, adopt G1 + G2 + R into spec 1.4 with new vectors (spec 1.16 schedules the re-cut), and run ten programs on the 5090 under the new generator. Extends M5 and M6. + +Evidence: `docs/analysis/weak-program-census-2026-10-03.md` sections 7 and 9. Review id R3.6. + +### M20. Pruning proofs are checked with the kHeavyHash stub +"Wait thirty hours, start a fresh node, and watch it reject the honest pruning proof: `validate.rs:192` runs kHeavyHash on headers mined under the lottery, which pass with probability 2^-28. And if you loosen that, I forge levels with an ASIC that already exists." + +Status: Open, acknowledged, fix named. + +Answer: Correct. `consensus/src/processes/pruning_proof/validate.rs:192` calls `calc_block_level_check_pow`, which runs the stub, and `apply.rs` and `mod.rs` call `calc_block_level` the same way; `docs/fork-divergence.md` records that seeds must be threaded through pruning-proof validation before a pruning network. The devnet will pass its pruning depth (108,000 blocks, sooner after tonight's overshoot) and a fresh node will show it. Fix: derive the epoch and day for proof headers from the proof's own headers (fork map a4, O-2.5) and remove the stub from the proof path. + +Evidence: the files above. Test: sync a fresh node from the 30-hour devnet. Review id R3.3. + +### M21. GHOSTDAG k is Kaspa's table value for Kaspa-sized bodies +"k = 18 assumes a 5-second delay bound measured with Kaspa's blocks. Yours carry EVM transactions and recursive proof records." + +Status: Open, extends O-2.2. + +Answer: Correct. Spec 2.1 takes k, max parents and the mergeset limit from Kaspa's table at 1 BPS. O-2.2's two-miner devnet measures the parallel and red rate; it should run with proof-bearing bodies of the size section 5.4 of the execution design implies, and k should be re-derived from the measured delay. + +Evidence: spec 2.1; `docs/design/execution-layer.md` 5.4. Review id R3.4. + +### F14. Weight in blocks over a window in blocks under a lagging retarget +"Your day counts assume the block supply is capped at one a second. It is not during a retarget lag, and weight is counted in blocks." + +Status: Open, rule change proposed. + +Answer: Correct; this is the finality half of M14. Spec W2 counts blue blocks over a window of 2,592,000 DAA seconds, and DAA seconds are blocks, so a burst both inflates a key's count and ages the window. Proposed change: denominate the vote window (W2) and the presence window (Q1) in past-median time, and define weight as a key's share of the blue blocks in each 60-second median-time bucket summed over the trailing 30 days, so 50x the blocks in one minute is one minute of weight. The simulation of M14 decides. + +Evidence: spec 3.1 W2, 3.3 Q1; `sim/results_v2.md` assumptions. Experiment: the M14 run under both definitions. Review id R3.1. + +### F15. Merge depth is not the reorg bound; the finality depth is +"You tell exchanges that in month one the one-hour merge-depth bound limits a reorganisation. `check_bounded_merge_depth` only forbids merging an old red. The virtual switches to any heavier chain until the finality point, which you kept at 12 hours." + +Status: Open, text and rule fix named. + +Answer: Correct on reading the fork. `post_pow_validation.rs:79` bounds which reds a block may merge; the selected-chain switch is bounded by the finality point (`virtual_processor/processor.rs:1626`, `FINALITY_DURATION` 43,200 DAA seconds). So the month-one reorg bound is 12 hours of DAA time, not one hour, and spec 3.8, 3.9, litepaper "Finality" and "What Igneum does not claim" item 4 rest on the wrong constant. Fix: state the finality depth as the bound, in median time (M14 explains why not DAA time), or lower `FINALITY_DURATION` and accept Kaspa's finality-conflict handling at that depth; test with a heavier private chain forked 2, 6 and 13 hours back on simnet. + +Evidence: the files above. Review id R3.2. + +### F16. A lock can become uncertified after a heal +"Section 3.5: two certificates at one index, strike the equivocators, re-evaluate, and if neither or both still lock the index is uncertified. So the lock my node reported and I credited on is revocable." + +Status: Open, decision at gate 3 (extends O-3.6). + +Answer: Correct as the proposal stands. For an exchange "locked" must be irrevocable or it is a confirmation count. The alternative is Kaspa's: a verified certificate is never re-evaluated; two certificates at one index are a chain split that halts `finality_active` until an operator intervenes, and the node never reports a lock it may withdraw. Equivocation costing history and not coins (F6) means the attacker who caused the split keeps the deposit either way. Decision at gate 3; the devnet partition-and-heal test of O-3.6 measures whichever rule is chosen. + +Evidence: spec 3.5, 3.9. Review id R3.17. + +### F17. Keys are free and the official client mints eight per card +"Your launcher runs 8 identities per vendor, each with its own vote key. For the vote that is harmless by design. For shard sortition, which ranks every eligible key and takes 8, it is the whole game: 1% of hashrate holds 259 keys above dust. And a few thousand PCs cross your 8,192-voter switch, whose construction is open." + +Status: Open, rule change proposed. Reopens P8. + +Answer: Correct. Spec 7.2 step 2 ranks per key and eligibility is 100 blue blocks in 30 days (0.004% of hashrate at one block a second), so a miner with share s holds up to s x 2,592,000 / 100 keys and the draw of 8 is dominated by whoever splits most. `proto-cuda/windows-miner/start-mining.ps1` derives a key per identity (`MINERS` default 8). O-5.1's acceptance test (fastest prover under 25% of shards) does not catch a splitter, who wins by count. Fix: draw the 8 assignees by weight (8 blue blocks uniformly from the window, take their keys), so splitting changes nothing; the client defaults to one key per operator; specify S2 (O-3.5) and the bitmap size (O-3.12) before the count can cross 8,192. + +Evidence: spec 7.2, 3.4; the launcher. Cross-reference added to P8. Review id R3.14. + +### F18. "A silent minority cannot freeze finality" is false under the floor +"Your own simulation: 45% silent stalls finality for as long as they stay silent, 40% gives 138 stalls in three days, 50% churn stalls 4.1 days. The litepaper says a silent minority cannot freeze it and a lock never waits for miners who have left." + +Status: Conceded, not yet stated; text fix named. + +Answer: Correct. The sentence was true of the active-only rule and was not updated when the 56.7% floor was added (spec 3.3.1 states the liveness cost honestly: liveness ends between 40% and 45% silent). Fix: the replacement sentence in the review (exchange engineer, sentence), and a line in "What Igneum does not claim". + +Evidence: `sim/results_v2.md` C and D; spec 3.3.1; `site/litepaper.html` Finality. Review id R3.18. + +### P11. The native-execution veto makes block validity depend on the node's current selected chain +"Section 5.5: a block is invalid if a proof record names a segment not on the node's selected chain, or disagrees with the node's own execution. Two honest nodes with different tips disagree on the same block. You removed the hidden-block penalty for this exact reason." + +Status: Open, text fix named. + +Answer: Correct. `docs/design/execution-layer.md` 5.5 (D12) and spec 7.2 item 5 reference the node's selected chain, which is a property of its virtual, not of the block's past. Fix: a proof record is valid if the chain block it names is on the carrying block's own selected-parent chain and its `post_root` and receipts equal the execution of that segment along that chain, which every node computes from the block's past. Add a two-node reorg test to section 8.5. + +Evidence: the two sections. Review id R3.9. + +### P12. An aggregator can name itself as every prover +"`ProofRecord.provers` is who is paid and nothing in a shard proof's statement says who proved it. I aggregate eight gossiped shard proofs and write my key eight times." + +Status: Open, format fix named. + +Answer: Correct. Section 5.1's shard statement (pre-root, transactions, post-root, receipts) and the `ProofSystem` trait of 5.6 carry no prover identity; 4.4 credits the pool to the record's `provers`. Fix: each shard proof's public input includes the prover's payout key, aggregation carries the keys as public outputs, and a record whose list does not match them is invalid; acceptance test A5 checks the match, not only the credit. + +Evidence: `docs/design/execution-layer.md` 5.1, 5.4, 5.6, 9.2 A5. Review id R3.10. + +### P13. The litepaper still claims shards with a bond +"'Miners claim shards with a small bond.' Your spec 7.2 decided sortition with no bond the same day." + +Status: Conceded, fix now. + +Answer: Correct. Fix: "Shards are assigned by lot to eight provers for ten seconds, then open to anyone; there is no bond." (`site/litepaper.html`, Proving, "How a block gets proven".) + +Evidence: spec 7.2. Review id R3.13. + +### P14. Two definitions of the proving base fee, and a quote that cannot know the ratio +"Spec 5.1 adjusts `f_p` from the unproven backlog smoothed over the difficulty window; design 4.1 adjusts it EIP-1559 style toward `B_p / 2`. And `eth_gasPrice` has no calldata, so your fold returns an average and a modexp-heavy transaction reverts on the budget and pays for it." + +Status: Open, decision named. + +Answer: Correct on both. Fix: one controller definition in both files; `eth_gasPrice` documented as a network-average fold with `eth_estimateGas` (which has the calldata) returning the limit that covers both charges; and the R1 band measurement (pgas / gas inside 0.1 to 10 for 95% of ethereum/tests) as the evidence that the average is usually close. R8 and R11 remain the experiments. + +Evidence: spec 5.1; `docs/design/execution-layer.md` 4.1, 4.3, 9.1. Review id R3.11. + +### P15. RPC blocks are segments, so `gasUsed` can exceed `gasLimit` +"A segment holds up to 180 blocks and your RPC block is the segment. Indexers assert `gasUsed <= gasLimit`." + +Status: Open, minor, extends R10. + +Answer: Correct; spec 7.1 states that a segment's total can exceed `gaslimit`. Fix: report the segment's limit as `k x B_e` in the RPC block, or document the invariant break for Blockscout (R10). + +Evidence: spec 7.1; `docs/design/execution-layer.md` 8.2. Review id R3.12. + +### E9. The specification's year is 365 days; the code's is 365.25 +"Spec 2.5: 31,536,000 DAA seconds a year, halving every 63,072,000, about 31.7098 IGN a second. `igneum.rs`: 31,557,600, 63,115,200, 31.688 IGN. Which one is the money?" + +Status: Open, decision now. + +Answer: Correct. `consensus/core/src/igneum.rs` (`SECONDS_PER_YEAR = 31_557_600`, `HALVING_INTERVAL_SECONDS = 63_115_200`, `BASE_SUBSIDY_PER_SECOND_SOMPI = 3_168_808_781`) and spec 2.5 disagree; `docs/fork-divergence.md`'s per-second table follows the code. Fix: pick one (the code's values are tested and summed under the cap) and rewrite spec 2.5 and the litepaper's schedule to match, with a test that asserts the published numbers against the code. + +Evidence: the two files. Review id R3.19. + +### E10. Reds are paid in the code and "more blocks never means more coins" is false +"Spec 2.5: red blocks are unpaid, emission is keyed to DAA score so more blocks never means more coins. `coinbase.rs` pays a red's 80% to the merging miner and its 20% to the pool, and every blue block in the DAA window mints `E(daa)`. Tonight your chain minted 4.7x the schedule for eight minutes." + +Status: Conceded, text fix named. + +Answer: Correct. `consensus/src/processes/coinbase.rs:102` to `109` follows Kaspa's rule for reds; `block_subsidy` is paid per blue (and red) block in the DAA window, so coins are blocks times `E` and the controller's rate sets the short-run emission, as on every proof-of-work chain. Timestamps still cannot mint. Fix: spec 2.5 to say what the code does (reds paid to the merger, emission per block under the controller's rate, the cap unaffected), or the code to say what the spec does; the review recommends the code's rule. + +Evidence: the files; `docs/review/round-3-2026-10-03.md`, "Tonight's devnet". Review id R3.20. + +### E11. The homepage burns job fees at launch +"'Jobs through Igneum's own market are paid in IGN and 10% of each fee is burned', with a tile 'IGN burned from jobs, at launch'. Your spec 5.4 settles launch jobs on the customer's chain with no burn. You fixed the litepaper (P10) and left the homepage." + +Status: Conceded, fix now. + +Answer: Correct. Fix: `site/index.html`, "Proofs sold to other chains": "Jobs are paid on the customer's chain at launch; settlement in IGN with a 10% burn follows the proof bridge in phase two", and the tile's label to "phase two". Extends P10 and overclaim 52. + +Evidence: spec 5.4; `site/index.html`. Review id R3.24. + +### C13. Monero's seven years do not price a 256 MiB SRAM die +"RandomX's cache is 256 MiB too. Nobody built the die for Monero because the prize was small. That is not evidence about the die." + +Status: Conceded, label needed; extends C2. + +Answer: Correct. The precedent argument transfers the cache size and not the economics; see M16 for the pricing and the lever. + +Evidence: none beyond M16. Review id R3.16. + +### L7. "Where the price comes from" +"A heading in a document that says it is not an offer, followed by 'both reduce supply as they happen'. That is a value-accrual argument under a heading about price." + +Status: Open, counsel; text fix now. + +Answer: Correct as a reading. Fix: the heading becomes "Where fees go" and the sentence "Demand comes from use of the chain and from outsiders buying proofs, and both reduce supply as they happen" is deleted; counsel reviews the Economics section (L1, L2). + +Evidence: `site/litepaper.html`, Economics. Review id R3.23. + +### L8. Third-party names as implied outcomes +"'Native USDC is requested from Circle during public testnet.' 'Canto and Blast proved builders come for this.' Names of companies next to outcomes you do not control." + +Status: Conceded, wording fix. + +Answer: Correct. Fix: "The project will ask Circle for native USDC during public testnet; whether it is issued is Circle's decision" and the Canto and Blast sentence as overclaim 39 already rewrites it. + +Evidence: `site/litepaper.html`, Building, Questions builders ask. Review id R3.25. + +### G9. The release-key steward is one person, and a lost key cannot be revoked +"Spec 8.2: the key that signs the software every miner runs is held by 'the steward named in the published key policy', policy deferred to testnet. And item 5 says a lost key is revoked by its own last signed release, which a lost key cannot sign." + +Status: Open, policy and rule fix named; extends O-8.1. + +Answer: Correct on both. The steward is the control point a regulator writes down, and the revocation path as written freezes the update channel for ever on a lost key. Fix: a pre-signed revocation certificate held apart from the signing key, or a 2-of-3 key set with the policy published before the client ships rather than before testnet; and the entity and jurisdiction that employ the steward named with it (L1). + +Evidence: spec 8.2. Review id R3.21. + +### G10. The signalling default on first run +"8.3 says the default is the choice the user last made. On first run there is none." + +Status: Open, minor. + +Answer: Correct. Fix: first run signals nothing until the user chooses, shown in the interface. + +Evidence: spec 8.3 item 2. Review id R3.22. + +### X12. Tonight's numbers are quoted before they are logged, and the worker efficiency gap is unexplained +"The 50x step, the trough near 9 million, 5.5 blocks a second and two-thirds efficiency over eight processes are in the brief and not in the bench-log. And the chain saw 70 to 83 MH/s from a card that benches 229, which is a third, not two thirds." + +Status: Conceded, fix now. + +Answer: Correct. The bench-log is append-only and the project's rule is that a number exists when it is logged with its command. Tonight's run needs its entry: the step profile, the retarget trajectory from the CSV (134.2 M to 14.2 M expected hashes by DAA 812, further per the brief), the 2-minute block-rate buckets, the epoch-boundary gap, and the launcher's summed status against the block rate, with the gap explained (template age, sibling blocks dropped by the one-block-per-job rule, or queue stalls across eight processes). + +Evidence: `/tmp/igneum-devnet/node1.log`, `sim/difficulty/devnet-2026-10-03.csv`, the launcher log on the PC. Review id R3.15. diff --git a/docs/review/round-3-2026-10-03.md b/docs/review/round-3-2026-10-03.md new file mode 100644 index 000000000..e3c30cdab --- /dev/null +++ b/docs/review/round-3-2026-10-03.md @@ -0,0 +1,409 @@ +# Igneum hostile review, round 3: the specification and the running code + +Date: 3 October 2026, evening. Rounds 1 and 2 reviewed the design document. This round reviews what exists: `docs/spec/00` to `08`, `docs/design/execution-layer.md`, `docs/fork-divergence.md`, `docs/bench-log.md`, `docs/analysis/weak-program-census-2026-10-03.md`, `sim/results_v2.md`, `sim/difficulty/`, `docs/fud-ledger.md`, `docs/provenance.md`, the public text of `site/index.html` and `site/litepaper.html`, and the fork code where a claim depends on it: `vendor/igneum-node/consensus/pow/src/igneum.rs` (the engine hook), `consensus/core/src/igneum.rs` (emission), `consensus/src/processes/coinbase.rs` (the split), `consensus/core/src/hashing/header.rs` and `header.rs` (`vote_key_hash`), `consensus/src/pipeline/header_processor/processor.rs` (the epoch seed and where the PoW check runs), `consensus/core/src/config/constants.rs` and `consensus/src/processes/difficulty.rs` (the DAA), `consensus/src/processes/pruning_proof/validate.rs` (block levels), `igneum-pow/src/bind.rs` (the header binding), and tonight's devnet record (`/tmp/igneum-devnet/node1.log`, `sim/difficulty/devnet-2026-10-03.csv`, `proto-cuda/windows-miner/start-mining.ps1`). + +Seven reviewers, each a role with a worldview, each in the first person. Each delivers three attacks concrete to this code and this spec, with the expected outcome if the design is right and the evidence that is still missing; one sentence of public text with the file and the fix; and the measurement that would change their mind. Every finding carries a rank: + +| Rank | Meaning | +|---|---| +| Fatal | Stops the roadmap as written | +| Serious | Must be fixed before the public testnet | +| Minor | Fix when convenient | +| Already answered | The ledger holds it; the id is cited and the entry is extended, not repeated | + +New findings carry ids `R3.n` here and are appended to `docs/fud-ledger.md` under the section letters (M mining, F finality, P proving and execution, E economics, C comparisons, L legal, X operations, G governance). Nothing found tonight is fatal. The count: 0 fatal, 18 serious, 8 minor, and 22 ledger entries cited as already answered (some extended with a new experiment). + +## Tonight's devnet, as the log shows it + +Read from `node1.log` (3,602 accepted PoW lines, 20:07:50 to 21:12:14 BST) and the CSV snapshot (818 rows to DAA 813). Genesis bits `0x1d100000`, 2^28 = 134,217,727 expected hashes per block. The brief's figures (a 50x hash-rate step when the PC joined, a trough near 9 million, 5.5 blocks per second, two-thirds worker efficiency over 8 processes) are not yet in `docs/bench-log.md`; where this review uses them it says so. + +| Window (BST) | Blocks/s, 2-min buckets | DAA score | What was happening | +|---|---|---|---| +| 20:07 to 20:28 | 0.07 to 0.17 | 0 to 139 | Metal worker alone at about 30 MH/s against 2^28 | +| 20:28 to 20:41 | 0.01 | 140 to 141 | no miner; the Metal worker log ends 20:28 | +| 20:42 to 20:55 | 0.42 to 0.62 | 142 to 592 | the PC, 8 identities per vendor; 0.52 to 0.62 blocks/s at 2^28 is 70 to 83 MH/s effective against the 229 MH/s bench | +| 20:56 to 21:02 | 3.14, 5.32, 5.67, 4.62 | 593 to 2,841 | first retarget at 600 blocks (150 samples) averages the whole slow history; expected hashes fall from 134.2 M to 18.3 M (DAA 715) and 14.2 M (DAA 812, end of the CSV); about 9 M at the trough per the brief | +| 21:04 to 21:12 | 1.47 to 1.66 | 2,841 to 3,599 | still 50% over target an hour after the step | +| 21:12:14 to end of capture (21:13:45) | 0 | 3,599 | epoch boundary; every AOT worker exits 42 and rebuilds; no block accepted in at least 90 s | + +Two derived numbers used below. Between 20:56 and 21:04 the chain produced 2,248 blocks against 480 expected, so 4.7x the emission schedule for eight minutes. Between 20:55 and 21:12 the chain advanced 3,006 DAA seconds in 17 wall minutes, so every DAA-denominated clock (merge depth, the 30-day vote window, the 2-hour presence window, the hourly epoch) ran 2.9x fast. + +--- + +## 1. Kaspa core developer + +I wrote and reviewed the code this fork sits on. I care about three invariants rusty-kaspa keeps: block validity is a function of the block and its past, finality is a pure function of the DAG, and the DAA is a block-count window that assumes hashrate moves slowly relative to the window. The fork breaks the first in two places, overlays the second, and launched into the third tonight. + +### Attack 1. Pulse a rental against the block-count DAA and buy weight at a discount + +What I do. I bring 50x the honest hashrate for about two minutes, leave, and repeat once an hour. Kaspa's `calculate_difficulty_bits` keeps the 661 most recent samples (one per 4 blocks, `DIFFICULTY_SAMPLED_WINDOW_SIZE`) and scales the average target by measured over expected duration. During my burst the window still holds the slow history, so I mine at the old target until about 2,644 blocks have flushed it: at 50 blocks a second that is about a minute. I get on the order of 5,000 blocks per pulse, 98% of them mine. When I leave, the honest network at 1/50 of the difficulty produces 0.02 blocks a second and the window, which only moves as blocks arrive, takes hours to ease (measured duration grows as `max_ts - min_ts` of the samples, so the target eases by about 1.4x per wall hour of crawl). Weight under W2 is counted in blocks over a window of 2,592,000 DAA seconds, and DAA seconds are blocks. So each pulse adds about 5,000 attacker blocks to the window while the honest crawl adds a few hundred. At one pulse an hour I hold a third of the window's blocks in about a week, against 16 days by the formula for the same average hashrate (a = 1.6), and the chain crawls 58 minutes of every hour while I do it. Tonight's honest step shows the mechanism: 5.67 blocks a second for two minutes, 4.7x the block supply for eight. + +If the design is right. A pulse buys under two windows of blocks and the day counts move by under 2x; the crawl is visible on every chart; and a controller that tracks the step inside a few minutes (the two-lane candidate in `sim/difficulty/sim.py`) shrinks the amplifier further. + +Evidence missing. `sim/results_v2.md` runs every scenario with a perfect retarget (its own assumption table). No finality run has a DAA model (O-3.9 lists it). `sim/difficulty/sim.py` exists with a `polluted` profile built from tonight, and no result from it is in the bench-log. The amplifier's size is unmeasured. + +Rank: serious. New: R3.1 (ledger M14, F14). Extends O-2.1 and O-3.9. + +### Attack 2. Reorg the chain two hours back, because merge depth is not the reorg bound + +What I do. I mine a private chain forked two hours ago with more blue work and release it. Spec 2.1 and 3.9 and the litepaper call the 3,600-second merge depth "the one-hour depth limit" and say exchanges may credit at that depth in month one. In the code, `check_bounded_merge_depth` (`post_pow_validation.rs:79`) only forbids a block from merging a red that is not in the future of its merge-depth root, unless a kosherizing blue covers it. It does not stop the virtual from switching to a heavier selected chain. What stops a switch is the finality point (`virtual_processor/processor.rs:1626`, `FINALITY_DURATION` = 43,200 DAA seconds, 12 hours at one block a second), and the fork keeps that value as a "backstop". So in month one, under the proposed 3.8 rule of no certificates, the reorg bound an exchange can rely on is 12 hours of DAA time, not one. The old chain's blocks beyond merge depth are simply never merged; their transactions vanish from the sequence. The litepaper's "double-spend bounded to one hour" rests on the wrong constant. + +If the design is right. A heavier chain forked 2 hours ago is rejected by every node. The code says it is accepted. + +Evidence missing. No reorg test exists in the fork (O-2.2's two-miner devnet has not run). Nobody has stated which depth a month-one deposit should wait for. + +Rank: serious. New: R3.2 (ledger F15). Public text: litepaper "Finality" paragraph, "What Igneum does not claim" item 4; spec 3.8, 3.9. + +### Attack 3. Serve a pruning proof, or try to sync from one + +What I do. Two things. First I wait for the devnet to pass its pruning depth (108,000 blocks, 30 hours at target rate, sooner after tonight's overshoot) and start a fresh node against it. `pruning_proof/validate.rs:192` checks proof headers with `calc_block_level_check_pow`, which runs kHeavyHash. The proof's headers were mined under the lottery, so their kHeavyHash values are uniform over 256 bits against a target of about 2^228: each passes with probability 2^-28. The fresh node rejects an honest proof and never syncs. Second, if that check were loosened to levels only, I would build a fake proof with a kHeavyHash ASIC, which exists, because block levels for proofs are computed from a hash no Igneum miner computes. + +If the design is right. A node syncs from a pruning point produced by a lottery-mined chain and a proof built under the stub is rejected. `docs/fork-divergence.md` already records that pruning-proof validation uses the stub and that seeds must be threaded through before a pruning network. + +Evidence missing. No node has synced from a pruning point. The devnet will cross pruning depth tonight. + +Rank: serious before testnet, acknowledged. R3.3 (ledger M20; extends the fork-divergence open decision and fork map a4). + +Also noted, minor: GHOSTDAG k = 18 is Kaspa's value for a 5-second delay bound with Kaspa-sized bodies. Igneum bodies carry EVM transactions and proof records of recursive proofs; k must be re-derived from a measured delay with proof-bearing bodies (R3.4, ledger M21, extends O-2.2). And the PoW check now runs after GHOSTDAG and before `pre_pow_validation` (`processor.rs:295` to `301`), which the fork file flags as a cost; the RandomX author below turns it into a specific attack. + +### (b) The sentence I would quote back + +`site/litepaper.html`, Finality: "And Kaspa's one-hour merge-depth bound limits any reorganisation beneath the latest lock." Fix: "Kaspa's merge-depth rule stops blocks older than an hour being merged. The depth that stops a reorganisation is the finality depth, 12 hours at one block a second, until a certificate is newer than that." + +### (c) What would change my mind + +A simnet run where a private chain with more blue work, forked 2, 6 and 13 hours back, is released: the node must refuse it at the depth the spec names. And `finality_v2.py` with the Kaspa DAA in the loop and a 50x pulsed renter, reporting the day it crosses a third. + +--- + +## 2. RandomX author + +I built a hash whose only defence is that the cheapest machine for it is one people already own. I judge a lottery hash by three things: whether the attacker who skips the memory is slower than the miner who uses it, whether the generator can emit a program that is cheap for someone, and whether the binding to the block leaves anything reusable. The construction here is sound in shape and unfinished in the places that decide those three. + +### Attack 1. Put the cache on a die and never hold the dataset + +What I do. The miner's honest cost per hash is 128 random 32-byte sector reads from DRAM (under the proposed 16-load rule). The verifier's path, which the chip takes, is: derive each item from the 256 MiB cache with 9 mixer applications of about 130 integer operations (spec 1.8.4) and 8 dependent cache reads (1.8.5). That is about 1,170 operations and 8 reads per item, 150,000 operations and 1,024 cache reads per hash. The "4.8x slower" measured on the M5 Max (`MEMHARD.md` 2.2) is a kernel whose 256 MiB cache lives in DRAM behind a cache it does not fit. A chip with the 256 MiB cache in on-die SRAM pays 1,024 near-free reads and 150,000 integer operations per hash. That is compute bound, and integer throughput per dollar is where a chip beats a GPU. The chip designer prices it below. My point is the construction's: memory hardness here is set by the cache size, not the dataset size, and 256 MiB was chosen to beat a GPU's L2 (spec 1.16: "96 MiB of L2 on the 5090 is the figure to beat"), not a die. + +If the design is right. The inline kernel on the RTX 5090 with a 64 MiB cache, which fits its 96 MiB L2 and so emulates on-die SRAM, is still slower than the honest kernel at 1 GiB. + +Evidence missing. O-1.5 (inline ratio on NVIDIA and AMD) and O-1.6 (time-memory curve, hoisting attacker) have not run. No operation count per item is measured on any device. + +Rank: serious. New: R3.5 (ledger M16). Extends M1 and M2. + +### Attack 2. Mine the hours the generator makes cheap + +What I do. Under the generator in `igneum-pow/src/generator.rs` today, the census says 94.8% of programs re-read addresses inside a hash and distinct loads per hash run 56 to 152 at the 1st to 99th percentile, so the 5090 rate runs 118 to 321 Mhash/s hour by hour (census section 8). I benchmark each program and point my fleet elsewhere for the slow hours. The fix is written (G1 + G2 + R, census section 6, proposed spec text in section 9) and not adopted: the spec still carries the free load count, and the census document that justifies the rule has empty cells where the proposed generator's own numbers go (`FRESH2_DIST`, `FRESH2_P`, `FRESH2_CAND`, section 7.2 `FRESH2_ACCEPTED`, section 7.3 `CF_AGREEMENT`), while the proposed section 1.4.6 cites a rejection rate it calls `REJECT-RATE`. A rule cannot be frozen on a placeholder, and until it is frozen every hour is a different coin. + +If the design is right. Under G1 + G2 + R every hour does 128 distinct reads and the residual spread is 1.10x (census section 5), small enough that the controller absorbs it. + +Evidence missing. The `fixed16-fresh2` census run (its numbers are the blanks); the distinct-versus-static reading on the 5090 (one run, `igneum-second-seed`, predicted 173 against 228 Mhash/s); any GPU number under the new generator. + +Rank: serious. New: R3.6 (ledger M19). Extends M5 and M6 (open). + +### Attack 3. Mine alone for the first ninety seconds of every hour + +What I do. I compile the next program before anyone else can run it. Tonight the log shows the last block of epoch 0 at 21:12:14 and no block to the end of the capture 91 seconds later: the Windows launcher stops every identity on exit code 42, re-exports the pack, rebuilds the CUDA and OpenCL binaries and restarts them (`start-mining.ps1`, `WINDOWS-MINER.md`), while the Metal worker compiles in 6.5 to 129 ms (bench-log). Whoever compiles in process mines the gap alone. In the devnet's v0 seed rule the seed is the last block of the previous epoch, so nobody can compile early and the gap is structural. Under the spec's VDF pipeline the seed is known 1,200 seconds ahead (4.3), so an honest client compiles ahead and the gap is a client bug, not consensus. Either way the official launcher today hands the first 2.5% of every hour to whoever does not use it. + +If the design is right. In-worker compilation (NVRTC, runtime OpenCL) brings the change to under a second, as the Metal path already does. + +Evidence missing. No NVIDIA or AMD run of a runtime-compiled bound kernel (M11, O-1.16). The 90-second figure is one epoch change on one machine. + +Rank: serious for the devnet client, minor for the design. New: R3.7 (ledger M17). Extends M11. + +Also noted, minor: the "per-hash random data path" is one bit of `r0` selecting between two immediates on `add` (spec 1.4.1). A chip computes both and muxes; it costs nothing and defends nothing. The litepaper presents it next to ProgPoW's data-dependent reads, which it is not (R3.8, ledger M18). On the binding: `I = seed_words_from_bytes("igneum-block/" || H || nonce_hi)` with `H` the nonce-zeroed hash including the timestamp (`bind.rs`) is the right shape; a nonce serves one header, and nonce_hi plus timestamp rolling give a miner all the template space it needs. The FNV-plus-SplitMix derivation is M7 and O-1.1, already open, and the test I would add there is a dependency test between the four salt chains over one message. + +### (b) The sentence I would quote back + +`site/litepaper.html`, vs RandomX table: "Per hour, compiled to native GPU code, with a per-hash random data path". Fix: "Per hour, compiled to native GPU code. The per-hash variation is a one-bit select of constants; the defence is the random reads, not the path." + +### (c) What would change my mind + +The inline kernel on the 5090 at a 64 MiB cache (the SRAM-chip emulation) at or below the honest rate, and the time-memory curve of O-1.6 with no point above 1x. For the generator: the filled census and a 10-program run on the 5090 under G1 + G2 within 1.15x of each other. + +--- + +## 3. Ethereum client engineer + +I maintain an execution client. I read `execution-layer.md` as a client spec: what must every node compute identically, what can a block carry that another node cannot check, and what will the first wallet break on. The transaction model (D1 to D5) is careful and the skip rule is the right call for a DAG. Three things are not. + +### Attack 1. Make block validity depend on a node's current selected chain + +What I do. Section 5.5 (D12): "A block is invalid if any proof record it carries ... names a segment not on the node's selected chain", or disagrees with "the node's own native execution of that segment". A node's selected chain is a property of its virtual, not of the block's past. Two honest nodes with different tips, which happens every second on a DAG, can disagree on whether a block carrying a record for a recently reorged segment is valid. I mine a block carrying a correct record for a segment on the chain I see, propagate it during a one-block reorg, and watch half the network mark it invalid and the other half build on it. The hidden-block penalty was removed in round 2 for exactly this reason: validity must be a function of the block and its past. + +If the design is right. The rule is relative to the carrying block's own selected-parent chain: a record is valid if the named chain block is on that chain and `post_root` equals the execution of that segment along that chain, which every node computes identically from the block's past. + +Evidence missing. None needed; the text is the finding. The fix is a sentence in 5.5 and in spec 7.2 item 5. + +Rank: serious. New: R3.9 (ledger P11). + +### Attack 2. Aggregate honestly and name myself as every prover + +What I do. `ProofRecord.provers: Vec<(VoteKey, shard_index)>` is "who is paid" (5.4) and the pool is credited to the provers the record names (4.4). The `ProofSystem` trait has `prove_shard(&ShardWitness) -> ShardProof` and `aggregate(prev, shards)`; nothing in the shard proof's statement (5.1: pre-root, transaction list, post-root, receipts) commits to who proved it. I collect the gossiped shard proofs of eight assignees, aggregate them, and write my own key eight times. Full nodes verify the aggregate and the roots and credit me. The 20% of emission that is supposed to pay a standing prover population pays the aggregator. + +If the design is right. Each shard proof's public input includes the prover's payout key, aggregation carries those keys as public outputs, and a record whose `provers` list does not match them is invalid. + +Evidence missing. The shard statement in 5.1 and the trait in 5.6 have no such field. The phase 4 devnet acceptance test A5 checks that the pool is "credited to the recorded provers", which this attack passes. + +Rank: serious. New: R3.10 (ledger P12). + +### Attack 3. Quote a gas price with no transaction and watch wallets revert on the budget + +What I do. 4.1 folds proving gas into `eth_gasPrice` as `f_e + f_p x (pgas_est / gas_est) + tip`. Wallets call `eth_gasPrice` once, with no transaction, so the node can only return a network-average ratio. A transaction heavy in `modexp` then carries a budget `gas_limit x max_fee_per_gas` that covers execution gas at the average ratio and not its own pgas. 4.1 says it "halts as out of gas, reverts, and pays what it consumed up to the budget". So the user pays for a revert the quote caused. I deploy a contract whose cheap-in-gas path is expensive-in-pgas and let every caller who used `eth_gasPrice` pay me a revert. Two other things in the same place: 5.1 of the spec says `f_p` is "adjusted per block from the unproven backlog, smoothed over the difficulty window" while design 4.1 says EIP-1559 toward `B_p / 2`; those are two different controllers. And an RPC block is a segment of up to 180 blocks (7.1), so `gasUsed` can exceed `gasLimit`, which indexers and `ethers` treat as an invariant. + +If the design is right. `eth_estimateGas` with the real calldata returns a limit that covers both dimensions at the quoted price, `eth_gasPrice` documents that it is an average, and the ethereum/tests replay shows `pgas / gas` inside the 0.1 to 10 band for 95% of tests so the average is close for most transactions. + +Evidence missing. R1, R8 and R11 are all phase 2 or 3 and none has run; the pgas table does not exist. + +Rank: serious for the two controller definitions, minor for the quote and the `gasUsed` invariant. New: R3.11 (ledger P14), R3.12 (ledger P15). + +Already answered. The duplicate-inclusion and nonce rules across parallel blocks (D3, D4, spec 2.6) are sound and closed (P5). The one user-facing consequence worth a sentence in the docs: a skipped transaction has no receipt and is re-queued once, so a wallet polling `eth_getTransactionReceipt` can see a transaction vanish without a failure; `igneum_getTransactionStatus` is where it went. + +### (b) The sentence I would quote back + +`site/litepaper.html`, Proving: "Miners claim shards with a small bond, prove them on consumer cards, and the shard proofs are folded by recursive aggregation into one proof for the block." Spec 7.2 decided sortition with no bond on the same day. Fix: "Shards are assigned by lot to eight provers for ten seconds, then open to anyone; there is no bond." (R3.13, ledger P13.) + +### (c) What would change my mind + +The differential plan's "order equivalence" row plus one more: two nodes with different tips validate the same proof-bearing block during a reorg and agree. And a shard proof format whose public outputs name the prover, verified by the test harness in A5. + +--- + +## 4. Operator of a large GPU farm + +I run tens of thousands of cards and I switch them to whatever pays by the hour. I read a new coin for three things: where its difficulty rule lets me take more than my share, how its software lets me multiply my identity, and what it costs me to plug in. Igneum tonight gives me all three. + +### Attack 1. Hop the hours and the window + +What I do. Two levers. The hourly program step: today's generator gives a 2.7x spread in distinct reads per hash across hours (census section 8). I benchmark the next program on one card (under the VDF rule I have 20 minutes to do it) and move the fleet in for the fast hours. Every entry and exit is a hash-rate step against a 44-minute block-count window, and the window lag pays whoever steps: I mine at the old target when I arrive and leave the stay-behinds mining at mine when I go. Tonight's step did this honestly: 5.67 blocks a second, 4.7x the block supply for eight minutes, 1.5x an hour later. The `hop3` and `hop10` profiles in `sim/difficulty/sim.py` are the experiment and no result is logged. + +If the design is right. The 16-load rule removes the per-hour spread, the two-lane controller tracks a step within minutes, and hopping earns less than it costs in switching. + +Evidence missing. Same as R3.1: no controller result in the bench-log, no finality run with a DAA. + +Rank: serious. Same finding as R3.1 from the other side; the ledger entry M14 carries both. + +### Attack 2. Run a hundred keys per card and sweep the shard lottery + +What I do. The project's own launcher runs 8 identities per vendor per PC, each with its own vote key and payout address derived from the PC name and an index (`start-mining.ps1`, `--payout-label nvidia--`; `MINERS` default 8). I set it to whatever I like. For the vote this costs me nothing and gains me nothing, which is the point of W2 (no damping). For shard assignment it is the whole game: spec 7.2 ranks "every eligible key" by a hash and takes the 8 lowest, and eligibility is 100 blue blocks in 30 days, 0.004% of hashrate at one block a second. A farm with 1% of hashrate mines 25,920 blocks a month and can hold 259 keys above dust; with 30% it holds 7,776. The draw is per key, so I hold most of the tickets for every shard and the exclusive 10-second window is mine almost every time. P8 was closed by this rule on the day it was written; the rule reopens it. The same key explosion reaches finality: S1 has every voter sign every checkpoint, S2 switches to sub-user sortition above 8,192 voters with the construction "Open (O-3.5)", and the certificate bitmap size at 10^4 keys is O-3.12. Eight keys per card across a few thousand cards crosses 8,192 on day one, triggered by the official client. + +If the design is right. Sortition is weighted by weight, not by key: draw 8 blue blocks uniformly from the window and take their keys, or rank keys by `H(...) / weight`. Then splitting changes nothing, as it changes nothing for the vote. And the client ships one key per operator by default. + +Evidence missing. 7.2 as written is per key. O-5.1's phase 4 test ("the fastest prover wins under 25% of shards") would not catch a key-splitter, who wins by count, not by speed. + +Rank: serious. New: R3.14 (ledger F17 for the key explosion; P8 reopened by cross-reference). + +### Attack 3. What I need before a single card switches + +What I do. I list what is missing and price it. A pool protocol: none (spec 0.1 scopes it out); the job line in `WINDOWS-MINER.md` (`job `) is a worker protocol, not stratum, and shares on a 64-bit lane hash with vardiff are unspecified. One worker per card: the launcher runs one process per identity because "one shared worker per vendor would need a multiplexing protocol and is not built", and the brief's two-thirds efficiency over 8 processes is the price; the chain tonight saw 70 to 83 MH/s effective from a card that benches 229, which is nearer a third, and nobody has explained the gap between the launcher's status lines and the block rate (template age, sibling blocks dropped, queue stalls). Memory: 1.3 GiB per identity at the 1 GiB prototype dataset, 2.3 GiB at the 2 GiB genesis size, so the homepage's 4 GB card runs one identity and nothing else. Hourly rebuilds: the 90-second gap of R3.7. HiveOS: promised "on day one", nothing exists. + +If the design is right. A stratum with the seed pair in the job, in-worker compilation, one worker per card, and a measured efficiency within 10% of the bench. + +Evidence missing. All of it; M11 and O-1.16 cover the compile, nothing covers the protocol. + +Rank: minor as design, serious as a testnet gate (the 1,000-miner gate is unreachable without it). New: R3.15 (ledger X12 for the unexplained efficiency gap; the protocol work is M11 and O-1.16). + +### (b) The sentence I would quote back + +`site/index.html`, "This hour's mining program": "A new random program every hour. A GPU compiles it in seconds." Fix: "The Metal worker compiles it in under a second. The CUDA and OpenCL workers are rebuilt ahead of time and paused for 90 seconds at tonight's epoch change; in-worker compilation is the fix and is not yet built." + +### (c) What would change my mind + +A mixed NVIDIA and AMD rig through 24 epoch changes with in-worker compilation: outage per change under 2 seconds, zero failed rebuilds. The controller simulation's `hop10` row in the bench-log with the hopper's excess under 2% of its share. And the sortition rule rewritten by weight, with a test where 1 key and 1,000 keys of equal weight win the same number of shards. + +--- + +## 5. Chip designer + +I design silicon for proof-of-work. I price a chip in three steps: what the honest machine is bound by, what a machine that skips the bound costs, and what the project would have to change to stop me. I use only this repository's measurements and label the rest approximate. + +### Attack 1. Price the recompute chip from the measured numbers + +What I do. The honest 5090 is bound at 23.7 G random 32-byte sector loads a second (bench-log, 5090 sweep), 18.0 G distinct loads after repeats (census section 5), which is 141 Mhash/s at 128 distinct loads. The recompute path, from spec 1.8: 9 mixer applications of about 130 integer operations per item and 8 dependent 64-byte cache reads; 128 items per hash under the 16-load rule. + +| Quantity | Value | Source or label | +|---|---|---| +| Honest cost per hash | 128 random 32-B DRAM sector reads | spec 1.4.2 (proposed), bench-log | +| Honest 5090 rate | 141 Mhash/s | census section 8, projection from 18.0 G distinct loads/s | +| Recompute cost per hash | about 150,000 int32 operations and 1,024 cache reads | 128 x (9 x 130 + 8), spec 1.8.4 and 1.8.5, approximate | +| Int32 throughput of a 5090-class die | about 50 T operations/s | from the shader count and clock, approximate | +| Recompute rate on that die with the cache in on-die SRAM | about 0.33 Ghash/s | 50e12 / 150e3 | +| Gain over the honest 5090 | about 2.4x at the same silicon budget | ratio of the two rows | +| SRAM for a 256 MiB cache | about 250 to 300 mm^2 on a current node | about 1 MB/mm^2, approximate, from wafer-scale parts | +| SRAM read rate the recompute needs | 1,024 x 0.33 G = 340 G reads/s of 64 B, about 22 TB/s | banked on-die SRAM reaches this, approximate | + +So a die of the 5090's class with the whole 256 MiB cache on it and integer pipelines in place of graphics gains about 2.4x on the arithmetic alone, before the usual chip-versus-GPU integer efficiency (a factor of several, approximate). The design's own target of "under 2x" is not met by this construction at 256 MiB. The measured 0.21 ratio on the M5 Max is a cache that does not fit the chip it ran on; it says nothing about a chip built around the cache. + +If the design is right. The 5090's inline kernel with a 64 MiB cache, which sits inside its 96 MiB L2 and so emulates on-die SRAM, is still slower than the honest 1 GiB kernel. + +Evidence missing. O-1.5 and O-1.6, plus a measured operation count per item. + +Rank: serious. Same finding as R3.5 (ledger M16), priced. + +### Attack 2. Price the memory-controller chip + +What I do. Keep the dataset in DRAM and build a better random-access controller. The 5090 achieves 23.7 G sectors a second against about 56 G theoretical (1.8 TB/s over 32 B), so a controller that reaches the DRAM's own random limit gains at most about 2.4x, approximate, and HBM buys bandwidth more than random access per dollar (M10). This chip stays under the project's 2x target or near it, and it is the one the litepaper argues against. It is not the one I would build. + +If the design is right. A pure random 32-byte read microbenchmark on the 5090 shows the 23.7 G figure is already the memory's limit, leaving the controller chip under 1.5x. + +Evidence missing. That microbenchmark. + +Rank: already answered (M1, M10), with the microbenchmark added as the evidence to collect. + +### Attack 3. What I would build first, and the FPGA question + +What I do. Not a chip for any program. An interpreter core: 11 integer families (plus the reserve, all simple ALU operations by 1.13.2), 8 registers of 32 bits, a 32-lane xor shuffle. That is a tiny in-order core; the hourly program and the era draws cost it nothing. Beside it, fixed-function ChaCha and ARX pipelines for the mixer and the 256 MiB SRAM cache of attack 1. The program changing hourly is a defence against a chip that bakes in a program, which nobody would build. FPGA: no. An HBM FPGA's random sector rate and its integer throughput are both below a 5090's at a higher price, approximate, and the mixer pipelines want ASIC density. The first thing I need from the project is the cache size, because that one number decides whether my die fits. + +If the design is right. The cache is sized to exceed any single die's SRAM, 1 GiB or more at genesis and growing, and the mixer cost per item is set so that recompute at on-die speed is slower than a DRAM read; both are open prototype values (spec 1.16) and 1 GiB fills in about 0.7 s on one core (from the measured 0.18 s for 256 MiB), so the verifier survives. + +Evidence missing. The decision; the measurement of attack 1; a CPU fill and verify time at 1 GiB. + +Rank: serious, the design change half of R3.5. Comparison note for the ledger: Monero's seven years without a public chip do not price this die either, because RandomX's cache is also 256 MiB and Monero's prize was small (R3.16, ledger C13). + +### (b) The sentence I would quote back + +`site/litepaper.html`, Mining: "A chip that dropped the graphics parts and kept the parallel cores and the memory would gain under 2x, approximate, which is below what pays for a tapeout, and that is the same margin that has protected Monero for seven years." Overclaim 18 already replaces it with a target; add to the replacement: "The lever that sets the gain is the size of the cache the verifier holds, which must not fit on one die." + +### (c) What would change my mind + +`--inline-dataset` on the 5090 at 64 MiB and 256 MiB caches against the honest 1 GiB kernel, and the time-memory curve of O-1.6. If recompute at a 64 MiB cache is at or below the honest rate, the SRAM chip is dead and I am wrong. + +--- + +## 6. Exchange listing engineer + +I decide when a deposit is credited. I need one word, "final", with one meaning, a reorg depth I can put in a config file, and a node flag that never lies. The spec gives me a flag, a depth that is the wrong constant, and a lock that can be undone. + +### Attack 1. Month one: credit at the depth the spec says and get reorged + +What I do. Spec 3.9 and the litepaper tell me that with `finality_active` false I should "credit nothing below 3,600 DAA s of depth". Two problems. The reorg bound in the code is the finality depth, 43,200 DAA seconds (R3.2), so a 2-hour-old deposit can be reorged by a heavier chain. And the depth is in DAA seconds: tonight DAA time ran 2.9x faster than the wall clock for 17 minutes, and a 50x burst runs it 50x faster. A rule that says "3,600 DAA s" is a rule whose wall-clock meaning the hashrate sets. I would set 12 hours of median time plus my own hashrate judgement, and I would expect the spec to say so. + +If the design is right. The month-one bound is the finality depth and the guidance names it in median time. + +Evidence missing. R3.2's reorg test. + +Rank: serious. Part of R3.2 (ledger F15) and R3.1 (M14). + +### Attack 2. A lock that becomes uncertified after a heal + +What I do. 3.5's post-heal proposal: when a node holds two certificates at one index, it strikes the equivocators' weight, re-evaluates both against Q3, and "if neither or both still lock, F2 decides and the index is treated as uncertified". So a checkpoint my node reported as locked, on which I credited, can be downgraded after a partition heals. "Locked" is then "locked unless a heal says otherwise". Finality that can be revoked is a confirmation count with a different name, and I would treat it as one until the rule says a certificate my node has verified is never re-evaluated, and the two-certificate case is a chain split handled as Kaspa handles a finality conflict: stop and ask a human. Equivocation costing history and not coins (F6) means the attacker who caused the split loses a month of votes and keeps my deposit. + +If the design is right. A verified certificate is irrevocable; two certificates at one index halt the node's `finality_active` until an operator intervenes; the node never reports a lock it may withdraw. + +Evidence missing. O-3.6 is open and the proposal in 3.5 goes the other way. + +Rank: serious. New: R3.17 (ledger F16). + +### Attack 3. Finality that pauses when two pools go quiet + +What I do. Q3's floor means a lock needs 56.7% of total weight. `sim/results_v2.md` C: 45% silent stalls finality for as long as they stay silent; 40% silent gives 138 intermittent stalls in three days; D: 50% churn stalls 4.1 days. A top pool at 17% plus a regional outage of 25% is a routine Tuesday on a GPU coin. During a pause the node sets `finality_active` false and I fall back to attack 1's guidance. The litepaper says the opposite: "a silent minority cannot freeze finality and a lock never waits for miners who have left". Under the rule as specified, a 42% minority freezes it indefinitely and a lock waits 4.1 days for miners who have left. The sentence was true of the pre-floor rule and nobody updated it. + +If the design is right. The pause is rare, visible, and the flag tells me. The sentence is rewritten. + +Evidence missing. O-3.13's stall test; the uptime model is a guess (results_v2 "cannot tell us"). + +Rank: serious as public text (it misstates a safety property to the people who rely on it). New: R3.18 (ledger F18). The liveness trade itself is stated in spec 3.3.1 and is the project's choice. + +Already answered: the first-month rule (F1, O-3.1), d = 60 as a placeholder (F7), pool concentration of votes (F10), equivocation costing history (F6), inclusion versus finality wording (C5). + +### (b) The sentence I would quote back + +`site/litepaper.html`, Finality: "Only miners who are present count: a key that stops signing drops out of the denominator within two hours, so a silent minority cannot freeze finality and a lock never waits for miners who have left." Fix: "A key that stops signing drops out of the active count within two hours. A lock still needs 56.7% of all 30-day weight, so if more than about 42% of weight goes quiet finality pauses until it returns or ages out, up to 30 days, and the chain runs on proof of work meanwhile. The node reports the pause." + +### (c) What would change my mind + +Thirty days of devnet with partitions and a silent top pool in which `last_certified` never moves backwards and `finality_active` never reports a lock the node later withdraws; and a written month-one rule in median time. + +--- + +## 7. Regulator's analyst + +I read what the project publishes and compare it with what the code does and who holds which key. I am looking for the places where the public description and the mechanism differ, for a person who controls an outcome, and for language that reads as a promise of value. + +### Attack 1. The published money and the coded money differ + +What I do. Spec 2.5: a year is 31,536,000 DAA seconds (365 days), the halving interval 63,072,000, the pre-ramp rate "about 31.7098 IGN per DAA second". `consensus/core/src/igneum.rs`: `SECONDS_PER_YEAR = 31_557_600` (365.25 days), `HALVING_INTERVAL_SECONDS = 63_115_200`, `BASE_SUBSIDY_PER_SECOND_SOMPI = 3_168_808_781` (31.688 IGN). Spec 2.5: "Red blocks are unpaid." `coinbase.rs:102` to `109`: a red block's 80% share and fees go to the merging miner and its 20% to the pool. Spec 2.5: "more blocks never means more coins". The coinbase pays `E(daa)` per blue block in the DAA window, so coins are blocks times `E`, and tonight the chain minted 4.7x the schedule for eight minutes because the controller let it. None of these is large and every one is a sentence a reader can hold against the code. The 8-versus-18-decimals decision is open (fork-divergence) and the cap is 4e17 units in a u64; the spec's "10^18 per IGN" does not fit the type. + +If the design is right. The spec and the code carry one set of numbers and the spec says emission is per blue block under the controller's rate. + +Evidence missing. None; the files disagree today. + +Rank: serious. New: R3.19 (ledger E9), R3.20 (ledger E10). + +### Attack 2. The fee that is not a fee, and the person who holds the key + +What I do. The protocol carries no fee to any team (spec 5.5, 5.6, litepaper). The official client charges "1% dev fee to the founder's company" (litepaper, For miners) and the homepage still says "0% anyone else" and "Not one coin to a founder, a fund or a stake" (E5, overclaim 62, still live). Which company, where, under what terms, is unstated; the entity is "offshore" and unnamed (L1). Then the release key: spec 8.2 puts it in hardware "held by the steward named in the published key policy", policy deferred to the public testnet (O-8.1). One named person signs the software that every miner runs and that creates every miner's wallet and vote key. That person is the control point a regulator writes down, and 8.2 item 5 has a hole: a lost key "is revoked by its own last signed release", which a lost key cannot sign; the only path to a new key is a release signed by the old one, so a lost key is a permanent freeze of the update channel. + +If the design is right. The company, the jurisdiction, the steward and the custody policy are published together before the client ships; revocation uses a pre-signed revocation certificate or a 2-of-3 key set; the homepage line matches the litepaper. + +Evidence missing. All of it is scheduled for later (O-8.1, L1 counsel). + +Rank: serious for the revocation hole and the steward disclosure (new: R3.21, ledger G9); already answered for the fee wording (E5). Minor: 8.3 item 2 says the signalling default is "the choice the user last made" and the first run has none (R3.22, ledger G10). + +### Attack 3. Words that promise value + +What I do. Litepaper, Economics, a heading: "Where the price comes from", followed by a paragraph in which base fees and job fees are burned and "both reduce supply as they happen". That is a value-accrual argument under a heading about price, in a document that also says it is not an offer. "Launch grants. Paid from the founders' own mined coins ... to the first apps that bring users" is the founders funding user acquisition. "Native USDC is requested from Circle during public testnet" puts a third party's name next to an outcome the project does not control; "the way Arbitrum and Base launched" was removed and "Canto and Blast proved builders come for this" remains. The homepage: "Jobs through Igneum's own market are paid in IGN and 10% of each fee is burned" with a tile reading "IGN burned from jobs, at launch", while spec 5.4 settles launch jobs on the customer's chain with no burn (P10 fixed the litepaper; the homepage kept it). And "the people who show up early get the most" is still on the page (overclaim 54, open). + +If the design is right. Headings describe mechanisms, not price; third-party names appear only for things they have done; the homepage matches the spec. + +Evidence missing. Counsel in the project's jurisdiction (L1, L2 open). + +Rank: serious for the heading and the homepage burn line before anything is shared (new: R3.23, ledger L7; R3.24, ledger E11); minor for the third-party names (R3.25, ledger L8). Already answered: L1, L2, X8. + +### (b) The sentence I would quote back + +`site/litepaper.html`, Economics: the heading "Where the price comes from". Fix: "Where fees go", and delete "Demand comes from use of the chain and from outsiders buying proofs, and both reduce supply as they happen." + +### (c) What would change my mind + +A published entity and jurisdiction, a counsel opinion on the litepaper in that jurisdiction, a steward policy with a working revocation path, and a test in the repository that reads the emission numbers from the litepaper and asserts them against `igneum.rs`. + +--- + +## Ranking of every finding + +| Id | Finding | Reviewer | Rank | Ledger | +|---|---|---|---|---| +| R3.1 | Pulsed rental against the block-count DAA buys weight at about 2x the formula and crawls the chain; finality numbers assume a perfect retarget; no controller result logged | Kaspa, farm | Serious | M14, F14 (new); extends O-2.1, O-3.9 | +| R3.2 | Merge depth is not the reorg bound; the finality depth (12 h) is; spec 3.9 and the litepaper credit at one hour | Kaspa, exchange | Serious | F15 (new) | +| R3.3 | Pruning-proof validation runs the kHeavyHash stub: no sync from a pruning point; a kHeavyHash ASIC forges proof levels | Kaspa | Serious | M20 (new); acknowledged in fork-divergence | +| R3.4 | GHOSTDAG k = 18 taken from Kaspa's table without a delay measurement with EVM and proof bodies | Kaspa | Minor | M21 (new); extends O-2.2 | +| R3.5 | The 256 MiB cache fits on a die; the recompute attacker is compute bound at about 150,000 operations per hash; about 2.4x at equal silicon before chip efficiency | RandomX, chip | Serious | M16 (new); extends M1, M2 | +| R3.6 | Census placeholders: the proposed generator's numbers and the acceptance rule's rejection rate are blanks; the spec still carries the free load count | RandomX | Serious | M19 (new); extends M5, M6 | +| R3.7 | Epoch-boundary outage of ahead-of-time workers, at least 90 s measured tonight; first blocks of every hour go to whoever compiles in process | RandomX, farm | Serious (client) | M17 (new); extends M11 | +| R3.8 | "Per-hash random data path" is a one-bit immediate select; no chip cost | RandomX | Minor | M18 (new) | +| R3.9 | Native-execution veto references the node's current selected chain; block validity is not a function of the block's past | Ethereum | Serious | P11 (new) | +| R3.10 | Shard proofs do not commit to the prover; an aggregator rewrites the `provers` list and takes the pool | Ethereum | Serious | P12 (new) | +| R3.11 | Two definitions of the proving base fee (backlog in spec 5.1, EIP-1559 in design 4.1); `eth_gasPrice` without calldata cannot fold pgas, so the budget cap causes paid reverts | Ethereum | Serious | P14 (new) | +| R3.12 | RPC blocks are segments: `gasUsed` can exceed `gasLimit` | Ethereum | Minor | P15 (new) | +| R3.13 | Litepaper still says shards are claimed with a bond; spec 7.2 says sortition, no bond | Ethereum | Minor (text) | P13 (new) | +| R3.14 | Keys are free and the official launcher mints 8 per card: shard sortition per key is sweepable (P8 reopened); voter count crosses the 8,192 switch with the construction unspecified | Farm | Serious | F17 (new); P8 cross-referenced; extends O-3.5, O-3.12 | +| R3.15 | The chain saw 70 to 83 MH/s effective from a 229 MH/s card over 8 processes; the gap to the launcher's own figure is unexplained; tonight's numbers are not in the bench-log | Farm | Minor | X12 (new) | +| R3.16 | Monero's seven years do not price a 256 MiB SRAM die; RandomX has the same cache size | Chip | Minor | C13 (new); extends C2 | +| R3.17 | A lock can become uncertified after a heal under the 3.5 proposal; "locked" is revocable | Exchange | Serious | F16 (new); extends O-3.6 | +| R3.18 | Litepaper: "a silent minority cannot freeze finality"; under the floor 42% silent freezes it indefinitely | Exchange | Serious (text) | F18 (new) | +| R3.19 | Spec year 365 days, code 365.25; halving interval and per-second rate differ | Regulator | Serious | E9 (new) | +| R3.20 | Spec says reds unpaid and "more blocks never means more coins"; code pays reds to the merger and mints per blue block under the controller's rate (4.7x for 8 min tonight) | Regulator | Serious | E10 (new) | +| R3.21 | The release-key steward is a single control person; 8.2 item 5 cannot revoke a lost key | Regulator | Serious | G9 (new); extends O-8.1 | +| R3.22 | Signalling default on first run undefined | Regulator | Minor | G10 (new) | +| R3.23 | Litepaper heading "Where the price comes from" and the burn-reduces-supply sentence | Regulator | Serious (text) | L7 (new); extends L2 | +| R3.24 | Homepage: jobs paid in IGN and burned "at launch", against spec 5.4 | Regulator | Serious (text) | E11 (new); extends P10 | +| R3.25 | Third-party names as implied outcomes (Circle, Canto, Blast) | Regulator | Minor | L8 (new) | +| R3.26 | Pre-validation PoW cost: a header with any past-day timestamp or any claimed DAA score forces a 256 MiB cache fill and evicts the honest entries before the checks that would reject it | Kaspa (from the fork file's own flag) | Serious | M15 (new); extends fork-divergence "PoW before or after GHOSTDAG" | + +R3.26 in full, because it was found reading the processor and belongs to the Kaspa developer. `validate_header` (`processor.rs:293` to `301`) runs `validate_header_in_isolation` (which checks the timestamp only against the future, `pre_ghostdag_validation.rs:48`), GHOSTDAG, then `check_pow_and_calc_block_level`, and only then `pre_pow_validation`, which is where `check_difficulty_and_daa_score` rejects a wrong `daa_score` and the past-median rule rejects an old timestamp. The engine keys the program on the header's own `daa_score` (`epoch_seed`, `processor.rs:325`) and the cache on the header's own `timestamp` (`day_index`), and keeps three entries (`IgneumEngine::KEEP`). A peer sends headers with random past days or a `daa_score` in a far epoch: each costs the node a 256 MiB ChaCha12 fill (0.18 s on one core) and a program generation, evicts the honest epoch, and the next honest header rebuilds it. The per-peer header rate is the only limit. The fix is in the five below. + +Already answered, cited and not repeated: M1 and M10 (chip target, random access), M5 and M6 (load count, weak programs: open, now with a measured census), M7 and O-1.1 (seed derivation), M11 (runtime compile), F1 and O-3.1 (first month), F6 (equivocation), F7 (d = 60), F10 (pools hold votes), P5 (EVM semantics, closed), P7 (soundness bug, with 5.5's veto now specified), P10 (burn contradiction, litepaper half), E5 (client fee), G2 (method), L1 and L2 (counsel), C4 (overlay changes GHOSTDAG), C5 (inclusion versus finality), X2 and X8 (button, listings), fork-divergence's own open decisions (PoW after GHOSTDAG, pruning levels, decimals, epoch seed, day seed). + +## The five findings to fix first, in order + +1. **R3.26, the pre-validation PoW cost.** In `validate_header`, run `check_difficulty_and_daa_score` and the past-median timestamp check before `check_pow_and_calc_block_level`, so the epoch and day used for the PoW check are the ones the chain will accept; derive the day from DAA score as spec 1.12 says, or from the selected parent's timestamp window rather than the header's own; raise `KEEP` to 4 so an epoch change across a day boundary never evicts a live pair; and cap cache builds per peer per minute. A day of code in `processor.rs` and `igneum.rs`, and it closes a live hole in the running devnet. + +2. **R3.9, the native-execution veto.** Rewrite `execution-layer.md` 5.5 and spec 7.2 item 5: a proof record is valid if the chain block it names is on the carrying block's own selected-parent chain and its `post_root` and receipts equal the execution of that segment along that chain. Validity is then a function of the block and its past, as every other rule in the fork is. Add the two-node reorg test to the differential plan. + +3. **R3.1, weight under a lagging DAA.** Two changes and one experiment. Denominate the vote window and the presence window in past-median time, not DAA score, so a burst cannot age the window. Normalise weight per unit of time: W2 becomes a key's share of the blue blocks in each 60-second median-time bucket, summed over the trailing 30 days, so 50x the blocks in one minute is one minute of weight. Then run `finality_v2.py` with the Kaspa DAA (and the two-lane candidate) in the loop against a 50x pulsed renter, and put `sim/difficulty/sim.py`'s results, including the `polluted` and `hop10` profiles, in the bench-log and pick the controller at gate 2. + +4. **R3.14, keys are free and the client mints them.** Spec 7.2 step 2: draw the 8 assignees by weight (8 blue blocks uniformly from the window, take their keys; a key drawn twice holds one slot and the draw continues), so splitting changes nothing, as it changes nothing for the vote. The official client defaults to one vote key per operator and the launcher's `MINERS` setting multiplies workers, not keys. Specify S2 (O-3.5) before the count can cross 8,192, which the current launcher reaches with about a thousand PCs. + +5. **R3.5, the cache on a die.** Run `--inline-dataset` on the RTX 5090 at 64 MiB and 256 MiB caches against the honest 1 GiB kernel this week; it is the chip emulation and it is cheap. Whatever it shows, change the gate 1 rule for the cache size from "exceed the largest GPU L2" to "exceed what one die can hold, and grow", with 1 GiB at genesis as the candidate, and re-measure the CPU fill (expected about 0.7 s on one core) and verify time at that size. If the inline kernel at 64 MiB is still below the honest rate, the design holds at 256 MiB and the rule still says why. + +Next three, each an hour: R3.19 and R3.20 (pick the code's 365.25-day year and the merger-paid reds, or change the code; rewrite spec 2.5 to say coins are blocks times `E` under the controller's rate); R3.2 (name the finality depth as the month-one bound, in median time, in spec 3.8, 3.9 and the litepaper); R3.18, R3.13, R3.24 and R3.23 (four sentences on the public pages).