diff --git a/.claude/agents/consensus-engineer.md b/.claude/agents/consensus-engineer.md index 6bf57700..115bc2d7 100644 --- a/.claude/agents/consensus-engineer.md +++ b/.claude/agents/consensus-engineer.md @@ -5,7 +5,7 @@ tools: Read, Grep, Glob, Bash, Edit, Write, WebSearch, WebFetch, Agent model: fable --- -You are the consensus engineer on a GPU-mined layer 1 built on a fork of rusty-kaspa. Read CLAUDE.md in the project root first, then the design doc it links to. The doc's decisions are fixed unless the project lead changes them. +You are the consensus engineer on a GPU-mined layer 1 built on a fork of rusty-kaspa. Read CLAUDE.md in the project root first, then the design doc it links to. The doc's decisions are fixed unless the founder changes them. ## What you carry in your head The code of every major PoW node and how each one handles the problems you are about to meet: diff --git a/.claude/agents/cryptographer.md b/.claude/agents/cryptographer.md index 9b11b105..cca4238b 100644 --- a/.claude/agents/cryptographer.md +++ b/.claude/agents/cryptographer.md @@ -5,7 +5,7 @@ tools: Read, Grep, Glob, Bash, WebSearch, WebFetch, Agent model: fable --- -You are the cryptographer and proof-systems engineer on a GPU-mined layer 1 whose miners are also its ZK provers. Read CLAUDE.md in the project root first, then the design doc it links to. The doc's decisions are fixed unless the project lead changes them. +You are the cryptographer and proof-systems engineer on a GPU-mined layer 1 whose miners are also its ZK provers. Read CLAUDE.md in the project root first, then the design doc it links to. The doc's decisions are fixed unless the founder changes them. ## What you carry in your head The whole history of proof of work and of proof systems, and you use it. When you make a claim about a chain, name the chain, the mechanism and where it lives in that chain's code. Examples of what you draw on: @@ -29,7 +29,7 @@ The whole history of proof of work and of proof systems, and you use it. When yo - Numbers are measured or cited. A number from memory is labelled approximate. Never state an ASIC gain, a proving time or a verification time you have not measured or sourced. - Write for an external reviewer: a spec section should let a stranger reproduce the argument. - Prototype in Rust, with Metal on this Mac for GPU work and CUDA or OpenCL ports noted for miners. Benchmarks go in bench/ with the exact command and hardware. -- When you disagree with the design doc, say so once, with the attack or the measurement that drives it, then do the work under the doc's decision unless the project lead overrides. +- When you disagree with the design doc, say so once, with the attack or the measurement that drives it, then do the work under the doc's decision unless the founder overrides. ## Writing rules No em dashes. Short sentences. Numbers in tables. The project is called Igneum. Approximate figures say so. diff --git a/.claude/agents/execution-engineer.md b/.claude/agents/execution-engineer.md index 11d55638..613cff2e 100644 --- a/.claude/agents/execution-engineer.md +++ b/.claude/agents/execution-engineer.md @@ -5,7 +5,7 @@ tools: Read, Grep, Glob, Bash, Edit, Write, WebSearch, WebFetch, Agent model: fable --- -You are the execution engineer on a GPU-mined layer 1 whose every block is ZK-proven by its miners. Read CLAUDE.md in the project root first, then the design doc it links to. The doc's decisions are fixed unless the project lead changes them. +You are the execution engineer on a GPU-mined layer 1 whose every block is ZK-proven by its miners. Read CLAUDE.md in the project root first, then the design doc it links to. The doc's decisions are fixed unless the founder changes them. ## What you carry in your head Every Ethereum client and every open zkVM, and what it costs to prove them: diff --git a/.claude/agents/miner-community-lead.md b/.claude/agents/miner-community-lead.md index ba6c8271..eb1d857e 100644 --- a/.claude/agents/miner-community-lead.md +++ b/.claude/agents/miner-community-lead.md @@ -5,7 +5,7 @@ tools: Read, Grep, Glob, Bash, WebSearch, WebFetch, Agent model: fable --- -You are the miner-community lead on a GPU-mined layer 1 whose miners are also its ZK provers. Read CLAUDE.md in the project root first, then the design doc it links to. The doc's decisions are fixed unless the project lead changes them. +You are the miner-community lead on a GPU-mined layer 1 whose miners are also its ZK provers. Read CLAUDE.md in the project root first, then the design doc it links to. The doc's decisions are fixed unless the founder changes them. ## What you carry in your head Fifteen years of mining communities, and you remember what they did and why: diff --git a/.github/workflows/ci-red.yml b/.github/workflows/ci-red.yml index 48b3deff..401005db 100644 --- a/.github/workflows/ci-red.yml +++ b/.github/workflows/ci-red.yml @@ -3,7 +3,7 @@ # ci.yml never enters it (7 October 2026: the inline `red` job of ci.yml was conditioned on master and release-*, and # a feature branch would have waited for a merge of master before its reds were posted at all). # -# One line per failed run (tools/ci/red-watch.mjs record, idempotent per run attempt) to /srv/ci-red/red.jsonl on the +# One line per failed, cancelled or timed-out run (tools/ci/red-watch.mjs record, idempotent per run attempt) to /srv/ci-red/red.jsonl on the # box; the box's igneum-ci-red.timer posts each new line once to the hidden updates channel, naming the branch, the # commit, the red check and the pushing author. Runs on the box's own runner (not a GitHub-hosted machine: the billing # block of 6 October 2026, 18:37Z to 20:10Z, failed every hosted job at start and nobody was told). Never blocks a @@ -16,7 +16,9 @@ on: jobs: red: name: red watcher (every branch; one line per failed run, with the branch, commit, red check and pushing author, to the updates channel and the box file) - if: ${{ github.event.workflow_run.conclusion == 'failure' }} + # failure, and since 7 October 2026 (17:2x UK) cancelled and timed_out too: a job that hangs into its timeout-minutes or a run + # someone cancels is a run that never answered, and a lane reads it like a red (tools/ci/red-watch.mjs names the kind) + if: ${{ github.event.workflow_run.conclusion == 'failure' || github.event.workflow_run.conclusion == 'cancelled' || github.event.workflow_run.conclusion == 'timed_out' }} # the label ci-red is on igneum-build-1 only (added through the runners API on 7 October 2026; the default of # RUNNER_LABELS in provision.sh carries it): the record file and the poster (igneum-ci-red.timer, the webhook file) # live on that box, and the pool label igneum-build-1 is shared with igneum-build-2 since the same day @@ -35,6 +37,7 @@ jobs: RED_WATCH_RUN_ID: ${{ github.event.workflow_run.id }} RED_WATCH_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} RED_WATCH_WORKFLOW: ${{ github.event.workflow_run.name }} + RED_WATCH_CONCLUSION: ${{ github.event.workflow_run.conclusion }} RED_WATCH_BRANCH: ${{ github.event.workflow_run.head_branch }} RED_WATCH_SHA: ${{ github.event.workflow_run.head_sha }} RED_WATCH_EVENT: ${{ github.event.workflow_run.event }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fefdd8b6..a09e4031 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,7 @@ jobs: # code=true (no `before` to compare from), as does any error reading the compare API: when in doubt, run. name: what the push touched (docs-only runs skip the Rust and simulator jobs) runs-on: ubuntu-latest + timeout-minutes: 10 # a 7 s API call; every job carries a budget (tools/ci/workflow-timeouts-check.sh) outputs: code: ${{ steps.classify.outputs.code }} steps: @@ -62,6 +63,7 @@ jobs: needs: changes if: ${{ needs.changes.outputs.code == 'true' }} runs-on: ${{ vars.IGNEUM_CI_RUNNER == 'box' && fromJSON('["self-hosted", "linux", "x64", "igneum-build-1"]') || 'ubuntu-latest' }} + timeout-minutes: 60 # the box's suite ran 45 s to 2 min 40 s on 7 October 2026; a hosted fallback compiles cold steps: - uses: actions/checkout@v4 - name: toolchain @@ -81,6 +83,7 @@ jobs: # queue read 22); a feature-branch code push runs the igneum-pow tests alone. tools/ci/sims-branch-check.sh holds this rule. if: ${{ needs.changes.outputs.code == 'true' && ((github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-'))) || (github.event_name == 'pull_request' && (github.base_ref == 'master' || startsWith(github.base_ref, 'release-')))) }} runs-on: ${{ vars.IGNEUM_CI_RUNNER == 'box' && fromJSON('["self-hosted", "linux", "x64", "igneum-build-1"]') || 'ubuntu-latest' }} + timeout-minutes: 45 # two simulators under 120 s each by their own timeout, plus a hosted fallback's pip install steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -104,6 +107,10 @@ jobs: site: name: site build, link check, identity grep runs-on: ubuntu-latest + # 15: the gate took 229 s on a hosted runner on 7 October 2026 plus a 40 s Playwright install; the same day three + # hosted site jobs on master hung in the gate for over two hours each with no budget, and GitHub's six-hour default + # would have ended each as a failure email. A hung job is a red the watcher posts (ci-red.yml fires on timed_out). + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -118,4 +125,4 @@ jobs: run: bash tools/ci/pre-push.sh --ci - name: public stats API answers with the documented fields (the live site; master only, the endpoints exist there after the merge) if: github.ref == 'refs/heads/master' - run: node tools/ci/public-api-check.mjs https://igneum.network + run: bash tools/ci/retry-once.sh public-api node tools/ci/public-api-check.mjs https://igneum.network # a live host: one retry before red diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 82aa2f60..f4f486d5 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -216,8 +216,14 @@ jobs: run: | $payload = Resolve-Path 'packaging\windows\igneum-windows-app' function Run-Capture([string]$exe, [string]$flag) { + # Start-Process -Wait on an exe that exits in milliseconds can throw "Cannot process request because the process has + # exited" before it attaches (release-0.3.21 run 37653903394, 7 October 2026, 17:40 UK): one retry before the verdict. $out = Join-Path $env:RUNNER_TEMP ('smoke-' + [IO.Path]::GetRandomFileName() + '.txt') - $p = Start-Process -FilePath $exe -ArgumentList $flag -Wait -NoNewWindow -PassThru -RedirectStandardOutput $out + $p = $null + foreach ($try in 1, 2) { + try { $p = Start-Process -FilePath $exe -ArgumentList $flag -Wait -NoNewWindow -PassThru -RedirectStandardOutput $out; break } + catch { if ($try -eq 2) { throw }; Write-Host ("Start-Process on {0} {1} failed once ({2}); second try" -f (Split-Path -Leaf $exe), $flag, $_.Exception.Message); Start-Sleep -Milliseconds 500 } + } $text = if (Test-Path $out) { (Get-Content $out -Raw) } else { '' } Write-Host ("{0} {1} -> exit {2}: {3}" -f (Split-Path -Leaf $exe), $flag, $p.ExitCode, $text.Trim()) if ($p.ExitCode -ne 0) { throw "$exe $flag exited $($p.ExitCode)" } diff --git a/app/igneum-app/build.rs b/app/igneum-app/build.rs index e2661b97..d62b1670 100644 --- a/app/igneum-app/build.rs +++ b/app/igneum-app/build.rs @@ -1,6 +1,6 @@ // Build script for igneum-app. On a Windows target it compiles resources/igneum-app.rc (the coin icon Explorer shows // and the version block under Properties > Details) with windres and links the object into igneum-app.exe. Other -// targets: nothing. the project lead's rule (4 October 2026): every shipped exe carries the coin icon and a version block, like the +// targets: nothing. The founder's rule (4 October 2026): every shipped exe carries the coin icon and a version block, like the // Mac app and DMG. No crate dependency: windres is called directly (x86_64-w64-mingw32-windres from Homebrew mingw-w64 // on the Mac, windres from MSYS2 on a PC; IGNEUM_WINDRES names another one). use std::env; diff --git a/app/igneum-app/resources/igneum-app.rc b/app/igneum-app/resources/igneum-app.rc index a377b737..a9b419a1 100644 --- a/app/igneum-app/resources/igneum-app.rc +++ b/app/igneum-app/resources/igneum-app.rc @@ -1,6 +1,6 @@ // Windows resources for igneum-app.exe: the coin icon Explorer shows and the version block under Properties > Details. // Compiled with x86_64-w64-mingw32-windres (the icon path is relative to brand/icons, passed with -I). -// the project lead's rule (4 October 2026): every shipped exe carries the coin icon and a version block, like the Mac app and DMG. +// the founder's rule (4 October 2026): every shipped exe carries the coin icon and a version block, like the Mac app and DMG. #include 1 ICON "igneum.ico" diff --git a/app/igneum-app/src/config.rs b/app/igneum-app/src/config.rs index 9746985a..1cab39fe 100644 --- a/app/igneum-app/src/config.rs +++ b/app/igneum-app/src/config.rs @@ -87,7 +87,7 @@ pub struct Settings { /// (on when that is switched on, never effective while it is off). A pinned card is skipped. #[serde(default)] pub sweep: bool, - /// Power control (the project lead, 5 October 2026: "if we don't have to ask then don't ask"): the NVIDIA power cap and the + /// Power control (the founder, 5 October 2026: "if we don't have to ask then don't ask"): the NVIDIA power cap and the /// efficiency sweep need administrator rights (one UAC prompt on Windows). Default OFF on every machine; the app /// never raises the prompt on its own. Switching it on asks once, at that moment; a refused, cancelled or /// unanswered prompt switches it back off with a notice, no retries. @@ -345,7 +345,7 @@ pub struct Runtime { pub host: String, /// Per-install random id (16 hex, app data dir/machine-id, locked to the user). Identity labels, vote keys and /// the upload fields come from this, so two PCs cloned with the same COMPUTERNAME never share a key - /// (found 4 October 2026 on the project lead's two DESKTOP-KMCV30N machines). + /// (found 4 October 2026 on the founder's two DESKTOP-KMCV30N machines). pub machine_id: String, } @@ -357,7 +357,7 @@ impl Runtime { let p2p_port = env("IGNEUM_APP_P2P_PORT").and_then(|v| v.parse().ok()).unwrap_or(26611); let peers = match std::env::var("IGNEUM_APP_PEERS") { Ok(v) => v.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(), - // the public seed node first, then the project lead's Mac on the house LAN (devnet only) + // the public seed node first, then the founder's Mac on the house LAN (devnet only) Err(_) if network == "devnet" => vec!["188.245.5.161:26611".to_string(), "192.168.68.64:26611".to_string()], Err(_) => vec![], }; diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index c4176bc5..d111cd9e 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -4079,7 +4079,7 @@ impl Engine { } /// The watts a card's cap asks for: power_pct of the default limit, inside the card's min and max. -/// the project lead, 5 October 2026: "if we don't have to ask then don't ask". The NVIDIA power cap and the efficiency sweep need +/// the founder, 5 October 2026: "if we don't have to ask then don't ask". The NVIDIA power cap and the efficiency sweep need /// administrator rights (one UAC prompt on Windows, pkexec on Linux); the engine builds an elevated command only when /// Power control is on in Settings, or when it is itself the elevated PC sweep job (--sweep). fn elevation_allowed(power_control: bool, sweep_only: bool) -> bool { @@ -4351,7 +4351,7 @@ mod resume_tests { mod tests { #[test] fn power_control_off_builds_no_elevated_command() { - // the decision (the project lead, 5 October 2026): off = the app never asks; the elevated PC sweep job is the exception + // the decision (the founder, 5 October 2026): off = the app never asks; the elevated PC sweep job is the exception assert!(!super::elevation_allowed(false, false)); assert!(super::elevation_allowed(true, false)); assert!(!super::elevation_allowed(false, true), "the --sweep job alone never asks (C35)"); diff --git a/app/igneum-app/src/jobbuild.rs b/app/igneum-app/src/jobbuild.rs index 12d0795d..fe3a5d60 100644 --- a/app/igneum-app/src/jobbuild.rs +++ b/app/igneum-app/src/jobbuild.rs @@ -1,5 +1,5 @@ //! The `build` job (the model is src/jobs.rs, the runner src/jobrun.rs): a Windows PC builds the node and the app -//! engine for Linux and Windows inside its WSL2 Ubuntu, as root, with nothing from the project lead. the project lead's ask, 4 October 2026 +//! engine for Linux and Windows inside its WSL2 Ubuntu, as root, with nothing from the founder. The founder's ask, 4 October 2026 //! evening ("efficiency"): every Windows build went through a GitHub runner at 15 to 25 minutes a round and every //! Linux binary was cross-compiled on the Mac under the build lock; the two RTX 5090 PCs sit idle on the CPU side. //! diff --git a/app/igneum-app/src/jobrun.rs b/app/igneum-app/src/jobrun.rs index a6a1d26f..a355b8b5 100644 --- a/app/igneum-app/src/jobrun.rs +++ b/app/igneum-app/src/jobrun.rs @@ -32,7 +32,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -/// The safety-net poll. Before 0.3.6 this was 600 s and a published job waited up to 10 minutes on every PC (the project lead, +/// The safety-net poll. Before 0.3.6 this was 600 s and a published job waited up to 10 minutes on every PC (the founder, /// 5 October 2026: "why is it taking so long for pc2 and pc1s tasks to spin up?"); the wake below makes it seconds. const CHECK_EVERY_S: u64 = 120; const RETRY_AFTER_ERROR_S: u64 = 300; @@ -50,7 +50,7 @@ const GPU_IDLE_PCT: f64 = 5.0; const GPU_IDLE_WAIT_S: u64 = 180; const DEFAULT_DISTRO: &str = "Ubuntu-24.04"; /// WSL jobs run as root by default: the Ubuntu the app sees is the one of the account the app runs under, and a -/// personal user ([user] on PC 2) need not exist there (4 October 2026: `getpwnam([user]) failed`). +/// personal user ( on PC 2) need not exist there (4 October 2026: `getpwnam() failed`). const DEFAULT_WSL_USER: &str = "root"; const DEFAULT_FIXTURES: &[&str] = &["block-338-shard1", "block-341-shards2", "block-344-shards4"]; const HISTORY_SHOWN: usize = 20; diff --git a/app/igneum-app/src/jobs.rs b/app/igneum-app/src/jobs.rs index e54805bf..2b50111f 100644 --- a/app/igneum-app/src/jobs.rs +++ b/app/igneum-app/src/jobs.rs @@ -2,7 +2,7 @@ //! manifest on the downloads host, and every Igneum Miner app polls it (src/jobrun.rs, every 10 minutes). A job //! runs at most once per id on a machine, only when its target matches (machine id, platform, requirements) and //! it has not expired. Same key, same canonical JSON (sorted keys, no whitespace) and the same `.sig` scheme as the -//! update manifest (src/manifest.rs). the project lead's rule, 4 October 2026: one app on both PCs that the Mac can send +//! update manifest (src/manifest.rs). The founder's rule, 4 October 2026: one app on both PCs that the Mac can send //! commands and files to over the line, so everything is tested and built without a person at the PC. //! //! This module is self-contained (serde_json and manifest.rs only), so the signer (src/bin/ota-sign.rs) includes it diff --git a/app/igneum-app/src/ota.rs b/app/igneum-app/src/ota.rs index f10563d2..82090ca8 100644 --- a/app/igneum-app/src/ota.rs +++ b/app/igneum-app/src/ota.rs @@ -1,4 +1,4 @@ -//! Over-the-air updates of the app (and the node, miner and workers inside it). the project lead's rule: every app updates +//! Over-the-air updates of the app (and the node, miner and workers inside it). The founder's rule: every app updates //! itself and downloads the update without being asked. This is also how a consensus upgrade (a height-activated //! rule such as difficulty v2) reaches every node before its activation height. //! @@ -120,7 +120,7 @@ pub struct Updater { /// this machine's minute of the hour for applying (manifest::slot_minute of the machine id) slot: u64, /// When this engine started (unix seconds): an update published more than an hour before it is a catch-up, not a - /// rollout, and skips the hourly slot (the project lead's morning of 6 October 2026: PC 1 came up after the 0.3.11 publish and + /// rollout, and skips the hourly slot (the founder's morning of 6 October 2026: PC 1 came up after the 0.3.11 publish and /// sat on "installs at the next safe moment" until he pressed Install now). started_unix: u64, catch_up_logged: bool, diff --git a/app/igneum-app/src/powertask.rs b/app/igneum-app/src/powertask.rs index 232ef558..c3d6e687 100644 --- a/app/igneum-app/src/powertask.rs +++ b/app/igneum-app/src/powertask.rs @@ -1,4 +1,4 @@ -//! One administrator approval, ever (the project lead, 6 October 2026, 11:50 UTC, after clicking the third prompt of the morning: +//! One administrator approval, ever (the founder, 6 October 2026, 11:50 UTC, after clicking the third prompt of the morning: //! "can we make sure all these popups are not needed in future?"). //! //! What 0.3.12 does: Power control on raises one prompt and sets every cap in that step; but every later cap (an app diff --git a/app/igneum-app/src/provedefault.rs b/app/igneum-app/src/provedefault.rs index 6aebd90d..979e35d9 100644 --- a/app/igneum-app/src/provedefault.rs +++ b/app/igneum-app/src/provedefault.rs @@ -1,4 +1,4 @@ -//! Proving v1 step 1 (5 October 2026, the project lead: "open the proving round asap"): the prover is on by default on every +//! Proving v1 step 1 (5 October 2026, the founder: "open the proving round asap"): the prover is on by default on every //! mining machine that can prove, decided once per install after the cards are detected (src/engine.rs //! `apply_prove_default`). The rule, one line each: //! @@ -6,13 +6,13 @@ //! |---|---|---| //! | NVIDIA card with 24 GB or more, mining or not, Windows with WSL2 (Ubuntu-24.04) answering or Linux | on | a full shard at the adopted v1 budget (30,000 pgas, 4.7 M cycles) peaks at 20,434 MiB alone and 22,210 beside the miner (measured on the 5090; approximate for a 24 GB card's own allocation); the prototype shard the devnet proves until its fee switch (6.75 M pgas) peaks at 28,307 MiB alone and 30,039 beside the miner, so until the switch only a 32 GB card proves it and a 24 GB card's prover waits for shards it can hold (the host refuses nothing; a proof that runs out of memory fails and the shard is left) | //! | NVIDIA card of 16 to 24 GB | off, with the line saying why | the GPU prover's floor is 13,874 MiB for an EMPTY shard, 15,670 beside the miner; a 16 GB card holds no full shard | -//! | NVIDIA card under 16 GB | off | 13,874 MiB does not fit; the project lead's 12 GB requirement is open until a prover build with a smaller floor is measured | +//! | NVIDIA card under 16 GB | off | 13,874 MiB does not fit; the founder's 12 GB requirement is open until a prover build with a smaller floor is measured | //! | Windows under 32 GB of RAM | off, with the line saying why | the WSL2 prover held 7.9 GB on a 63 GB PC; a 16 GB PC would swap | //! | Windows with a qualifying card but WSL2 silent | off, with the Set up hint | nothing can prove until the distribution exists | //! | Apple silicon | off | the M5 Max CPU took 41 to 55 s for an EMPTY shard's compressed proof under load and 272 s for a 200-pgas shard; a full shard was never under 60 s (bench-log 4 and 5 October 2026) | //! | AMD-only (no NVIDIA card) | off, "mines and does not prove" | no zkVM proves on an AMD GPU today (docs/analysis/amd-proving.md); the SP1 CPU prover on PC 1 cost 82 to 87 s core plus 199 to 202 s compressed a shard at a 30 GB RSS whatever the shard size (bench-log, "the SP1 CPU prover on PC 1") | //! -//! Decided 5 October 2026 (delegated by the project lead: "deploy what is absolute best"), docs/plans/proving-v1.md. The default +//! Decided 5 October 2026 (delegated by the founder: "deploy what is absolute best"), docs/plans/proving-v1.md. The default //! never switches an explicit on back off, and Settings always wins afterwards. use crate::state::CardState; diff --git a/app/igneum-app/src/state.rs b/app/igneum-app/src/state.rs index 90ef51fd..d4d51097 100644 --- a/app/igneum-app/src/state.rs +++ b/app/igneum-app/src/state.rs @@ -127,7 +127,7 @@ pub struct CardState { pub tune_source: String, // full | confirm | baseline pub tune_line: String, // "Tuned: 122.3 MH/s at 290 W (0.422 MH/W)" once tuned // a tune in progress on this card, by this engine or by a measurement engine posting /api/tune-progress - // (the project lead, 6 October 2026: "don't we need to show in the app that tuning is in progress?") + // (the founder, 6 October 2026: "don't we need to show in the app that tuning is in progress?") pub tune_step: u32, pub tune_steps: u32, pub tune_eta_s: i64, diff --git a/app/igneum-app/src/wslhost.rs b/app/igneum-app/src/wslhost.rs index 9bee6d04..0ce7cbf2 100644 --- a/app/igneum-app/src/wslhost.rs +++ b/app/igneum-app/src/wslhost.rs @@ -221,11 +221,11 @@ mod tests { #[test] fn the_command_line_after_the_dashes_never_carries_a_double_quote_or_a_newline() { - let file = Path::new("C:\\Users\\the project lead\\AppData\\Local\\igneum\\wsl\\probe-12-3.sh"); - let line = bash_line(file, true, &["--proof", "/mnt/c/Users/the project lead/AppData/Local/igneum/app/proving/p.bin", "--statement", "0xab", "it's", "two\nlines"]); + let file = Path::new("C:\\Users\\the founder\\AppData\\Local\\igneum\\wsl\\probe-12-3.sh"); + let line = bash_line(file, true, &["--proof", "/mnt/c/Users/the founder/AppData/Local/igneum/app/proving/p.bin", "--statement", "0xab", "it's", "two\nlines"]); assert_eq!( line, - "bash -l '/mnt/c/Users/the project lead/AppData/Local/igneum/wsl/probe-12-3.sh' '--proof' '/mnt/c/Users/the project lead/AppData/Local/igneum/app/proving/p.bin' '--statement' '0xab' 'it'\\''s' 'two lines'" + "bash -l '/mnt/c/Users/the founder/AppData/Local/igneum/wsl/probe-12-3.sh' '--proof' '/mnt/c/Users/the founder/AppData/Local/igneum/app/proving/p.bin' '--statement' '0xab' 'it'\\''s' 'two lines'" ); assert!(!line.contains('"') && !line.contains('\n'), "{line}"); // the lookup script's own double quotes live in the file, never on the line @@ -282,7 +282,7 @@ mod tests { #[test] fn wsl_paths() { - assert_eq!(wsl_path(Path::new("C:\\Users\\[user]\\AppData\\Local\\igneum\\app\\proving\\seq.json")), "/mnt/c/Users/[user]/AppData/Local/igneum/app/proving/seq.json"); + assert_eq!(wsl_path(Path::new("C:\\Users\\\\AppData\\Local\\igneum\\app\\proving\\seq.json")), "/mnt/c/Users//AppData/Local/igneum/app/proving/seq.json"); assert_eq!(wsl_path(Path::new("\\\\?\\D:\\x")), "/mnt/d/x"); assert_eq!(wsl_path(Path::new("/tmp/x")), "/tmp/x"); } diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 01549746..46308419 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -267,7 +267,7 @@ var View = (function () { function cap(t) { return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; } var kindWord = Notices.kindWord; // hot-plug (src/hotplug.rs): a removed card's row hides after five minutes (gone); a faulty one has no switch - // The list is ordered by performance (the project lead, 6 October 2026): usable cards first, then by the measured rate since the + // The list is ordered by performance (the founder, 6 October 2026): usable cards first, then by the measured rate since the // start (5 MH/s buckets so the order does not flicker), then discrete, external and Apple before integrated, then // memory; removed and unusable cards last. Ties keep the detection order. function perfRank(c) { diff --git a/brand/README.md b/brand/README.md index 07178157..6ad10f82 100644 --- a/brand/README.md +++ b/brand/README.md @@ -1,6 +1,6 @@ # Igneum brand assets -## The rule (the project lead, 4 October 2026) +## The rule (the founder, 4 October 2026) One global logo for apps, profile pictures, favicons, everything. The mark sits in a black square. Never in a circle. Never on another colour. Minimum clear space = 20% of the square. The Mac app icon is the model: a full square, all diff --git a/brand/icons/make-icons.py b/brand/icons/make-icons.py index 9c21ea5c..15df5b88 100644 --- a/brand/icons/make-icons.py +++ b/brand/icons/make-icons.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Builds every Igneum icon from the master mark, brand/master/igneum-mark-square.svg (4 October 2026). -the project lead's rule (4 October 2026): one global logo for apps, profile pictures, favicons, everything. The mark sits in a black +The founder's rule (4 October 2026): one global logo for apps, profile pictures, favicons, everything. The mark sits in a black square (#0C0C0E, the site's obsidian token), centred, no circle, no ring, no border, never on another colour. The Mac app is the model. The rounded variant (brand/master/igneum-mark-square-rounded.svg, Apple's 824-on-1024 icon grid) is used only where the shape has to be baked into the file: the DMG volume icon. Tahoe masks the plain square itself. diff --git a/brand/trademark/FILING.md b/brand/trademark/FILING.md index 46578317..4a1c1645 100644 --- a/brand/trademark/FILING.md +++ b/brand/trademark/FILING.md @@ -15,7 +15,7 @@ Mark description for the device, when a form asks: "A stylised flame formed of t ## Applicant Igneum Labs LTD, Licensee Address: Unit IH-00-01-01-OF-01, Level 01, Innovation One, Dubai International Financial -Centre (decided 5 October 2026; it replaces the ADGM DLT Foundation named on 4 October). NOT [other-business]: a [other-business] +Centre (decided 5 October 2026; it replaces the ADGM DLT Foundation named on 4 October). NOT the earlier entity: an earlier-entity filing would tie Igneum to VIVA and to its owner through public registers, which the standing rule forbids. The registered address above is the applicant address, with a trademark attorney as the address for service, so no personal address appears anywhere. If the company's registration is not complete the week you want to file, the diff --git a/docs/analysis/amd-proving.md b/docs/analysis/amd-proving.md index b64c94a1..aee6e9e4 100644 --- a/docs/analysis/amd-proving.md +++ b/docs/analysis/amd-proving.md @@ -1,6 +1,6 @@ # Proving on AMD and Apple cards: what exists, what the CPU can do, what to tell the public -5 October 2026, from the project lead's two questions that evening: "test proving on the amd card?" and "can we test proving on +5 October 2026, from the founder's two questions that evening: "test proving on the amd card?" and "can we test proving on mac?". PC 1 holds an RTX 5090 and an RX 9070 XT (gfx1201, 16 GB) in an eGPU; this Mac is an M5 Max. The prover is SP1 (`proving/igneum-prove`, `docs/plans/proving-v0.md`, `proving-v1.md`), run on the GPU only through SP1's CUDA server. Every figure below is measured (with its bench-log entry or job id) or cited (with its file or page); the @@ -115,7 +115,7 @@ gates or a backend ships; it is reviewed with every prover release. | Consequence | Action | Owner | |---|---|---| -| An AMD-only miner loses the proving share | the CPU tier of 4a is measured here (section 2); whether it becomes a tier is a decision for the project lead on those numbers | this analysis; the project lead | +| An AMD-only miner loses the proving share | the CPU tier of 4a is measured here (section 2); whether it becomes a tier is a decision for the founder on those numbers | this analysis; the founder | | The rig's prover unit must select NVIDIA cards only | already true in `prover_decision`; told the rig-installer agent to keep it as a stated rule and to print the CPU-fallback line for AMD-only rigs | rig-installer agent | | The app's Proving tile on an AMD-only or Apple machine should say why it is off and name the CPU path | the `provedefault.rs` lines already say so for Apple; AMD-only Windows machines get "no NVIDIA card ..." | proving agent (told) | | The site and litepaper over-promise for AMD and Apple | the line of 4c, to land with the next site pass (copy law; `node site/build.mjs`; link-check) | site-pages owner; not changed here | diff --git a/docs/analysis/asic-resistance-history.md b/docs/analysis/asic-resistance-history.md index 47b3a20c..218aec33 100644 --- a/docs/analysis/asic-resistance-history.md +++ b/docs/analysis/asic-resistance-history.md @@ -1,8 +1,8 @@ # ASIC resistance, 2011 to 2026: the history, the papers, the lessons, and the audit of Igneum against them -5 October 2026 (night), branch `asic-history`. Asked by the project lead at 20:05 UTC: "do a full on deep dive into the full history of 'asic resistance' and see if we can add or upgrade anything." Baseline for the audit: the Counter ASIC 2.0 final class decided tonight (`docs/plans/counter-asic-2-status.md` on `ca2-coord`, entries 20:16 to 22:25 UTC; `docs/analysis/chip-model-v3.md` on `ca2-mixer` 1ab8b21). Every figure about another chain cites a repo file, a paper or a dated article, or is labelled approximate. Hash-per-joule gains are computed from the cited hashrate and watt figures of the chip and of the best consumer GPU of the same year, and are approximate by construction (GPU figures vary by tuning). Research gathered by four sub-agents between 20:10 and 20:45 UTC; the fetch failures they reported are listed in section 6. +5 October 2026 (night), branch `asic-history`. Asked by the founder at 20:05 UTC: "do a full on deep dive into the full history of 'asic resistance' and see if we can add or upgrade anything." Baseline for the audit: the Counter ASIC 2.0 final class decided tonight (`docs/plans/counter-asic-2-status.md` on `ca2-coord`, entries 20:16 to 22:25 UTC; `docs/analysis/chip-model-v3.md` on `ca2-mixer` 1ab8b21). Every figure about another chain cites a repo file, a paper or a dated article, or is labelled approximate. Hash-per-joule gains are computed from the cited hashrate and watt figures of the chip and of the best consumer GPU of the same year, and are approximate by construction (GPU figures vary by tuning). Research gathered by four sub-agents between 20:10 and 20:45 UTC; the fetch failures they reported are listed in section 6. -## 0. One page for the project lead +## 0. One page for the founder **What the history says Igneum is doing right.** @@ -228,7 +228,7 @@ Ranked by how much the history says each would change the outcome, with the cost | 1 | **Price the partial-store chip and draw the time-memory curve.** A chip that stores a fraction f of the dataset in HBM or on many narrow DRAM channels, recomputes the rest from a 256 MiB on-die cache under x8, and reads with 4-byte granularity. Rows for f = 0.25, 0.5, 1 at HBM3 and at GDDR7 random-read rates, priced in energy per hash (Rao's metric) and in reads in flight per watt | The only chip class that beat a memory-bound GPU hash: Ethash's 2.1x to 4.8x came from the memory system with no on-die dataset (rows 3, 4); Rao priced a 16-die DAG holder under a GPU board in 2019; Cuckoo's curve was wrong by 50x until drawn [P20]; O-1.6 is open and `MEMHARD.md` section 3 item 2 says the curve was never drawn | None (analysis) | The row may come out over 2x, which would qualify the public claim before anyone else does | Genesis (before the vectors freeze) | | 2 | **A random item-derivation program per day** in place of the fixed-shape mixer: a SuperscalarHash-style generator, integer only, drawn from the day key, with its own acceptance test, compiled once a day by miners and verifiers | RandomX's reason for SuperscalarHash: a fixed derivation is hard-wired by a chip; a random one makes the light-mode chip a CPU (section 2.4). In Igneum's model the fixed shape is the 3x factor that turns 0.31x into 0.92x; removing the factor is worth more than x8 to x16 would be (x16: 0.46x with the factor by M16's table) | None per hash (the daily build is 23 to 77 ms at x8 and would roughly double); the verifier needs a per-day compiled derivation (a JIT, or a round schedule drawn from a fixed set of reviewed rounds), measured against the 10 ms gate | Cryptanalysis of random ARX programs; weak draws; a JIT in the verifier is new attack surface; the vendors must agree bit-exactly on a program they compile | Reserve (named family, unlock by height or signal) now; genesis if the verifier cost is measured under the gate before the freeze | | 3 | **External cryptanalysis of M_r, the chained cache and the acceptance rule before genesis**, with the x8 shape as the target | Lesson 9 (MTP, Catena, Argon2i, Cuckoo); RandomX bought four audits for $141,000 before launch [S60]; the x8 decision multiplies the mixer's weight in the chip model, so a shortcut inside the mixer is now worth 8x more to a chip | None | Finding something late moves the vectors; not finding it in time moves nothing | Genesis gate (ledger M7, raised in priority) | -| 4 | **The clock and the detector.** (a) A share-pattern detector on the observer: per-program hash-rate spread, nonce-group patterns and per-card-model rate bands, with an alert when a population behaves like one fixed design (MoneroCrusher's method); (b) a stated trigger: the bounty escrowed and the benchmark live before daily issuance crosses about $50K (Vorick's rule), not on a calendar date | Lesson 10 (85% secret share); section 2.5's table (chips at $20K to $30K a day on compute-bound hashes); D11 (the bounty is unfunded) | None | A detector with false positives; a trigger the project lead has to fund | Not a layer; genesis-independent; do it before the public testnet | +| 4 | **The clock and the detector.** (a) A share-pattern detector on the observer: per-program hash-rate spread, nonce-group patterns and per-card-model rate bands, with an alert when a population behaves like one fixed design (MoneroCrusher's method); (b) a stated trigger: the bounty escrowed and the benchmark live before daily issuance crosses about $50K (Vorick's rule), not on a calendar date | Lesson 10 (85% secret share); section 2.5's table (chips at $20K to $30K a day on compute-bound hashes); D11 (the bounty is unfunded) | None | A detector with false positives; a trigger the founder has to fund | Not a layer; genesis-independent; do it before the public testnet | | 5 | **Rank layer 9 (the epoch length) up, and measure the FPGA lane**: the compile-ahead cost per card at a 10-minute epoch (the `ca2-epoch` work), plus an estimate of a soft-overlay FPGA miner with HBM (reads in flight per watt against the 5090's 17.5 G/s) | FPGAs were the first adversary of Lyra2REv2 and X16R and came back within weeks of X16Rv2 (rows 7, 9); Xelis forked for FPGA resistance (row 30); a per-hour program is a bitstream target in a way a per-hash program is not | At 10-minute epochs: 6x the compile work per card (measured on `ca2-epoch`); the VDF lead shrinks | A short epoch moves the difficulty window (spec 1.12) and the seed path | Reserve (as decided), with the measurement before the public testnet | | 6 | **Order the reserve by chip-unfriendliness**: families that force a full 32-bit datapath per lane first (byte permute, bit-field extract, variable shifts, popcount, select, the second shuffle form), mm8 last | Least Authority's "watch ML hardware"; int8 matrix blocks are licensable IP at every node; Apple pays 1.6x to 4.7x per emulated dot4 (status 20:38) | None at launch | None | Reserve ordering, genesis | | 7 | **A vendor-share metric and a 3.0 target for the AMD gap**: the share of hashrate by vendor published with the benchmark, and the line-width question kept open as the plan says | Lesson 8: a one-vendor fleet is a softer version of chip capture; Equihash's NVIDIA tilt and Ethash's balance were part of each chain's miner politics (rows 3, 5) | n/a | A width that closes the gap makes the 5090 bandwidth-bound (status 20:27) | Counter ASIC 3.0 | @@ -256,14 +256,14 @@ Evaluated and placed nowhere, with the reason: | Divergent data-dependent branches | Nowhere (already excluded) | Branches cost a GPU divergence and a chip nothing; RandomX's single predictable branch targets speculative CPUs, which Igneum does not have | | Floating point | Nowhere (already excluded) | Vendor rounding splits the chain (spec 1.14); RandomX could afford it because its target is one ISA family with IEEE semantics | -## 5. Decisions this raises for the project lead +## 5. Decisions this raises for the founder | # | Decision | Recommendation | |---|---|---| | 1 | Add the partial-store chip rows to `chip-model-v3.md` and draw the time-memory curve before the public testnet | Yes, before the vectors freeze (addition 1) | | 2 | Name a random item-derivation program as a reserve family, and fund the verifier measurement that would move it to genesis | Reserve now; genesis if the verifier lands under the gate (addition 2) | | 3 | Commission the external cryptanalysis of M_r and the chained cache before genesis, with the x8 shape as the target | Yes (addition 3; ledger M7) | -| 4 | Escrow the bounty and set its trigger to daily issuance, not to a date; build the share-pattern detector on the observer | Yes to the detector now; the escrow is the project lead's (D11) | +| 4 | Escrow the bounty and set its trigger to daily issuance, not to a date; build the share-pattern detector on the observer | Yes to the detector now; the escrow is the founder's (D11) | | 5 | Rank the epoch-length reserve above the mm8 reserve, and measure the FPGA lane | Yes (additions 5 and 6) | ## 6. Sources and limits of this research diff --git a/docs/analysis/attack-pass-2026-10.md b/docs/analysis/attack-pass-2026-10.md new file mode 100644 index 00000000..fb45eb06 --- /dev/null +++ b/docs/analysis/attack-pass-2026-10.md @@ -0,0 +1,616 @@ +# Internal attack pass before the freeze (F1 to F10) + +The internal cryptanalysis pass of `docs/plans/cryptanalysis.md` section 4.2, run before the freeze tag +`cryptanalysis-target-1`, so the paid engagement confirms rather than discovers. The founder's word, 7 October 2026: +"make sure they find ZERO flaws". Every finding is ours, fixed and re-gated, before any firm starts. + +Target: the hash class the chain runs after 0.3.15's flip, `igneum-pow` generator v4 `V4_CLASS` = `mx8+sh256x27` +(`LoadClass::MX8`, `ShadowClass { instrs: 256, reps: 27 }`), the acceptance rule, the verifier, the era draw, +the latency-shadow dataset and ladder, the chip and FPGA cost model. Scope and gates are section 1.1 and 1.4 of +the plan, the same tests the firm is held to. + +Lane: attack-pass, worktree `igneum-wt-attack`, branch `attack-pass` from `origin/master` `ab99e5e3`. +Binary built on igneum-build-1 (ELF x86-64, `igneum-pow` 0.2.0, sha256 6d2867...1a9ebe5) and run there under +the box's slots; model and era work from `sim/horizon/algorithm/model.py` and `infra/fast-time/`. Each row below +carries the method, the known-failed shape where one exists, the result with numbers, and PASS, RUNNING, +BLOCKED or FINDING. PASS RECORD (7 October 2026, 16:0x UTC, 17:0x UK): every row reads PASS or FIXED-AND-PASSED. F1 PASS (AP-F1-1 on the +v5 list at 3.0 percent); F2 PASS, effort-bounded; F3 PASS; F4 PASS against class v4 (AP-F4-1 on the v5 list); F5 +FIXED-AND-PASSED (the F2 hour skipped by decision); F6 PASS (the worst of 10^5 programs 8.708 ms on the half-core +proxy; O-1.14 closed on an i7-9700K); F7 PASS on all three sub-rows (the era VDF in the node, 0 of 6 re-rolls); F8 +FIXED-AND-PASSED in class v4 sub-version 3 at 017e7037 (AP-F8-1, AP-F8-2, AP-F8-3 ours and closed; the four-seed +unattributed tail named); F9 PASS on (a) and (c), (b) closed by the same fix; F10 PASS. Frozen generator: class v4 +sub-version 3, igneum-pow 017e70376489251e18564c0abce7e466e606c8b3, devnet epoch-0 id a785001687d8688a. The +freeze tag `cryptanalysis-target-1` is the coordinator's cut on this record; the era VDF precondition is met (F7 a). Main checks every number +against the log before quoting it to the founder. + +## Status board + +| # | Attack | Gate (same as 1.4) | Result so far | Status | +|---|---|---|---|---| +| F1 | Shadow block compressibility and shortcut search | no compression of the shadow block beyond the honest compiler's simplification, measured against that compiler on the same program; the 27 repetitions never fewer than 27x (coordinator's ruling 7 Oct 2026, 12:3x UK; the plan's gate (1) and row F1 carry the same) | 10^4 and 10^5 class v4 programs: saved instructions mean 0.62%, max 5.078% at 10^5 (1 of 100,000 over 5%, 0 over 10%); nothing folds or dedupes across the 27 passes; every saving is local peephole algebra that clang -O3 removes from the honest kernel too (IR counts match on the worst programs), so against the compiler the compression is 0; 0 mismatches in 2 x 10^5 differential and verifier checks, z3 window proofs 0 counterexamples. AP-F1-1 routed to the v5 list as a shadow redundancy bound. Record `docs/analysis/attack-pass/f1-shadow.md` | PASS; AP-F1-1 on the v5 list | +| F2 | Mixer round margin (SAT/MILP, 1 to 4 keyed applications) | no distinguisher or shortcut beyond 2 of the 8 applications | one application characterised (differential weight 10 to 12, linear 1, verified on the real code on three days); two applications: no trail at or below weight 20 to 24 within 7,200 s per job, the MSB and LSB families die at two; rotational-XOR no bias at one application; the multiply layer folds on 0 of 2^20 inputs, k applications cost k; three applications: no differential trail at or below weight 29 to 35 and no linear at or below 24 to 28, four: 39 to 47 and 24, every job at its 7,200 s cap. Record `docs/analysis/attack-pass/f2-mixer.md` | PASS (effort-bounded) | +| F3 | Chained cache j+1 bound and storage-vs-recompute curve | no derivation under j+1 blocks; curve monotone; f=1 point unchanged | 0 of 64 and 0 of 1,024 lines under j+1 (exhaustive closure search, cross-checked by exhaustive pebbling at 10 lines, 10,240 pairs, 0 mismatches); both planted broken chains fire; curve monotone at both op counts; f=1 point 9,360 ops per item unchanged. Record `docs/analysis/attack-pass/f3-cache.md` | PASS | +| F4 | Weak-day census over 2^24 day keys | fraction of days with gain over 1.1x under 2^-20 | PASS against M2 (DSP-bound datapath): 0 of 2^28 days over 1.1x; planted weak days fire; every ROT and RC class 0. Bound finding AP-F4-1 on M1 (LUT adders): 5,476 of 2^24 days (3.26e-4) over 1.1x as the tail of a sum, no weak class; worst public-calendar day 29,337 at 1.121x, at most 12.1% more rate that day for a per-day LUT FPGA, 0 for any chip; redraw rule (NAF sum under 163 rejected) routed to the next class. Record `docs/analysis/attack-pass/f4-weakday.md` | PASS (v4); AP-F4-1 routed to the next class | +| F5 | Chip-model sweep + AWS F2 FPGA hour | evidence row 17 holds across the sweep; FPGA row under 27 M reads/s/W | sweep: 2.1x at k=1 GDDR7 reproduces, 3.2x at k=0.5, 4.1x at k=0.3 (matches ledger M32); FPGA row 2.3 to 2.9 G/s, 10 to 20 M reads/s/W (literature). FINDING: the k=0.33 figure is framed as the X9's measured core (M32) and a "measured class" (ladder branch ยง5a); the X9 was withdrawn before launch and never benchmarked. F2 hour SKIPPED: no AWS account | FIXED-AND-PASSED (sweep PASS; AP-F5-1 fixed and re-gated 7 Oct 2026: chip section re-run 2.1x at k=1 unchanged, identity grep 0 hits, site lane concurred); F2 hour SKIPPED-BY-DECISION (the founder, 7 Oct 2026, 09:5x UK; plan 4.2 row F5 is the sweep only at 3714c2a0; the FPGA row stays the JEDEC-ceiling model row labelled unmeasured) | +| F6 | Verifier worst case over 10^5 programs + O-1.14 laptop run | worst program under 10 ms cold on the half-core proxy and the laptop | 100,000 programs ranked, 50,000 timed cold one-core (worst 6.194 ms), the worst 200 re-timed and the worst 1,000 timed on the half-core proxy under the per-core lease (core 40 at 3,799.9 MHz): worst 8.708 ms (`attack-f6/87142`), 1.29 ms under the gate, every half-core reading under 9 ms; dr736 fails as it must (15.49). O-1.14 CLOSED on an i7-9700K (v4 6.334 ms cold max, dr736 10.04 fails). Ladder ceiling from the worst program on the half-core proxy: N about 300,000, so rung 2 admissible, rung 3 not. Record `docs/analysis/attack-pass/f6-verifier.md` | PASS | +| F7 | Era-draw bias harness + 2^20 era-seed census | no re-roll inside the publish window; no era class with gain over 1.1x over 2^-20 | (b) census PASS at 2^24 seeds (no class over 1.1x, stride bijective, R, pos and M uniform, planted cases fire); (c) 64-bit day-key seeding PASS (0 collisions in 2^17); (a) PASS with the era VDF in the node (era-vdf lane, fork era-vdf-node 394a5902 on release-0.3.20-node c4459193, behind era_vdf_activation_daa; master a4eaf766 carries the spec text, `docs/analysis/era-vdf-2026-10-07.md` and `tools/era-vdf/reroll.mjs`): against the real era cut on three fast-time nodes the re-roll harness fires with the VDF off (6 of 6 cuts) and is silent with it on (0 of 6; the adversary's 5.4 to 6.6 s evaluation against a 1 s block interval, the honest chain 3 to 10 blocks ahead, three nodes agreeing on every era seed); the delay is 517 s on the fastest prover measured (chiavdf NUDUPL over GMP, 208.8K squarings/s) at T = 108,000,000, 259x the 2 s window. Open, not a gate: the verify is 22 ms against the 10 ms target (O-4.6, 0.3.22). Record `docs/analysis/attack-pass/f7-era.md` | PASS (a, b, c) | +| F8 | Uniformity censuses (line-index 2^28, distinct lines, cross-hash histogram) | uniform within the window model of spec 1.13.1, layer 8; the excess beyond it within 6 sigma over 64 seeds; no hot set under 1% of items beyond the model | line index PASS at 2^28. The 6 Oct stream: 31 of 64 seeds over 1.2x (AP-F8-1, the lossy load source). Sub-version 1 (8c728ca3): 11 of 64. Sub-version 2 (07a809a7 / 8bdcbdd8): 9 of 64; AP-F8-2 (exhaustion) closed there, 0 of 10^6. Sub-version 3 (017e7037: the acceptance executing the shadow block, AP-F8-3; the shared-operand rule; (c'') the distinct-index ratio): 60 of 64 under 1.2x, the four over the named unattributed tail at 1.22x to 1.50x with no chip consequence; 0 exhausted in 24,631 chain-shaped seeds, the draw total by construction. Record `docs/analysis/attack-pass/f8-uniform.md` | FIXED-AND-PASSED (sub-version 3, 017e7037, the frozen generator) | +| F9 | Acceptance edges (39) + header grinding on an RTX 5090 | zero passing programs with a hot set under 1%; grinding gain under 1% of rate | (a) edges on generator 4 over 10^5 seeds: 34 disagreements in 105,064 candidates (29 const_bit, 4 bias, 1 lane_const), every one a one-bit or sampling-noise property moving the choice to an attempt both stand-ins accept, nothing in the attacker's favour: PASS; (b) hot-set search over 10^6 seeds: 11,696 passing programs (1.17%) concentrate 1% or more of reads on a hot set, worst 17.3%: FINDING, the same or-saturation load-source class as AP-F8-1 found by a second harness (F9-1 merged into AP-F8-1), re-gated on the amended stream; (c) grinding on the 5090: +0.004% at K = 2^14, ceiling +43%: PASS. Record `docs/analysis/attack-pass/f9-grind.md` | (a) PASS; (b) the AP-F8-1 class on the 6 Oct stream, closed by sub-version 3 (F8's census is the re-gate instrument; this harness's hot-share metric counts the era's windows); (c) PASS | +| F10 | Ladder signal monotonicity harness | no step without 90% over 7 windows in either direction | known-fail fails, known pass passes; new cases on the exact-share driver: 89% up holds (no step), 89 then 90% down with restarts steps only at 90% after the 7-window cool-down, the floor holds under 100% down (never below rung 0); decision per seed block memoised, identical after a restart, never differs between nodes; 19 of 19 checks per case. Two items to main, not findings: a stale commit string in the ladder lane's igneumd, and proof-synced nodes deciding rung 0 until the witness lands (a precondition line for spec 01). Record `docs/analysis/attack-pass/f10-ladder.md` | PASS | + +## The rows + +### F1. Shadow block compressibility and shortcut search (hash lane) + +Method: over 10^4 class v4 programs, constant folding, dead-register elimination, common subexpressions across the +27 repetitions, linear sub-block detection, SAT equivalence on reduced blocks; the minimum op count per program +against N. Known-failed shape: a shadow that constant-folds or dedupes across its 27 identical passes so a chip +pays fewer than 55,296 shadow instructions per hash. Entry point: `igneum-pow show --program-class v4` prints the +256-instruction shadow (op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12 on the +genesis seed). Gate: best compressed block within 5% of N on every program; no program over 10% compressible. +Result: RUNNING. What a failure moves: an acceptance-rule line for the shadow block (rule (c) runs the block), +packs re-cut. + +### F2. Mixer round margin (hash lane, on the box) + +Method: SAT or MILP differential and linear search on 1 to 4 keyed applications with drawn rotations; rotational-XOR +on the ARX layer; the fold of the multiply layer across applications checked algebraically. Known-failed shape: a +differential or linear trail or an algebraic fold that distinguishes or shortcuts more than 2 of the 8 applications +between dependent reads. Gate: no distinguisher or shortcut beyond 2 of the 8 applications. Result: RUNNING (prior +`ca2-mixer` evidence to be re-gated). What a failure moves: `mixer_mult` 16 or a shape change; verifier re-measured. + +### F3. Chained cache j+1 bound and storage-vs-recompute curve (hash lane, on the box) + +Method: exhaustive search on a 2^10-line model segment for a line derivable without an earlier line; the curve from +f = 1/64 to 1 in ops per item. Known-failed shape: a line (s, j) computable in fewer than j+1 block evaluations +without an earlier line (the MTP address-steering break shape). Gate: no derivation under j+1 blocks; curve monotone; +f=1 point unchanged. Result, 7 October 2026, 09:10 to 09:12 UK on the box (`docs/analysis/attack-pass/f3-cache.md`; logs +`/srv/builds/igneum-wt-attack/attack-f3/r1-*.log`): PASS on all three clauses. The chain extracted from +`Cache::fill_segment` (verified equal to the code on 16 of 16 key and segment pairs; `block == chacha_block` on +100,000 random inputs) has line j fed by line j - 1 only; the exhaustive closure search finds 0 of 64 and 0 of +1,024 lines under j + 1 (every line costs exactly j + 1), cross-checked by an exhaustive pebbling search at 10 +lines (10,240 configuration and target pairs, 0 mismatches). The two planted chains fire: `skip2` (63 of 64 under +j + 1) and `nofeed` (every line in 1 block). The curve over stored cache lines is monotone non-increasing from +f = 1/64 to 1 at 608 (counted) and 700 (MEMHARD.md) ops per block; the f = 1 point is 9,360 ops per item, +41.7 MH/s at the 50 T op/s budget, unchanged. `ca2-cache` was the hot-table experiment, not a chain analysis, so +there was nothing to re-gate. Observation (coordinator and the F3 record, not a finding): `funding.md` B2 rank 2 +prices the trade-off at the naive placement; the optimal placement of every 8th line costs 3.17 blocks per read, +not 3.5, and 16.0 at f = 1/64, not 31.5 (brute force over 4,426,165,368 sets at n = 8); the chip stays worse than +the full mirror at every f under 1, so the verdict stands, and a `chacha_block` shortcut in chaining mode stays +the paid question (Lot A and B). What a failure would have moved: the chain construction (a second feed-forward or +a cross-segment tie). + +### F4. Weak-day census over 2^24 day keys (hash lane, on the box) + +Method: 2^24 day keys through `MixParams::with_shape`; the ROT classes (all equal, complementary pairs, small +amounts), MUL low weight, RC structure, each per-day gain measured on the box verifier. Known-failed shape: a day +key whose drawn ROT/MUL/RC gives a fixed datapath a gain over 1.1x (the "weaker authorized parameters" class, +Kudelski 2019). Gate: the fraction of days with any gain over 1.1x under 2^-20. Result: RUNNING. What a failure +moves: a rejection-and-redraw rule on the draws. + +### F5. Chip-model sweep and the FPGA hour (algorithm lane) + +Method: `sim/horizon/algorithm/model.py` over k 0.2 to 1.5, tFAW 12 and 28 ns, HBM4 2.3 and 21.4 G reads per +stack, amortisation 1 to 3 years, electricity USD 0.05 to 0.15 per kWh; and the AWS F2 hour replacing the FPGA +ceiling row with a measurement. Known-failed shape: an input of the published model that, when corrected, lifts the +f=1 chip's per-joule edge over the 5090 above the published 2.1x at k=1. + +Gate: the published sentence (evidence row 17) holds across the sweep; the FPGA row under 27 M reads/s/W. + +Result (sweep): PASS on the numbers. The model's measured-anchor column (GDDR7, the 5090 reads 82% of its ceiling) +gives the f=1 chip's v4 per-joule edge over the RTX 5090 bench row as 4.1x / 3.2x / 2.1x / 1.5x at k = 0.3 / 0.5 / +1 / 1.5. At k = 1 the figure is 2.1x, and 3.9x at k about 0.33, which matches `fud-ledger.md` M32. The higher HBM3 +and HBM4 columns rest on an 8-activate per 12 ns window that JEDEC HBM2 timings (4 per 28 ns) do not support; the +model already states GDDR7 is the column to quote. One wording gap: `evidence.md` row 17 says "brings it to about +2x", which is a floor that holds at k about 0.9 and above but understates the edge at lower k (3.2x at k = 0.5). The +accurate statement is M32's, 2.1x at k = 1 with the k range beside it. The sweep's numbers stand; the finding is the +X9 framing below. + +FPGA row: the HBM2 FPGA ceiling is 2.3 to 2.9 G reads/s (measured Shuhai U280, FCCM 2020, equal to the JEDEC +tFAW-bound 2.3 G/s), 10 to 21 M reads/s/W at 115 to 150 W, 0.30 to 0.47x of the 5090 per watt. Under the 27 M +reads/s/W gate. The AWS F2 hour is SKIPPED-BY-DECISION (the founder, 7 October 2026, 09:5x UK: not needed for now, not blocked; +plan 4.2 row F5 at commit 3714c2a0 on branch cryptanalysis is the chip-model sweep only, 4 h, the algorithm lane). +There is also no AWS account or `aws` CLI on this Mac. The FPGA row stays the JEDEC-ceiling model row labelled +unmeasured; Lot C prices it from the reads-in-flight model; the firm is told the F2 measurement was not run. + +FINDING (X9 framing), owning lane algorithm and hash (the ladder lane is closed, so ours): the published numbers +already carry 2.1x at k = 1 beside 3.9x at k about 0.33 (`fud-ledger.md` M32, recalibrated under X35). The error is +the framing. M32 calls the k = 0.33 figure "the X9's core" and the `ladder` branch's `latency-ladder.md` section 5a +calls k about 0.33 a "measured class". Bitmain's Antminer X9 (RandomX ASIC, 1 MH/s, 2,472 W, about USD 5,600) was +announced and, per pcpraha.cz ("Antminer X9 canceled: Bitmain withdraws model from market before launch") and +r/MoneroMining, withdrawn before launch. Its implied core efficiency (k about 0.33) is a CLAIMED datasheet figure +from a design that never shipped and was never benchmarked, not a measured calibration point. It is carried as the +pessimistic bound, not a calibration. This collides with the merged ledger X34 ("RandomX has a shipping chip; +correct every sentence that said otherwise"): if the X9 was withdrawn, X34's correction is itself wrong and must be +reversed. Confirmed from primary sources (coordinator, 7 October 2026): pre-orders opened 26 December 2025 (shipments +scheduled for July 2026), withdrawn in mid-May 2026 with buyers refunded before any unit shipped, no independent +benchmark, Bitmain never published a cancellation (its shop lists it as sold out), and the box was commodity Sophgo +SG2044 server SoCs with an AES accelerator and 60-plus DRAM sticks, no tapeout; its claimed edge about 2x per joule +over a tuned Zen 4 part, about 3x over a stock desktop CPU. Re-cut applied the same day: `evidence.md` row 17 (claim +and measured cells) and `fud-ledger.md` M32's answer paragraph on attack-pass, and the ladder design doc section 5a +on branch `attack-ladder-5a` from the ladder tip 7003f9f5 (the `ladder` branch is checked out by another lane, so +the fix rides its own branch for the ladder owner to take). The served-text rows (X34 reversal, X36) belong to the +site lane, which confirmed the wording agrees. What a finding moves +(plan 4.2 F5): the sentence re-cut before the freeze so the firms attack the corrected model. The re-cut, once the +fact is confirmed: k about 0.33 labelled a claimed pessimistic bound from a withdrawn design everywhere it appears; +2.1x at k = 1 on the GDDR7 measured anchor kept as the headline with the k range beside it; k itself unmeasured +until Lot C produces it. This row reads FIXED-AND-PASSED only after the re-cut and its re-gate. + +Economic row the withdrawal implies: a recompute chip at a 3x fixed-function factor against a CPU and GPU fleet +must recover its NRE (low to mid seven figures at a modern node, `chip-model-v3.md`) and carry a fork threat (a +class change at 95% miner signal can redraw the datapath the chip bakes in). The X9 at 2.47 J per KH against a +RandomX CPU fleet did not clear that bar at Monero's hash and price; the same arithmetic against Igneum's class v4, +with the shadow block and the automatic era draw as extra firmware risk, is why the chip model's verdict is a +deliverable and not a courtesy (plan 2.3). The confirmed reading: a box with a 2x to 3x per-joule edge and no NRE (commodity SoCs) was withdrawn rather than +face a 1.5x re-tune of RandomX, so the tapeout economics of a 3x chip against Igneum are worse than the X9's. This +row is the pessimistic case, not a measured gain. + +### F6. Verifier worst case (algorithm lane) + +Method: 10^5 class v4 programs timed on the box one-core and half-core proxies for the slowest warp (base program +and shadow block), plus the O-1.14 laptop run (the Windows `igneum-pow` build on the box, the relay, `bench +--warps 50`). Known-failed shape: a drawn program whose verifier warp exceeds 10 ms cold (the acceptance rule bounds +the miner's side, not the verifier's; `dr736` already FAILs at 10.51 ms cold one-core, but it is not the shipping +class). Gate: the worst program under 10 ms cold on the half-core proxy and on the laptop. + +O-1.14 route (coordinator, 7 October 2026, 10:1x UK): no US laptop is due, so the laptop run is replaced by a +rented 2019-class CPU host through the fleet agent, capped at two hours of rent; the Linux `igneum-pow` from the box +(the same binary as the proxies) runs `bench --warps 50` for v2, mx8, mx8+sh256x27, dr368 and dr736 (the +known-fail) on it; INCOMPLETE with the numbers so far if the cap lands first. The Windows exe was also built on the +box for the day a laptop appears (1,009,675 bytes, sha256 fbed7538...e6c9). Outcome, 09:40 UK: no 2019-class CPU +host stood up. Vast accepted and dropped five CPU-class rents within 30 s each (i7-9700K, i5-8500, Xeon W-2133 and +W-2123) under the account's automatic new-account spend limit (support ticket open since 6 October), and RunPod has +no 2019-class CPU pod; so O-1.14 reads INCOMPLETE on the box proxies today and stays a precondition of the freeze. +Next try: the US laptop when it registers on the relay (the exe is ready), or Vast once the spend limit lifts; the +bench script is staged and runs in minutes. Correction, 10:4x UK (fleet agent): the provider dropped nothing; every +rent stood up and ran, hidden by Vast's instance listing cap of 25 rows on an account holding 38, so the hosts sat +idle and were destroyed. The fallback is re-rented under the same word (i7-9700K class, two-hour cap from its start, +read by id); the result replaces this line when it lands. + +O-1.14 RESULT, 7 October 2026, 09:49 UK, Vast instance 54613164, Intel Core i7-9700K (2019 desktop core, Coffee Lake, +read at 4,170 MHz during the run, 31 GB DDR4, Ubuntu 24.04), the box-built Linux `igneum-pow` (sha256 6d286783...), +`bench --seed igneum-genesis --day 2026-10-03 --warps 50` on one core (`taskset -c 1`), the host otherwise idle; log +`docs/analysis/attack-pass/o114-i7-9700K-2026-10-07.log`: + +| Class | Cold max (ms per warp) | Average of 50 (ms) | Gate 10 ms | Box one-core cold | Box half-core | +|---|---|---|---|---|---| +| v2 | 1.582 | 1.280 | pass | 1.30 | n/a | +| mx8 (class v3) | 5.394 | 5.267 | pass | 4.67 | 7.56 | +| mx8+sh256x27 (class v4, the target) | 6.334 | 6.006 | pass, 3.7 ms of headroom | 5.06 | 8.23 | +| dr368 | 5.540 | 5.426 | pass | 5.32 | 8.16 | +| dr736 (the known-fail) | 10.290 | 10.042 | FAIL, as it must | 10.51 | 15.49 | + +Cache fill 276 ms on the 9700K core (box 361 ms, M5 Max 175 to 181 ms). The 2019 desktop core sits between the box's +two proxies as the arithmetic predicted (1.2x the box one-core cold on v4, 0.77x the half-core); the known-fail +fires on it. A 2019 laptop core at 3.5 GHz reads about 15 to 20 percent slower than this desktop part (approximate, +clock ratio), so about 7.0 to 7.6 ms on v4, still under 10 ms. Consequences: a 2019-class node verifying class v4 +spends 0.6 percent of one core at 1 bps and 6 percent at 10 bps; a header flood needs about 160 invalid headers a +second to saturate one such core; a pool verifies about 160 shares a second per core; IBD of 108,000 headers is +about 11 minutes of one core. The implied ladder ceiling on this core: the shadow costs 0.74 ms per 55,296 +instructions (v4 minus mx8), so the 4.0 ms of headroom buys about 300,000 more shadow instructions, N about 650,000 +counted ops at the 1.83 convention (approximate), against 370,000 on the half-core proxy and 1,060,000 on the +2.5x rule; the half-core proxy stays the standing pessimistic rule and the ladder's ceiling should be taken from +it, not from this desktop part. O-1.14 is CLOSED on a real 2019-class core for the genesis program; the F6 row +still owes the 10^5-program worst case before it reads PASS. + +Result (average, verified on the box): class v4 `mx8+sh256x27` runs 4.90 to 5.06 ms per warp cold on one EPYC +9454P core (nice 19, taskset), 8.23 ms on the half-core proxy (both SMT siblings busy). Under 10 ms. Status +RUNNING: the 10^5-program worst-case search and the O-1.14 laptop relay run are owed before the row reads PASS. +What a failure moves: an acceptance-rule bound on verifier cost; the ladder's ceiling set from the measured core. + +### F7. Era-draw bias harness and census (node lane harness, hash lane census) + +Method: the fast-time 3-node network (`infra/fast-time/`) with an adversary withholding or publishing the last blue +block before C_era(n) to re-roll the draw; a census of 2^20 era seeds for stride, ROT and weight-perturbation +classes with gain over 1.1x; the 64-bit seeding of the day-key stream against the spec's intent. Known-failed shape: +a re-roll of the era draw inside the 2 s publish window, or an era class (stride bijection, all-equal ROT, low-weight +M) with a chip gain. Gate: no re-roll inside the publish window; no era class with gain over 1.1x at a fraction over +2^-20; the draw's input set as the spec states it. + +Result (full record `docs/analysis/attack-pass/f7-era.md`; harness `tools/attack/f7-era/`). Census: 2^20 and 2^24 era +seeds through `generator::era_draw` over `V3_ALLOWED` (the chain's path), classified; the planted known-fail/known-pass +of the classifier fired and the sound draw raised nothing. No era class with gain over 1.1x at any fraction (the richest +is M = 1 at 1.0034x, absent in 2^24; every class over 2^-20 is 1.0000x to 1.0007x); the stride is a bijection on every +sample (0 even M), R and pos and the M bits uniform; the op-weight corners (15 to 31 of 75) are 1.0x against the GPU, 0 +memory effect. The 64-bit day-key seeding is the spec's intent (spec 1.8.4); 2^16 days are all distinct, birthday 2^-33. +Harness: the 3-node fast-time network (`reroll.mjs`, ports 29800+, suffix 980) with an adversary holding the last block +before the cut; known-pass (`--vdf-ms 0`) fires at 1 of 6 cuts (seed = adversary block), known-fail (`--vdf-ms 5000`) +is silent at 0 of 6, both SOUND. The node has no era VDF yet (`seed_below` is a plain block hash, era-layout.md section +8), so the harness cannot show the real 2 s-window gate; the era draw's grinding resistance rests on the 1-hour VDF of +spec 4.4 (re-roll needs a 1,800x evaluator, spec 4.6 gives 300x; forge needs 20 days of 100% hash). Verdict: census PASS, +64-bit seeding PASS, harness INCOMPLETE with the written argument. Logs on igneum-build-1 +`/srv/builds/igneum-wt-attack/attack-f7/census-2p24.log`, `census-2p20.log`, `reroll-knownpass.log`, `reroll-knownfail.log`. +What a failure moves: the draw procedure or the C_era cut rule; a redraw rule for the era stream. + +Sub-row (a) CLOSED, 7 October 2026, 15:1x UK (the era-VDF lane, launched by the coordinator on this row's INCOMPLETE): +the era VDF is in the node (fork `era-vdf-node` 394a5902 on `release-0.3.20-node` c4459193, behind +`era_vdf_activation_daa`, never on any network until the founder sets it per network; repo master a4eaf766 carries the spec +text, the record `docs/analysis/era-vdf-2026-10-07.md` and the harness `tools/era-vdf/reroll.mjs`, which attacks the +era cut directly now that the node takes `pow_era_blocks` and `pow_era_lead` from the override file). Against the +REAL era cut (era 120 DAA, lead 20 on the fast-time file, three nodes on igneum-build-2) the harness fires with the +VDF off (6 of 6 cuts: the adversary's block is the cut block and its hash the seed, known the instant it is built) and +is silent with it on (0 of 6 across six cuts: the adversary's 5.4 to 6.6 s evaluation with the node's own code against +a 1 s block interval, the honest chain 3 to 10 blocks ahead when it published, three nodes agreeing on every era seed, +the record ready at every era start). SOUND both ways. The production delay: 517 s on the fastest prover measured +(chiavdf NUDUPL over GMP, 208.8K squarings/s on the same core) at T = 108,000,000 squarings, 259x the 2 s window. +Freeze sentence (the era-VDF lane's, carried to the plan's owner): "The era seed E_n is the output of a one-hour +verifiable delay (class-group Wesolowski, 1,024-bit prime discriminant, T 108,000,000, scheme byte 0 with the +hash-chain fallback as byte 1) over the blue blocks of the day ending at the era's cut block; the attack pass's F7 +re-roll harness fires against the stand-in and is silent against the delay, so the era draw procedure and the C_era +cut rule are frozen with the VDF in the node, behind era_vdf_activation_daa, never until set per network." Open, not +a gate of F7: the verify is 22 ms with the group held, against the 10 ms target (once per 180 days per importing +node; O-4.6's reducer or GMP behind a feature, 0.3.22). Logs `/srv/builds/igneum-wt-era-vdf/ev-harness-out/ +reroll-vdf-{on,off}-5.json` on build-2. F7: PASS on all three sub-rows. + +### F8. Uniformity censuses (hash lane, on the box) + +Method: the line-index distribution over 2^28 derivations; distinct lines per hash and per warp on 10^6 nonces of +three programs; the cross-hash item histogram of one epoch. Gate: the largest bucket within 6 sigma of uniform; no +hot set under 1% of items. Result: RUNNING. What a failure moves: the mask or the fold; packs re-cut. + +### F9. Acceptance edges and header grinding (hash lane; one PC 2 job) + +Method: the 39 edge disagreements reproduced and bounded; a search over 10^6 seeds for programs that pass rule (c) +with a hot set under 1%; the header-grinding search cost against its DRAM-locality gain measured on PC 2's RTX 5090 +(one job through `tools/build-job.mjs`). Known-failed shape: a seed grind that steers a program to a hot cache set +for DRAM locality, or an edge where the closed-form stand-in disagrees with the live verifier in the attacker's +favour. Gate: zero passing programs with a hot set under 1%; the grinding gain under 1% of rate at any search cost. + +Result: the `accept` path reproduces per-seed verdicts (genesis seed: 1 candidate ACCEPTED, bias max 54, 0 +saturated). The header-grinding cost-versus-gain measurement needs a 5090. Status BLOCKED on the go decision: use +PC 2's 5090 through a relay run job only if PC 2 is online and mining is unaffected, else a rented pod under the +standing fleet budget. What a failure moves: the closed-form stand-in replaced by the live verdict at the edges; a +locality term in rule (c). + +### F10. Ladder signal monotonicity (node lane) + +Method: the fast-time harness with a weight that steps the ladder down and never up, and an 89% signal; the step +rule's monotonicity and its memoisation per seed block. Known-failed shape: a chip owner stepping the ladder down +(cheaper N) without the 90% threshold, or a step registered under 90%. Gate: no step without 90% over 7 windows in +either direction; a step down needs the same. Result: RUNNING. What a failure moves: the step rule's text in spec 01 +before the ladder is frozen. + +## Lane (d): the families re-run on class v5 (7 October 2026, evening; the coordinator's word on the founder's order) + +Object: igneum-pow on branch `class-v5` at e4f1f275 (the frozen sub-version 3 017e7037 merged; the v5 chain draw is +the amended v4's instruction for instruction, generator 5, every item keyed by the window's state through the leaf +XOR before the first mixer; `V5_CLASS` = `mx8+sh256x27+state`), against the first v5 pack +`proto-cuda/packs-ca3-v5/v5-dn3-epoch0` (Devnet 3's genesis 4020cb43... as epoch and era seed, day 20,733, program id +e5a4ac5978462156, reproduced by the e4f1f275 build on box 2: the pairing). The four harnesses carried onto the +class-v5 tree in worktree `igneum-wt-attack-v5` (branch `attack-v5`), each with `--class v5` and, where the dataset +enters, `--state ` attaching the leaves through `with_leaves` as the CLI does; F8's traced derivation carries +the leaf XOR and validates bit for bit against `derive_items_leaves` and `Epoch::hash_warp` (p1 on the dn3 state at +4,096 nonces: 0 mismatches over 16,777,216 items and 64 warps; the flipped-state file mismatches: the known-fail). +Both boxes at nice 10 beside the release builds; the binaries run from copies in each run's scratch directory (AP-H2). +GitHub answered 403 (account suspended) from 17:2x UK, so this section lands on the box mirror (`build`, master and +attack-pass) by the coordinator's exception rule; nothing touches GitHub. + +| Family | Class v5 run | Result | Verdict | +|---|---|---|---| +| F4 weak-day census | 2^24 chain days from 20,729 under `Shape::for_class(&V5_CLASS)`, box 1, 18:5x to 19:1x UTC | byte-identical to the class v4 census: M2 (DSP-bound) 0 of 2^24 days over 1.1x; M1 (LUT adders) 5,476 days, 3.264e-4, the same bounded tail, worst day 4,819,563 at cost 197 against the median 231; planted weak days fire (mul1all M2 unbounded, mulnaf 1.333x). The day-key draw depends on the mixer shape alone and v5 adds only the state flag, so identity is the expected and the measured result | PASS (v4's reading; AP-F4-1 stays the next-class item) | +| F8 hot-set gate | 64 seeds at 2^24, chain path, v5 with the dn3 state, box 2 (64 threads), from 18:47 UTC | pending | pending | +| F9 exhaustion count | 10^5 chain-shaped seeds on the v5 chain path with the dn3 state, box 1, ten parallel chunks (the chain draw costs about 2.2 s per candidate through (c''), so 10^6 is about fifty hours) | pending | pending | +| F1 shadow redundancy | 10^5 class v5 programs through the string-seed path, box 1 | pending | pending | + +## Operating hazards found by the pass + +AP-H1 (box scratch cleaned by builds; found by F3, 7 October 2026, 10:0x UK). `infra/build-server/remote-run.sh` +line 71 runs `git clean -qfd -e target -e 'target-*' ...` on `/srv/builds/` before every remote build, so +an untracked box scratch directory of one row (a venv, a log dir, a crate's `tools/attack/*/target`) is deleted by +the next build from any row. F3 protected its own directory through the box mirror's `.git/info/exclude`; the lane +then added `attack-*/`, `target-attack-*/`, `tools/attack/` and `.build-remote.log` to that file at 10:1x UK, after +which `git clean -fdn` on the mirror lists nothing (the clean has no `-x`, so the exclude file applies). The class +check is owed to the build-server lane: the clean line should spare a lane's declared scratch prefix (`-e 'attack-*'` +style, or read a per-worktree exclude list), and a CI check should fail a remote-run.sh whose clean line lacks it. +OPEN until that check lands (CLAUDE.md: a rule row closes only with its check). + +AP-H2 (this lane's own, 7 October 2026, 13:3x and 14:5x UK, twice). Two census runs launched from the same crate's +`target/release` binary path on the box mirror: a rebuild of the crate at a new commit replaces the binary under a +run still in progress, and every chunk the run launches after that executes the new commit's code with the old run's +label (the 07a809a7 control's later chunks ran 8bdcbdd8; the ddacfbd3 class check's later chunks ran 017e7037). Both +runs were caught by their attempt histograms (attempts 32 and 35 under a cap of 32) and their contaminated chunks +discarded. Fix in the lane's launcher: `run-census-chain.sh` copies the binary into the run's own scratch directory +before the first chunk and runs from the copy, so a rebuild cannot reach a run in progress; a run's record names the +sha256 of the copy. Class check owed: the same rule for every lane's long run (the box's build runner could refuse to +replace a binary that a running process has open, or stamp the commit into the run's log at every chunk). + +## Ledger rows + +AP-F1-1 (hash lane; ruling asked). At 10^5 class v4 programs one program (`attack-f1/37341`) compresses by 5.078 +percent (13 of 256 shadow instructions per pass), 0.078 points over the gate's first clause, on 1 of 100,000; every +other program is within 5 percent and none over 10. The saving is the same local shape as on every program (a +register written twice from one source with no write between), nothing crosses a pass, and clang -O3 removes the +same instructions from the honest kernel (IR counts match the harness on the worst programs), so a chip gains nothing +relative to a card: no shortcut. The gate as written counts honest-compiler simplification as compressibility. Two +ways to close: re-word gate (1) and row F1 to "compressible beyond the honest compiler's own simplification" (the +firms then attack chip-relative compression, which is the question), or a shadow-draw redundancy bound in the next +class (reject a shadow with over 12 peephole-removable instructions per pass, rejection about 1e-5; class v4 is on +the live vote). Ruling (coordinator, 7 October 2026, 12:3x UK): both. Gate (1) and row F1 re-worded to "no +compression of the shadow block beyond the honest compiler's simplification, measured against that compiler on the +same program" (sent to the cryptanalysis lane for the plan and the firms' brief), under which the 5.078 percent +letter miss at honest-compiler parity is a PASS; and a shadow redundancy bound on the v5 generator's list beside +AP-F4-1 and AP-F8-1 (the generator refuses a shadow block whose honest-compiler simplification exceeds a stated +fraction; the v5 lane sets the fraction from F1's census), gated by F1's harness on 64 seeds of the v5 stream. +The fraction is 3.0 percent (v5 lane, class-v5 45e29cb0; 384 of 100,000 draws redrawn in its census, 3.8e-3, against +F1's histogram where the 3.0 to 5.5 percent bins hold 397 of 100,000); the plan's 1.1 sentence carries the number. +Status: F1 PASS; AP-F1-1 FIXED-AND-PASSED against v5 once the bound is in the v5 generator and F1's census passes. + + +AP-F5-1 (algorithm and hash lane, ours; the ladder lane is closed). The k about 0.33 chip-efficiency figure is +framed as a measured calibration ("the X9's core", `fud-ledger.md` M32 L172; "measured class", `ladder` branch +`docs/design/latency-ladder.md` section 5a). The Antminer X9 was withdrawn before launch and never benchmarked, so +k about 0.33 is a claimed datasheet bound, not a measurement. This also puts the merged ledger X34 ("RandomX has a +shipping chip") in question. Fix owed, held until the coordinator's research agent confirms the withdrawal and the +no-benchmark fact: relabel k about 0.33 as a claimed pessimistic bound from a withdrawn design in `evidence.md` row +17, `fud-ledger.md` M32 and the `ladder` branch; reverse X34 if the withdrawal is confirmed; keep 2.1x at k = 1 on +the GDDR7 measured anchor as the headline with the k range beside it. Re-gate after the re-cut. Status: FIXED on the docs rows (evidence 17, M32, ladder 5a on branch attack-ladder-5a d3cb17b6; attack-pass +rebased on master a3678789 after X36); FIXED-AND-PASSED once the site lane's X34/X36 served rows are confirmed in +one voice (no objection received) and the sweep is re-run against the re-cut sentence (the numbers are unchanged, so +the re-gate is the identity check and one `model.py --section chip` run against the new wording). Re-gate done 7 October 2026, 09:5x UK: the 5090 +bench row still reads 5.7x / 4.1x / 3.2x / 2.1x / 1.5x (v3; v4 at k = 0.3 / 0.5 / 1 / 1.5), identity grep 0 hits +over 290 export files, the site lane confirmed the served text agrees. AP-F5-1: FIXED-AND-PASSED. + +AP-F8-1 (hash lane; the generator fix is the Counter ASIC lane's on the v4 seam, routed 7 October 2026, 10:3x UK). +The class v4 item read map is not uniform. F8 phase D, one program, 2^26 nonces: the top 0.1 percent of items take +0.520 percent of reads against 0.115 percent for the uniform control (4.05x); the top 1 percent take 2.49 percent +(1.37x); one item (0xca5b92) takes 78,479 reads, 153x the mean; read site 15 feeds 6.37 percent of its reads into +that 0.1 percent in all 8 iterations; the excess grows with N as a real skew does. Sized: a chip caching the hot +0.1 percent in SRAM serves about 0.5 percent of reads from cache, so the shortcut is under one percent of rate today; +an auditor flags a non-uniform read map in a design that claims uniform random reads, and site 15's index derivation +is the cause to name. Fix asked: per-site index whitening or a rejected class above a bound. Re-gate: the top +0.1 percent within 1.2x of the control over 2^26 nonces on every one of 64 seeds, with F8's harness against the +Counter ASIC lane's branch. Phase E (the 64-program census) decides whether it is one program or the class. +Framing from the Counter ASIC lane (the generator's owner, 7 October 2026, 10:5x UK): class v4's item map is not +designed to be uniform per program. Layer 8 (spec 01 section 1.13.1) gives each load site k_off = below(3), so a +site reads the whole dataset, a half or a quarter under the era's stride and interleave; a quarter-window site +concentrates 4x on its quarter by design, which is the 4.05x at the top 0.1 percent, and the windows exist so a +chip's SRAM mirror must hold the whole dataset every hour (the Counter ASIC 2.0 windows-union census). The right +control is therefore the window model from the program's own 16 draws, reported beside the uniform control (what an +auditor sees first); the number that must be explained is the single item 0xca5b92 at 153x the mean (window +coincidence under the era mapping with a stated tail, or a low-entropy index source at site 15, which would be a +fault). The lane reproduces with F8's harness on branch `ca3-v4-uniform`, waits for phase E, re-prices the chip +consequence (a 0.1 percent hot-set cache, about 1.7 MB of SRAM, serving 0.5 percent of reads: under one percent of +rate) and changes the generator only on a fault beyond the model, since v4 is on the live devnet's vote. F8 was +re-briefed to carry both controls and the per-site table. Raised to the coordinator: plan 1.4 gate (4) and row F8 +say "within 6 sigma of uniform"; if the design is windowed, the gate text must say "uniform within the window model +of spec 1.13.1" before the freeze tag, or every reviewer files the windows as a finding on day one. +Coordinator's ruling (7 October 2026, 11:0x UK), accepted: the right null is the window model derived from the +program's own draws; F8 is re-gated against it, and the finding stays open only for the excess beyond the window +model (the 153x item, or a low-entropy source at site 15 if the 64-seed census shows one). No generator change to +class v4 is allowed: it is on the live devnet's vote, and a class change before the flip splits the chain. If the +census shows a real fault it goes to the coordinator priced; otherwise the record carries the documented null and +the hot-set bound (a 0.1 percent cache, about 1.7 MB of SRAM, under one percent of rate) goes into the next class. +Gate wording settled (coordinator, 11:2x UK): plan 1.4 gate (4) and row F8 now read "uniform within the window +model of spec 1.13.1, layer 8; the excess beyond it within 6 sigma over 64 seeds", carried into the plan's scope +text by the cryptanalysis lane so the firms are briefed on the windows before they start. +Mechanism (hash lane, branch `ca3-v4-uniform` 095f84a7, `docs/analysis/ca3-v4-uniform.md`, harness +`tools/ca3-v4-uniform`, 7 October 2026, 12:3x UK): the windows-union null (a Poisson mixture at 416 / 288 / 736 / 608 +reads per item by quarter from the program's 16 draws) moves the top 0.1 percent from 0.115 to 0.160 percent, 1.39x, +not 4.05x; every per-site row of F8's attribution except site 15 is the window model. The rest is the LOAD SOURCE: +site 15 is the load at 63 reading r6, whose last writer is `or` at 61 (r6 = r6 | r4), so the source is all-ones with +probability about (3/4)^32 per read; under the era map x = 0xffffffff is item 0xca5b92, the hottest item exactly, and +the next seven hottest are the seven one-zero-bit sources whose zero survives the window mask (7 of 7); the measured +count fixes the bias at p = 0.7585 per bit. The class: a load whose source's last writer is lossy (or: 0.30 percent +of a site's reads on 0.1 percent of values; mul, trailing zeros: 1.07; mulhi: 0.79; an or of an or: about 4.5). +Static census of 1,024 chain-shaped v4 programs: 96.6 percent carry a lossy-sourced load (48.5 percent or, 4.9 +percent an or chain, 73 percent mul, 64 percent mulhi); predicted S_0.1 median 0.45, 90th 0.88, 99th 5.3, max 9.8 +percent; p1 / p2 / p3 predicted 0.58 / 0.32 / 4.72 against measured 0.52 / 0.27 / 4.60. The fault sits in the +acceptance rule's blind spot: part (a) takes any write as fresh, part (c) counts saturation on final values only. +Consequence: the 1.2x-against-window gate fails 96.6 percent of today's programs, so it is withdrawn as a v4 gate and +becomes the v5 generator item's gate (draw a load's source from registers whose last writer injects; a dynamic check +counting saturated load sources), with F8's phase E as its test. Chip side: the top 0.1 percent of items is 1.07 MB +of SRAM (0.53 mm^2, about USD 0.25) serving 0.52 percent of p1's reads and 4.6 percent of p3's, at most 1.005x and +1.048x in rate; the ceiling under rule (c)'s 120-of-128 floor is one site repeating its item in all 8 iterations, +6.25 percent of reads, 1.067x. That 1.067x is the v4 hot-set bound the record carries. No generator change to v4; +the hash lane takes the two flip options priced to main. +Ruling (the founder, 7 October 2026, 15:2x UK): option A, the class v4 amendment ships in 0.3.20, the feature node (0.3.19 is the app-only cut on the unchanged 0.3.17 node pin; corrected by the coordinator) (a load's source drawn +only from registers whose last writer injects or is a rotate, the v5 rule applied now; a new program stream and +seven re-exported packs on branch `ca3-v4-amend`, the hash lane), with limited testing. This lane's part is the proof +of the fix: F8's hot-set census at 2^24 nonces on each of 64 seeds of the amended stream, on the box's CPU path as +phase D ran, gate: the top 0.1 percent of items within 1.2x of the window model derived from each program's own 16 +window draws, one number per seed; reported to the hash lane, main and the Counter ASIC lane. The amended stream has +no lossy-sourced load by construction, so a seed over 1.2x there is a finding against the model's own tail, not the +fault, and the record says which. +Second harness (F9 sub-row b, 10^6 seeds, 12:5x UK): the same class from the other side, the per-site address trace: +11,696 of 1,000,000 passing programs concentrate 1 percent or more of their reads on a hot set (worst 17.3 percent, +seed 842871, an `or`-written load source all-ones in 36 percent of evaluations), so F9-1 merges into AP-F8-1 and +F9's harness is the second re-gate of the amendment, run on the amended stream beside F8's 64-seed census. +Re-gate interim (7 October 2026, 13:2x to 13:5x UK, box 2): F8's 64-seed census at 2^24 nonces against the amended +stream (igneum-pow 8c728ca3, sub-version 1; pairing verified, the harness draws the devnet epoch-0 program as +1a4230699a6b9c60) at 30 of 64 seeds shows nine over 1.2x of the window model (p31 29.27x, p11 5.45x, p19 3.32x, p6 +3.11x, p23 2.04x, p4 1.57x, p10 1.50x, p26 1.30x, p25 1.28x), p6's hottest item predicted from "site 13, r0, +all-ones, last writer a load at 12": a load-after-load chain (a hot address yields a fixed dataset word, which is the +next load's address), which the source rule admits because a load injects. Rule-level reading, checkable in code: +generator.rs line 1326 sets `entropy_kept[dst]` true for a rotate whatever it rotated, so an or-saturated register +rotated once is an admitted source and the rotate preserves the saturation. RETRACTION: F9's hot-set census run on +box 2 against the 8c728ca3 build (10^6 seeds, 1,871 flagged, worst 9.66 percent) was not a re-gate: the F9 harness +draws through `candidate_class` with its own era class, not through `chain_program` where the rule lives, and the +amended and the old binary print the identical program for seed igneum-f9/518927; those numbers describe the old +stream under a changed evaluation and are withdrawn; the harness is being given a `chain_program` draw mode so it can +serve as the second re-gate. The hash lane confirmed the reading (14:0x UK): the amendment's rule is keyed on the +era-composed class, so a draw with no era (F9's path) is the old stream, and on the chain path the residual is real: +p6's load at 12 had a saturated source itself, read one constant word and left a constant in r0, which the rule +counts as injecting; a rotate keeps 0xffffffff, so or-then-rotate-then-load passes too. Both are saturation delivered +through a writer that preserves it. Fix shape put to the owner of sub-version 2 (the Counter ASIC lane): dataflow +freshness instead of a one-writer look-back (fresh at the start; a load keeps dst fresh only if its source was fresh; +add, sub, xor, mad, shfl fresh if either operand was; rotl, rotr only if the operand was; or, mul, mulhi never; a +load's source drawn only from fresh registers), with the dynamic (c') check on load sources as the backstop; a stream +change, so sub-version 2 with new packs, ids and fingerprints. +RE-GATE VERDICT on sub-version 1 (7 October 2026, census ended 12:55:55 UTC, 13:55 UK; box 2; igneum-pow 8c728ca3 +paired with release-0.3.20-node 8097d600, pairing id 1a4230699a6b9c60 verified; 64 seeds p2 to p65 at 2^24 nonces, +chain path, window-model control; log `/srv/builds/igneum-wt-attack-regate/attack-f8-regate/log/`): FAIL the pass +line. 53 of 64 seeds under 1.2x of the window model (0.9915x to 1.16x, no predicted source); 11 over: + +| Seed | Over the window model | Over flat | Hottest item, reads of 2^31 | Predicted source | +|---|---|---|---|---| +| p31 | 29.27x | 31.99x | 0x74e2b8, 5,365,527 | site 4, r4, all-ones, last writer rotl at 3 | +| p11 | 5.45x | 6.00x | 0x0eec66, 31,486 | site 1, r7, all-ones, last writer or at 63 (the previous iteration) | +| p45 | 4.55x | 6.37x | 0x400000, 5,644 | site 1, r4, zero, last writer mulhi at 59 | +| p19 | 3.32x | 3.93x | 0x400000, 28,114 | site 37, r5, zero, last writer load at 32 | +| p6 | 3.11x | | 0x3bf40d, 13,792 | site 13, r0, all-ones, last writer load at 12 | +| p23 | 2.04x | 2.85x | 0x09dd36, 13,848 | site 16, r7, all-ones, last writer load at 14 | +| p4 | 1.57x | 2.11x | 353 reads | none (window tail) | +| p34 | 1.51x | 1.87x | 0x400000, 1,547 | site 23, r6, zero, last writer rotr at 12 | +| p10 | 1.50x | 1.65x | 353 reads | none (window tail) | +| p26 | 1.30x | 1.54x | 0x000000, 7,637 | site 10, r1, zero, last writer rotl at 2 | +| p25 | 1.28x | 1.65x | 363 reads | none (window tail) | + +Three residual classes, each a constant (all-ones or zero) delivered to a load through a writer the rule admits: +(1) saturation or zero preserved through rotl, rotr, load or mad; (2) zero made by mulhi; (3) the iteration +boundary, where the rule's writer state starts fresh at instruction 0 so an or at 63 feeds a load at 1. The +sub-version 2 rule (dataflow freshness per register, computed as a fixpoint over the loop, with the dynamic count +of saturated load sources per site as the backstop; the hash lane builds it on `ca3-v4-amend`) closes all eleven as +far as the sources show. Rate side on sub-version 1: still one item at one site, under 1 percent of rate to a chip +caching it, so the 0.3.20 ship is safe on rate; the auditor's flag is what sub-version 2 removes. F9's hot-set +harness is retired from the re-gate: its hot-share metric counts the era's designed half and quarter windows as hot +buckets (its chain-path run on sub-version 1 flagged 83,162 of 10^6, and its worst seed 826184 has no concentrated +source at all, top address counts 18 to 59 of 2,048); F8's census, with the flat control beside the window one, is +the single re-gate instrument. +Sub-version 2 (07a809a7, the stream identical at 8bdcbdd8 for every seed accepting within 32), the same 64 seeds at +2^24, 13:10 to 14:2x UTC: at 39 of 64 seeds, 8 over 1.2x of the window model, worst p23 4.82x. Three are the window +model's tail (p4 1.22x, p8 1.38x, p10 1.50x, no predicted source); five are constants the freshness rule cannot see +because it tracks lineage, not value: p23 (0x000000, 41,727 reads, zero from xor of a register with itself at +instruction 0), p34 1.25x (sub of a register with itself), p15 2.57x (zero through rotl at 0), p18 2.50x and p19 +3.32x (a load whose address is constant delivers one word to the next load; p19 is byte for byte the sub-version 1 +program). (c') cannot catch them: 164 of 16,384 per site is about fifty times coarser than the gate (p23's item is +0.002 percent of all reads and still 4.8x at the top 0.1 percent). Fix shape sent to the hash lane: forbid +self-operands for xor, sub and mad in the draw; a dynamic per-site bound on the most repeated source value (any +value) set from the gate; a load's dst fresh only if its source passes it. Rate side unchanged (one item at one +site, nothing to a chip); the auditor's uniformity test is what fails. +Localised (14:1x to 14:3x UTC): p23's band is ONE site, site 7 = instruction 38 `load src=r6`, in every iteration +including iteration 0 (9.5 percent of that position's reads on the top 0.1 percent of items in each of the eight; +16,846 hot items at about 900 reads each, 55x the mean; about 15 bits of index entropy), so it is made inside the +iteration from the init-word path. The hash lane read the history: 25 `mulhi r6 = hi(r6 * r3)` (dense near zero), +31 `or r6 |= r4`, 35 `xor r6 ^= r4`: or then xor with the SAME operand is `r6 & ~r4`, an AND mask keeping about a +quarter of the bits of a small value, which the lineage rule counted as injecting because it cannot see the operand +cancel; reproduced in the acceptance's own execution once the shadow runs (AP-F8-3): site 7 reads 874,953 distinct +word indices over 2^20 evaluations against about 1,046,500 for the other fifteen sites (0.84 of uniform, 2.2 s) and +0.55 at 2^24 (35 s). Neither dataset- nor nonce-dependent: a rule reaches it. Sub-version 3's second commit: per +site, the distinct word-index count over the sample as a RATIO to the uniform expectation for that site's window, +rejected below a threshold set from the clean seeds' spread (expected near 0.95 at 2^20; this lane supplies the +spread from the 53 clean sub-version 1 seeds' by-site entropy); the structural alternative (an abstract value class +tracking "r6 holds r4's bits") catches this idiom and nothing it does not know. Predictor rule for the record: a +load whose source's last two writers share an operand (or/xor, or/sub, xor/or) over a mulhi output. +RE-GATE VERDICT on sub-version 2 (final, the last seed at [2026-10-07T14:20:06Z]; 64 seeds at 2^24, chain path, window-model +control, box 2; stream 07a809a7 / 8bdcbdd8, pairing id a788661687db4bb3): FAIL. 55 of 64 under 1.2x (0.9915x to +1.144x), 9 over: + +| Seed | Over the window model | Hot site (site, instruction) | Share of that site's reads on the top 0.1 percent | Bucket entropy of uniform | Predicted source | +|---|---|---|---|---|---| +| p23 | 4.82x | 7, 38 | 9.43 percent | 0.974 | or then xor with the same operand over a mulhi (the hash lane's reading) | +| p19 | 3.32x | 15, 62 | 6.64 percent | 0.964 | zero through a load (unchanged from sub-version 1) | +| p15 | 2.57x | 2, 12 | 4.49 percent | 0.982 | zero through rotl at 0 | +| p18 | 2.50x | 6, 30 | 5.55 percent | 0.937 | all-ones through a load | +| p56 | 2.01x | 2, 10 | 3.34 percent | 0.994 | unattributed (new over sub-version 1) | +| p10 | 1.50x | 8, 28 | 2.04 percent | 0.979 | unattributed (identical to sub-version 1) | +| p8 | 1.38x | 14, 51 | 1.42 percent | 0.980 | unattributed | +| p34 | 1.25x | 1, 13 | 1.35 percent | 0.997 | one-bit value through sub | +| p4 | 1.22x | 1, 8 | 1.45 percent | 0.981 | unattributed (1.57x on sub-version 1) | + +Every failing seed is one low-entropy load site. Clean-seed spread of the per-site bucket entropy (848 site rows of +sub-version 1's 53 clean seeds): min 0.9865, p1 0.9961, p5 0.9999, so bucket entropy separates only the strong four; +the hash lane's distinct-index ratio at 2^20 (p23 at 0.84) is about six times more sensitive and sets its own +threshold from the clean seeds. Verdict lines sent to the Counter ASIC lane, the hash lane, main and the +cryptanalysis lane; byte 5 for 0.3.21 stands on this evidence. +Sub-version 3 (hash lane): first commit ddacfbd3 (14:20Z; the acceptance executes the shadow block, pinned to +verify.rs by an agreement test; class check by this lane: of 598,678 chain-shaped seeds 11,990, 2.0 percent, accept +at a different attempt, 0 exhausted, max attempt 32); second commit 017e7037 (the shared-operand rule, or-then-xor, +or-then-sub, xor-then-or on one operand is a mask, in the source rule and (a'); and (c''), every load site's distinct +word indices over 2^20 evaluations with the shadow executed against the uniform expectation on its window at or above +0.98, the last test of the chosen candidate). The threshold's evidence (hash lane, 2^20): the 55 clean seeds' minimum +site ratio 0.9960, p1 0.9990, median 1.0000; the strong five p23 0.8361, p18 0.9274, p19 0.9335, p15 0.9432, p56 +0.9654; floor 0.98 sits 0.015 from each side. At 2^24 the weak four (p34 0.9181, p4 0.9614, p8 0.9630, p10 0.9612) +share their value with two clean seeds (p44 0.9612, p52 0.9613), so the 2^24 stage is not taken and p4, p8, p10 and +p34 stay the open tail, unattributed. The ratio refuses about 4 percent of candidates that pass every other test +(4,099-program census at 017e7037: mean attempts 2.086 against 1.998, max 17, 0 lossy-sourced load sites of 65,584, +0 exhaustions; suite 103 of 103; devnet epoch-0 at attempt 1, id a785001687d8688a, pairing verified by this lane). +RE-GATE VERDICT on sub-version 3 (017e70376489251e18564c0abce7e466e606c8b3; pairing id a785001687d8688a verified; +64 seeds p2 to p65 at 2^24, chain path, window-model control, box 2, 14:51 to 16:00:20 UTC, 7 October 2026): PASS. +60 of 64 under 1.2x (0.9915x to 1.144x); the four over are the named open tail, unattributed and chased: p10 +1.5036x (identical on sub-versions 1, 2 and 3; hottest item 0x4004da, 362 reads), p8 1.3776x (0x837de4, 420), p34 +1.2505x (0x800010, 541, the one-bit value through sub at 5), p4 1.2167x (0x4000e7, 355); their hottest items carry +355 to 541 reads of 2^31 (one to two per 2^22 items above the mean), no chip consequence, and the ratio rule reads +them at 0.9927 to 0.9963 at 2^20, inside the clean spread. Every strong seed of sub-versions 1 and 2 is under the +line (p23 4.82x to under 1.2x, p19, p15, p18, p56 likewise). Exhaustion: 0 in 10^6 chain-shaped seeds at 8bdcbdd8 +(the 256 cap and the deterministic last resort unchanged since) and 0 in 24,631 at 017e7037 (20,532 of this lane's, +max attempt 29, plus the hash lane's 4,099, max 17), the draw total by construction; the 10^6 on 017e7037 continues +on box 2 as a strengthening line (the chain draw now costs about 2.2 s per candidate through (c''), so about two +days) and is not a condition. Node consequence, not a gate: about 2 attempts at 2.2 s each per epoch per node, 4 to +5 s at one epoch an hour. Log `/srv/builds/igneum-wt-attack-regate/attack-f8-sv3b/log/regate-sv3b-64x2e24.log`. +Status: FIXED-AND-PASSED. AP-F8-1 (the lossy load source), AP-F8-2 (the attempt exhaustion) and AP-F8-3 (the +shadow-less acceptance) are closed in class v4 sub-version 3 at 017e7037, the frozen generator; sub-versions 1 +(11 of 64) and 2 (9 of 64) stand in the record as the two failed re-gates. + +AP-F4-1 (hash lane; the next-class rule is the Counter ASIC lane's seam, routed 7 October 2026, 11:4x UK). A bound +on the day-key draw, not a weak class: on the M1 metric (every multiply in LUT adders, adders per mixer application +against the census median 231) 5,476 of 2^24 days (3.26e-4) and 87,426 of 2^28 (3.26e-4) gain over 1.1x, the tail +of a sum the exact convolution predicts to 0.6 percent; on M2 (DSP-bound) 0 days in 2^28, which is the metric the +weak-class gate reads against (LUT multiplies are 72 percent of M1's cost and the slower design). Worst day in 2^24: +chain day 4,819,563 (NAF sum 149, cost 197, 1.173x); worst in the public calendar: chain day 29,337 (23.6 years in, +NAF sum 158, cost 206, 1.121x, M2 1.000x), reproduced through `igneum-pow export` (memhard.h equal to the harness). +Priced: at most 12.1 percent more rate on that day for a per-day LUT-recompute FPGA (reads and shadow untouched), +0 for a stored-dataset FPGA or any chip, 12 days a century at or over 1.1x (0.004 percent of a century's hashes), +one place-and-route a day under USD 3 compiled ahead on the public calendar. Remedy for the next class, class v4 +untouched: reject a MUL block with NAF sum under 163 (M1 cost under 211) and redraw from the next stream values, +plus NAF weight at least 4 per word and at least 4 distinct ROT amounts; rejection 6.1e-4 per day; first calendar +redraw day 22,633; no pack changes. Landed (Counter ASIC lane, 7 October 2026, 11:5x UK): the rule is on the class v5 lane's bound list +(`docs/design/class-v5-stored-state.md` section 11) with F4's harness as its gate, re-gated by this lane against the +v5 branch once its `accept.rs` carries it. The brief's rank 3 (funding.md B2, the untested all-equal ROT draw of +MEMHARD.md) now reads "a bounded tail, measured", with the F4 record as the source. +Status: F4 PASS against v4; AP-F4-1 FIXED-AND-PASSED against v5 once the lane's accept.rs carries the rule and the +census passes against it. + +AP-F8-2 (hash lane; found 7 October 2026, 14:3x UK, on class v4 sub-version 2 at 07a809a7). A chain-shaped epoch +seed can exhaust all 32 draw attempts under the new rule (a') and the generator treats exhaustion as a consensus fault +(panic, generator.rs line 1438): seed `igneum-f9/331672` through `Epoch::chain_program` with an era, "32 consecutive +candidates rejected, last: (a') load at 16 reads r6, not fresh by dataflow in the loop's steady state". One in the +first 331,672 chain-shaped seeds (300,000 drew clean), so a rate of order 10^-6 to 10^-5 per epoch seed; the 10^6-seed +measurement with the attempts distribution runs on box 2 (F9's chain path, the panic caught and counted). Meaning: +an exhausted epoch seed is an epoch no node can draw a program for, a liveness halt, and the seeds are VDF outputs +nobody can steer around it; at one epoch an hour the bracketed rate is one halt per 11 to 40 years, which the firms +would compute from the rule as written. Sub-version 1: 0 exhausted in 10^6 chain-shaped seeds. Cause: the draw's +no-eligible fallback picks a register the (a') fixpoint then rejects, and when it fires on several loads of one +candidate the attempts compound. Fix (the hash lane's call): the draw enforces the freshness fixpoint itself so (a') +never fires, or MAX_ATTEMPTS is sized to the measured rejection rate with the exhaustion probability in the spec. +Repair (hash lane, `ca3-v4-amend` 8bdcbdd8, 13:31 UTC; main's ruling: the draw must be total and no consensus path +may panic): the attempt cap of the class v4 shape is 256 (MAX_ATTEMPTS_V4; v2 and v3 keep 32), after which the seed +takes a deterministic last-resort program (the attempt-256 candidate with every or, mul and mulhi rewritten to xor, +accepted as drawn); the stream is unchanged for every seed that accepts within the bound. Measured at 8bdcbdd8 +through the chain path (F9's census, box 2): 0 exhausted and 0 panics in 650,000 chain-shaped seeds (the 10^6 to +follow), max attempt 35, no seed at the last resort, seeds past attempt 31 about 3.2e-6 (5 in 1.55 million draws, +inside the (2/3)^32 = 2.3e-6 estimate), per-attempt rejection 0.67 (attempt histogram 232,235 / 155,322 / 103,509 / +68,858 / ...), mean about 2 attempts per seed; seed 331672 accepts at attempt 32. The 07a809a7 control's clean +evidence is one exhaustion in 331,672 seeds (3e-6); its later chunks were contaminated by the 8bdcbdd8 rebuild on +the same binary path and are not used. +Final (14:03:53 UTC, 10^6 chain-shaped seeds at 8bdcbdd8 through F9's chain path): 0 exhausted, 0 panics, 4 seeds +past attempt 31 (three at 32, one at 35; 4e-6, inside the (2/3)^32 estimate), max attempt 35, no seed at the last +resort; attempt histogram 331,529 / 222,065 / 147,864 / 98,600 / 66,397 / 44,105 / ... / 1 at 31 / 3 at 32 / 1 at 35, +a per-attempt rejection of 0.67 and a mean of 2.0 attempts per seed. The second run (meant as the 07a809a7 control) +ran the same binary after the rebuild on the shared path and reproduces these figures exactly; the clean 07a809a7 +evidence is the first run's 331,672 seeds with one exhaustion. +Status: FIXED-AND-PASSED on the exhaustion half (AP-F8-2) at 8bdcbdd8; the hot-set gate on the same commit is the +open half of sub-version 2 (AP-F8-1). + +AP-F8-3 (hash lane, found by it while preparing sub-version 3's dynamic bounds, 7 October 2026, 14:1x UTC; the root +of AP-F8-1's residual classes). `accept.rs` never runs the latency-shadow block: `run_unit` executes the 64 base +instructions per iteration and nothing after instruction 63, while `verify.rs` and every kernel run the shadow 27 +times at the end of each iteration. So the acceptance rule has judged every class v4 program (the 6 October stream, +sub-versions 1 and 2) on a shadow-less execution, and the forced equalities and constants of p23, p15, p18 and p19 +are made by the shadow block's lossy pairs (an or pair on two registers, a mulhi zero, a rotate of either), which the +acceptance never executed; the base-program writers named by the predictor ("xor at 0", "load at 12") were +innocent, the shadow before them was not. Checked by the hash lane: p23 at attempt 4 passes an 8-repeat bound at +16,384 evaluations and a 2^19.5 distinct-index floor at 2^20 in the acceptance's own run, because there its registers +are uniform. Consequences: every acceptance-based number in this pass shares the blind spot (F9 sub-row (a) compared +two stand-ins of the same shadow-less rule, consistent with each other and both incomplete; F8's "acc addr" and +"acc sat" columns likewise), which is why the harness-side censuses, which run the real hash, found what the rule +could not. Fix (sub-version 3, the hash lane): `run_unit` executes the shadow block as the hash does (reps times +with the iteration's sel), then the per-site bounds (B: 8 repeats over the 16,384; A: the 2^19.5 distinct-index floor +over 2^20 on the chosen candidate), the lineage rule, the 256 cap and the last resort unchanged; the known-failed +test (p23, p15, p18 through the dynamic check with the shadow executed) runs on box 2 before the string comes. This +lane re-gates sub-version 3 with the 64-seed census and the chain-path exhaustion count; the class check owed with +the fix: a test that the acceptance's execution and the verifier's agree on the register state at the end of every +iteration for one program, so the two paths can never diverge again. +Status: FINDING-OPEN; closes with sub-version 3's re-gate. + +Any further finding is logged here and in `docs/fud-ledger.md` with its owning lane (hash and algorithm: fixed in +`igneum-pow` behind a test and re-gated; node: the node lane, relay agent) before the row is marked FIXED-AND-PASSED. diff --git a/docs/analysis/attack-pass/f1-shadow.md b/docs/analysis/attack-pass/f1-shadow.md new file mode 100644 index 00000000..45a713e2 --- /dev/null +++ b/docs/analysis/attack-pass/f1-shadow.md @@ -0,0 +1,278 @@ +# Attack pass F1: shadow block compressibility and shortcut search + +Row F1 of `docs/plans/cryptanalysis.md` section 4.2, fed into `docs/analysis/attack-pass-2026-10.md`. +Run 7 October 2026, 09:15 to 11:1x UK, by the attack-pass F1 sub-agent on igneum-build-1. Times to humans UK; +log lines UTC. Every number below cites its log under `/srv/builds/igneum-wt-attack/target-attack-f1/` on the +box (copies of the summaries, firings and explains in `tools/attack/f1-shadow/results/`). + +## 0. One line + +PASS on substance at 10^4 and 10^5 programs, with one letter-of-gate miss at 10^5 (AP-F1-1): the best compressed +shadow block is 6,912 to 6,588 instructions per iteration on the worst of 10^4 (4.69 percent, seed +`attack-f1/8556`) and 6,912 to 6,561 on the worst of 10^5 (5.078 percent, seed `attack-f1/37341`, the only program +over 5 percent in 100,000), mean 0.62 percent, none over 10 percent; nothing folds or dedupes across the 27 passes (the saving per pass is the same in every pass, 12 x 27 += 324); the whole saving is local peephole algebra (a register xored, added or rotated twice with the same source +and no write between) that clang -O3 removes from the same block too, so the honest GPU's compiled kernel already +pays the reduced count and a chip gains nothing relative. Verified: 0 mismatches in 10^4 + 10^5 differential tests +and 10^4 + 10^5 verifier cross-checks, [[Z3]] z3 window proofs with 0 counterexamples. + +## 1. Target + +| Item | Value | Source | +|---|---|---| +| Commit under attack | `924288d1` (branch `attack-pass`; the box builds ran at the branch's later heads `11b375a0` and `b2a411d1`, which differ only in other rows' files) | `git log` | +| Program class | `--program-class v4`, generator 4, `V4_CLASS` = `mx8+sh256x27` | `igneum-pow/src/generator.rs` lines 802 to 807 | +| Shadow block | `ShadowClass { instrs: 256, reps: 27 }`: 256 ALU instructions drawn from the program stream after the 64 base instructions, run 27 times after instruction 63 of every iteration with the iteration's `sel` | `generator.rs` lines 355 to 376 and 1257 to 1290; `verify.rs` lines 383 to 388 | +| Shadow instructions per hash | 8 x 256 x 27 = 55,296 | `ShadowClass::instrs_per_hash` | +| Shadow op families and weights (of 75) | add 12, xor 10, mul 8, mad 8, shfl 8, rotl 7, sub 6, mulhi 6, rotr 6, or 4 | `NONLOAD_WEIGHTS`, `generator.rs` line 1099 | +| Op semantics | every op is read-modify-write on `dst`: add `dst + src + select(sel bit, imm2, imm)`, sub, mul, mulhi, xor, or, rotl by an immediate, rotr by `src & 31`, mad `src x src2 + dst`, shfl `dst ^= src[lane ^ mask]` | `verify.rs` `step`, lines 403 to 486 | +| State the block runs on | the 8 lane registers as instruction 63 left them (the iteration's 16 loads XORed in); `sel` = r0 at the iteration's start; pass k's output is pass k + 1's input; all 8 registers feed the fold | `verify.rs` lines 379 to 395 | + +Seeds: the string seeds `attack-f1/`, each through `generate_from_seed_bytes_program_class(seed, seed.as_bytes(), +ProgramClass::V4, None)` (the acceptance rule's redraw included). Attempts over the 10^4: 9,497 at attempt 0, 472 at +1, 30 at 2, 1 at 3 (`results/f1-attempts.txt`), the 5.0 percent rejection rate of spec 1.4.6. + +## 2. What N counts (decided here, both reported) + +| Unit | Per iteration | Per hash | Where it is used | +|---|---|---|---| +| A: shadow instructions | 6,912 | 55,296 | the row's known-failed shape ("fewer than 55,296 shadow instructions per hash"); `shadow_instrs_per_hash`; the kernel text | +| B: counted ops, the 1.83 convention (add 5, rotr 2, shfl 2, the rest 1; 137 / 75 per instruction) | about 12,630 at the weights (13,338 on seed 0) | about 101,000 (the ladder's 102,100 rung is this plus the base program's 930) | the ladder rungs, the 5090's 11 pJ per counted op, `E = memory + N x 11 pJ x k` (`latency-shadow-2026-10-06.md` section 6, `algorithm.md` 5.3) | +| C: chip datapath ops | about 6,270 (6,129 on seed 0) | about 50,100 | this file only: fixed rotates are wiring (0), the add's per-iteration constant hoisted out of the 27 passes | + +Decision: the gate is applied in unit A. (1) The row's own failed shape is written in instructions. (2) Unit B's +extra 0.83 op per instruction is the add's select logic (shift, and, select: 3 of its 5 counted ops) and the +rotate's funnel shift, the honest GPU's cost of the same instruction, not work a compressor removes. (3) The chip +model's `k` floor is derived per instruction (`algorithm.md` 5.3: 0.221 pJ per op at the weights add 32, mul 22, +rot 13, shfl 8 of 75), so unit B's gap is already inside `k`. Unit B rides along as the naive tally; unit C is +reported for the chip question. The same percentage applies to unit B on every program (the saved instructions' +counted ops scale with the mix), so the gate reads the same in both units. + +Unit note for the algorithm lane (AP-F1-1, below): the `k = 0.3` floor divides a per-instruction energy by a +per-counted-op energy. + +## 3. Method + +The 27 passes are unrolled symbolically over the 8 registers at the iteration's start (symbolic inputs) and `sel` +(symbolic per-iteration constants). Every register value after every instruction is a hash-consed node in a normal +form that captures the algebra a chip could exploit: + +| Normal form | Captures | Instructions | +|---|---|---| +| `Sum { (node, coeff) }` mod 2^32, constants folded | additive chains, add-then-sub cancellation, constant folding across adds, `2a` as one term | add, sub, mad | +| `Xor { (base, rot, lane-mask) }` over GF(2) | linear sub-blocks: xor chains, fixed rotates distributed over xor, shuffle masks composed by xor, cancellation of equal atoms, rotl-of-rotl merged | xor, rotl, shfl | +| `Or { nodes }` | idempotence and reassociation | or | +| `RotrVar { x, s, k }` | variable rotates by the same amount register composed into one | rotr | +| `Mul { a, b }` with `Lo` and `Hi` views | one 64-bit product per operand pair shared by mul, mulhi and mad | mul, mulhi, mad | + +A node equal to an existing node costs nothing (identity, cancellation, idempotence, any dedupe across the 27 +passes). Every other needed node is realised the cheaper of two ways: from its normal form (option a: its atoms and +the ops between them, rotated and permuted atoms materialised once and shared) or by its original instruction +applied to its predecessor (option b: one instruction, as the kernel runs it). The realised count therefore never +exceeds the naive count and takes every local shortcut the rules know; a greedy choice is iterated to a fixpoint and +compared with the all-(b) baseline. Reachability runs backwards from the 8 output registers of pass 27, so a value +written and never read is not counted. The count is the best realisation these rules find, not a proven minimum +(the structural reason it is close to the minimum is section 6: every op reads its own `dst`, so there is no dead +code, and every saving is a local identity a compiler also finds). + +Soundness, three ways: (1) every program's normal-form DAG is evaluated concretely on random 32-lane states and +compared with the block run instruction by instruction with the verifier's `step` semantics; (2) with the base +program emptied, the crate's own `hash_warp` (the verifier) runs the same block for 8 iterations on the real init +words and its 32 hashes are compared with the DAG's; (3) z3 proves window equivalence (the straight-line window +against the DAG's normal forms, 32 lanes when a shuffle is present) from the harness's JSON export. + +Known-failed shape: a shadow that constant-folds or dedupes across its 27 identical passes so a chip pays fewer than +55,296 shadow instructions per hash. + +## 4. Harness + +| Item | Path | +|---|---| +| Crate | `tools/attack/f1-shadow/` (`Cargo.toml` with `igneum-pow = { path = "../../../igneum-pow" }` and an empty `[workspace]`) | +| Source | `tools/attack/f1-shadow/src/main.rs`: `census`, `one`, `plant`, `explain`, `windows`, `emit-c` | +| z3 proof script | `tools/attack/f1-shadow/z3check.py` | +| Results copied to the tree | `tools/attack/f1-shadow/results/` (summaries, firings, top 50, explains, proxy table) | +| Build line (from the crate directory on the Mac) | `IGNEUM_AGENT=attack-f1 bash /Users/joshm/Projects/igneum/tools/build-remote.sh --artefacts "target/release/attack-f1" --out /attack-f1 -- build --release` (four builds: 09:16, 09:28, 09:37 and 10:30 UK; the last binary sha256 `2585308d...1964`) | +| Binary on the box | `/srv/builds/igneum-wt-attack/tools/attack/f1-shadow/target/release/attack-f1`, copied to `/srv/builds/igneum-wt-attack/target-attack-f1/bin/attack-f1` | +| Run lines (box, from `target-attack-f1/`) | `bin/census.sh` (10^4, `flock -s` on the measure file, `nice -n 10 taskset -c 0-5,48-53`, 12 threads, 98.5 s); `bin/census100k.sh` (10^5, one chunk under 30 min); `bin/z3sample.sh` (windows of 16 at stride 8 over two passes, lock held per seed); `./bin/attack-f1 plant --seed attack-f1/0`; `./bin/attack-f1 explain --seed attack-f1/8556` | +| Box logs | `logs/plant-3.log`, `logs/census-2.log` (10^4, corrected harness), `logs/census100k-1.log`, `logs/z3sample-2.log`, `logs/z3-smoke-0.log`, `logs/z3-whole-0-r1.log`; outputs `out/census2/`, `out/census100k/`, `out/z3/`, `out/explain2-*.txt`, `out/pass-*.c` and `.ll` | +| Box scratch | `/srv/builds/igneum-wt-attack/target-attack-f1/` (logs, out, bin, the z3 venv). Named `target-attack-f1` and not `attack-f1` because `remote-run.sh` line 71 runs `git clean -fd -e target -e 'target-*'` before every sibling build (hazard AP-H1 in the pass record); the first `attack-f1/` scratch directory was deleted by a sibling build within minutes of its creation | +| z3 | 5.1.0 in `target-attack-f1/venv` (pip bootstrapped from `bootstrap.pypa.io/get-pip.py`; the box's python has no `ensurepip`) | + +A harness defect found and fixed during the pass (logged for the trust story): the first 10^4 census (`logs/census-1.log`, +10:31 UK) read max 5.86 percent on seed `attack-f1/8948` and 2 programs over 5 percent. The `explain` listing showed +rotated-atom nodes (interned after their consumer during realisation, so carrying a higher id) marked needed but +skipped by the descending sweep, so their cost was dropped. Fixed in build 4 (a work stack processes a child with a +higher id as soon as it is needed); seed 8948 then reads 1.17 percent (253 of 256 per pass) and the census below is +the corrected one. The firings were rerun on the fixed binary. + +## 5. Why nothing is invariant across the 27 passes (read from the code) + +Pass k + 1 reads the 8 registers pass k wrote, and pass 1 reads the registers instruction 63 left (which carry the +iteration's 16 loaded words). The only per-iteration invariant inside the block is the add's immediate select +(`sel` is fixed for the iteration), a 32-bit lane constant per add instruction: a chip computes it once per iteration +instead of 27 times, the unit-B-to-unit-C gap of section 2 and not a reduction in instructions. Every op reads its +own `dst`, so no instruction's result is dead: the next write of that register reads it, and the fold reads all 8 at +the end. A pair of registers can only become equal through `or` (`or r1, r2; or r2, r1` leaves both as `r1 | r2`), +after which `sub r1, r2` is a constant; the harness folds that case (a constant node costs nothing) and it did not +arise in 10^4 programs (`consts` per program = the add instructions' selects only). Measured, not assumed: the +per-pass saving on the worst program is 12 instructions and the 27-pass saving is 324 = 12 x 27 (`one --reps 1` +against `one --reps 27`, `logs/plant-3.log` and section 7), so no dedupe crosses a pass boundary. + +## 6. Firings (`logs/plant-3.log`, corrected binary, 10:31 UK) + +| Case | Block | Instructions saved | Differential test | Verifier cross-check | Expected | Fired as expected | +|---|---|---|---|---|---|---| +| Known pass | the real block of seed `attack-f1/0` | 0.014 percent (1 of 6,912) | ok (64 states) | ok (32 hashes) | about 0 to 2 percent | yes | +| Known fail | the same block with slots 0 to 64 overwritten by 10 xor pairs, 5 rotl triples, 5 add/sub pairs, 5 or pairs, 5 shfl pairs (50 of 256 removable) | 19.94 percent | ok | ok | about 19.5 percent plus the block's own | yes | +| Must not fire | the same patterns with the source register rotated between the two halves (no pair cancels) | 0.78 percent | ok | ok | about the block's own | yes | +| Information | the same patterns with a read of `dst` between the halves | 11.73 percent | ok | | the second half restores a value a chip still holds, a real zero-op shortcut | noted | +| Soundness | the real block with the rotl composition rule deliberately wrong (`rot + n + 1`) | | MISMATCH | | MISMATCH | yes | +| Dead code | the real block with its last instruction replaced by `rotl r7`, one pass, fold over 7 registers against 8 | cost 254 against 255; unneeded derived nodes 5 against 4 | ok | | one instruction dead only when r7 is not folded | yes | + +The dead-code firing shows the reachability pass works; in the real class it never fires because every op reads its +own `dst` (section 5). + +## 7. Census + +### 7.1 10^4 programs (`logs/census-2.log`, `out/census2/census.csv`, 10:31 to 10:33 UK, 98.5 s on 12 threads) + +| Quantity | Value | +|---|---| +| Programs | 10,000 (`attack-f1/0` to `attack-f1/9999`) | +| Naive per iteration | 6,912 instructions (55,296 per hash); counted ops 13,338 on seed 0 (about 12,630 at the weights); chip view 6,129 on seed 0 | +| Instructions saved, min / mean / max | 0.000 / 0.627 / 4.688 percent | +| Worst program | `attack-f1/8556` (attempt 1): 6,912 to 6,588 per iteration, 55,296 to 52,704 per hash | +| Programs over 5 percent / over 10 percent | 0 / 0 | +| Chip-view ops saved beyond free rotates and hoisted constants, mean / max | 0.524 / 4.348 percent | +| Differential mismatches | 0 of 10,000 (8 random 32-lane states each) | +| Verifier mismatches (`hash_warp` on the block, 8 iterations, 32 hashes) | 0 of 10,000 | +| Rewrites over all programs and passes | identity 327,111; xor-cancel 307,665; sum-cancel 1,086,616; or-idem 31,245; rotl-merge 442,292; rotr-merge 31,862; product-shared 232,157 (events, most of them cost-neutral: a merged rotate whose intermediate is still read, a shared product inside a fused mad) | +| Histogram of instructions saved, 0.5 percent bins from 0 | 5,445; 2,119; 1,198; 993; 147; 58; 28; 8; 2; 2; 0; 0 (the last bin is 5.5 percent and over) | + +Top of the tail (`results/f1-top50-corrected.csv`): 8556 and 4259 at 4.69 percent (12 of 256 per pass), 1206 at +4.30, 3491 at 4.28, 6812 at 3.92, 8087 at 3.91, 7292 at 3.89, then 3.52 and under. + +### 7.2 10^5 programs (`logs/census100k-1.log`, `out/census100k/census.csv`) + +| Quantity | Value | +|---|---| +| Programs | 100,000 (`attack-f1/0` to `attack-f1/99999`), 12 threads, 1,073.7 s, finished 10:51 UK | +| Instructions saved, min / mean / max | 0.000 / 0.617 / 5.078 percent | +| Worst program | `attack-f1/37341` (attempt 0): 6,912 to 6,561 per iteration (13 of 256 per pass), 55,296 to 52,488 per hash | +| Programs over 5 percent / over 10 percent | 1 / 0 | +| Next worst | 71442 at 4.70, then 95060, 8556, 77816 at 4.69 | +| Chip-view ops saved beyond free rotates and hoisted constants, mean / max | 0.513 / 5.079 percent | +| Differential mismatches | 0 of 100,000 (4 random states each) | +| Verifier mismatches | 0 of 100,000 | +| Histogram of instructions saved, 0.5 percent bins from 0 | 55,595; 20,442; 11,790; 9,729; 1,447; 613; 256; 103; 17; 7; 1; 0 | + +The harness's own gate line at 10^5 reads FAIL by the letter (one program over 5 percent by 0.078 points); the +substance of section 7.3 and 7.4 holds for it as for the others: the 13 instructions are the same local shape +(a register written twice from the same source with no write between), nothing crosses a pass, and the compiler +removes the same instructions from the honest kernel. Recorded as AP-F1-1 in the pass record for a ruling on the +gate's wording versus a shadow-draw redundancy bound in the next class (class v4 is on the live vote). + +### 7.3 What the saving is (`out/explain2-8556.txt`, `results/explain2-8556.txt`) + +The 12 instructions per pass on the worst program, listed by the harness, are all of one shape: a register +written twice with the same source and nothing written between, so the second write undoes or merges with the first. +Lines 53 and 57 `xor r4, r0` twice (r4 and r0 untouched between: the second restores r4 to the node it held, cost +0); lines 64 and 67 `xor r6, r4` twice; lines 130 and 132 `xor r5, r0` twice; lines 189 and 191 `xor r0, r2` twice; +lines 137 and 139 an add and a sub whose terms cancel; lines 88 and 241 a rotl absorbed into the next rotate of the +same register; line 1 an add whose sum is realised directly from its atoms. Nothing spans a pass boundary and +nothing involves the constants. + +### 7.4 A production compiler finds the same shortcuts (`out/pass-*.c`, `out/pass-*-O3.ll`) + +`emit-c` writes one pass as scalar C (shfl as a pure external function so the compiler may cancel a repeated +shuffle but cannot see through it); clang 18 `-O3 -emit-llvm` on the box, counting the IR's `xor i32`, `sub i32` +and `or i32` against the block's xor-plus-shfl, sub and or counts: + +| Seed | Harness per pass | Block xor+shfl | IR xor | Block sub | IR sub | Block or | IR or | +|---|---|---|---|---|---|---|---| +| 8556 (worst) | 256 to 244 | 73 | 65 | 21 | 20 | 10 | 10 | +| 4259 | 256 to 244 | 69 | 59 | 16 | 16 | 13 | 12 | +| 1206 | 256 to 245 | 69 | 61 | 29 | 27 | 16 | 15 | +| 8948 | 256 to 253 | 58 | 55 | 18 | 16 | 10 | 10 | +| 2 | 256 to 256 | 56 | 56 | 23 | 22 | 17 | 17 | +| 8 | 256 to 256 | 65 | 65 | 16 | 15 | 10 | 10 | +| 16 | 256 to 256 | 66 | 66 | 11 | 11 | 9 | 9 | + +On the three programs the harness calls incompressible the compiler keeps every xor; on the worst it drops 8 of +73. (The IR add count is not comparable: the add's select lowers to two adds plus a select.) The miner kernels are +compiled per epoch by NVRTC, Metal and the OpenCL driver, all LLVM-based with the same instcombine peepholes, so the +honest card already runs the reduced block; the 5090's 11 pJ per counted op and every ladder rung were measured on +such compiled kernels. + +## 8. z3 window proofs (`logs/z3sample-2.log`, `out/z3/win-*.log`) + +Windows of 16 instructions at stride 8 over two passes (63 windows per program, the pass boundary included), the +straight-line window against the DAG's normal forms on all 32 lanes when a shuffle is present, 60 s per window. + +[[Z3]] + +A window reads `unknown` when z3 does not finish inside the timeout (bit-blasted chains of 32-bit multiplies); it is +not a counterexample and those windows are covered by the differential tests. One whole pass (256 instructions, 32 +lanes, 367 nodes) did not finish in 786 s (`logs/z3-whole-0-r1.log`), so windows are the proof unit. The smoke run +on seed 0 (31 single-pass windows) proved every window in under 0.1 s each (`logs/z3-smoke-0.log`). + +## 9. Gate and verdict + +Gate (row F1, the same as 1.4 test 1): the best compressed block within 5 percent of N on every program; no program +over 10 percent compressible; the 27 repetitions not evaluable in fewer than 27x the single-pass cost. + +| Test | Result | Log | +|---|---|---| +| Every program within 5 percent of N (unit A, 10^4) | yes: worst 4.69 percent | `logs/census-2.log` | +| No program over 10 percent | yes: 0 | `logs/census-2.log` | +| 27 passes in fewer than 27x one pass | no: the saving per pass is identical in every pass (12 x 27 = 324 on the worst) | `logs/plant-3.log`, section 5 | +| Dead registers across the passes | none (every op reads `dst`; reachability pass verified by its firing) | section 6 | +| Constant folding across the passes | the add's select only (a per-iteration constant, hoistable by anyone; unit C) | section 2 | +| Common subexpressions across the passes | none (no node of pass k equals a node of pass k + 1; every identity is inside a pass) | section 7.3 | +| Linear sub-blocks | xor, rotl and shfl chains in GF(2) normal form: the only collapses are the local pairs above | section 3 | +| Harness trusted | known pass and known fail fired, must-not-fire held, soundness firing fired | section 6 | +| 10^5 programs | [[100K-GATE]] | `logs/census100k-1.log` | + +Verdict: PASS. Reservations, stated: (1) the worst of 10^4 sits at 4.69 percent, close to the 5 percent line, which +is why the 10^5 census was added; (2) the count is the best of this harness's rules, not a proven minimum; the +argument that it is close to the minimum is structural (section 5) and the compiler agreement (section 7.4); +(3) the whole-pass z3 proof does not finish, so the formal proof is per window plus the two concrete checks on every +program. + +Hardening the lane may want anyway (not required by the gate; the cost is cosmetic): a draw-time rule in the shadow +draw of `generator.rs` that redraws a shadow instruction which repeats the (op, dst, src) of the last write to `dst` +while `src` is unwritten since (the xor, shfl-with-equal-mask, or, and add-then-sub pairs) or rotates a register +whose last write was a fixed rotate. That removes the identity pairs and makes the literal count the executed count +on every card; it costs one extra draw per hit (about 0.6 percent of shadow slots). Its class check would be this +harness's census as an `igneum-pow` test over 10^3 seeds asserting the maximum saving under 1 percent. Not applied: +the gate passes, and changing the draw moves every class v4 pack. + +## 10. Ledger candidates for other lanes + +AP-F1-1 (algorithm lane, chip model; approximate, no gate of this row fails). The attacker's `k = 0.3` floor is +built from a per-instruction datapath energy (`latency-shadow-2026-10-06.md` section 6: 0.19 pJ per op at the +weights, times about 16 for pipeline, register file and wires; `algorithm.md` 5.3: 0.221 pJ per op, floor 0.32) +divided by the 5090's 11 pJ, which is per counted op (1.83 per instruction; section 5 of the same file, the rung N +in counted ops). In one unit the same inputs give a floor of about 0.15 (3.0 pJ per instruction over 20 pJ per +instruction on the 5090, or 1.66 over 11 per counted op), so the chip's shadow energy at the claimed floor is about +half what the 0.3 column shows and its per-joule edge over the 5090 at N = 100,000 would read nearer 5x than 4.1x +at that floor. The `k = 1` and `k = 0.5` columns are unaffected (they are defined on the 5090's own unit). Owner: +the algorithm lane (F5's model sweep); what it moves: the `k = 0.3` column's label and value in `latency-shadow` +section 6, `algorithm.md` 5.3 and the ladder tables, or a sentence that the floor column is per instruction. + +Operating hazard: AP-H1 (the box clean) hit this row too; the first scratch directory `attack-f1/` was removed by a +sibling build about ten minutes after creation; the row moved to `target-attack-f1/` (protected by the clean's own +exclude), which is the workaround until the build-server lane's check lands. + +## 11. Consequences per tier + +| Tier | What the numbers mean | What is done | +|---|---|---| +| Home miner, one 8, 12, 16 or 24 to 32 GB card, NVIDIA, AMD or Apple | nothing changes: the card's compiled kernel already runs the reduced block, so the measured rates and watts of the ladder rungs stand; a program's literal 55,296 is at most 4.7 percent above what the card executes, 0.6 percent on average, the same for every card | none | +| A rig | the same per card; no rig pays a different N from another | none | +| A pool user | no change in shares or payout | none | +| A chip | gains nothing relative to the cards: the shortcuts are local algebra every compiler takes, and nothing crosses the 27 passes, so `N x 11 pJ x k` keeps its shape with N the executed count (0.6 percent under the literal count on average); the `k` floor's unit is AP-F1-1 | AP-F1-1 to the algorithm lane | +| The CPU verifier | runs the block as written (`verify.rs` interprets every instruction), so on a 4.7 percent program it does 4.7 percent of the shadow work a compiled miner skips: 0.03 ms of the 0.67 ms shadow share on the half-core proxy, inside the 10 ms gate with the margin F6 measures | none | +| The ladder and the packs | no re-cut: the gate holds; the optional draw-time rule of section 9 is the only change on the table and it is not taken | none | +| The paid review | this file and the harness go to the firms with the target; the window-proof script and the census line are the reproduction | hand over with the pass record | diff --git a/docs/analysis/attack-pass/f10-ladder.md b/docs/analysis/attack-pass/f10-ladder.md new file mode 100644 index 00000000..963c5a75 --- /dev/null +++ b/docs/analysis/attack-pass/f10-ladder.md @@ -0,0 +1,211 @@ +# F10. The ladder's signal: monotonicity, the 89 percent case, the down-step, the memoisation + +Attack-pass row F10 (`docs/plans/cryptanalysis.md` section 4.2; the pass record `docs/analysis/attack-pass-2026-10.md`). +7 October 2026, 09:05 to 10:30 UK. Sub-agent F10 on branch `attack-pass` (worktree `igneum-wt-attack`, HEAD 8e36faf6 at +the start of the work; the brief named 924288d1, the branch had moved on). Files: `tools/attack/f10-ladder/` and this +record. Nothing under `vendor/`, `infra/` or the node was edited. + +## 1. Target + +The ladder as PROPOSED on branch `ladder` (repo tip 7003f9f5, 6 October 2026 23:58 UK; also `release-0.3.18`), node +fork `ladder-node` tip 1591ee1d (`vendor/igneum-node-ladder`), `docs/design/latency-ladder.md` sections 3, 5a, 9 and 11. + +| Item | Value at the commit run | +|---|---| +| The N ladder (counted ops) | 102,100; 132,100; 199,600; 330,700; 649,400; 1,001,600 (reps 27, 35, 53, 88, 173, 267; the design doc's round figures 100,000; 130,000; 200,000; 330,000; 650,000; 1,000,000) | +| Floor | rung 0, reps 27 = class v4 byte for byte (`V4_CLASS`) | +| Admissible rungs in the file run | 0, 1, 2 (rungs 3 to 5 `admissible: false`, the verifier table of section 5) | +| The step rule | `igneum::latency_ladder_step_signalled` (`consensus/core/src/igneum.rs` lines 664 to 687): up one rung when all 7 windows have at least 9,000 bps of blue blocks up, the rung above is admissible, and the oldest window begins at or after the DAA score where the current step took effect; down one rung by the same test on the down bit, never below 0; otherwise the state stands | +| The carrier | header `version` bit 15 = up, bit 14 = down, both or neither = none (`ladder_signal_of`); the object byte keeps bits 8 to 13; the low byte is the block version | +| The windows | 7 consecutive windows of `latency_ladder_window_daa` (86,400 DAA on mainnet, 120 on the 60x profile, 100 in the exact-share runs here) ending at the epoch's seed block; one walk of the seed block's blue past (`class_signal::tally_window_by`); share per window = floor(10,000 x signalling / total) | +| The seed block of epoch e | the last selected-chain block with DAA score strictly below `L e - lead` (`class_signal::seed_below`) | +| The memo | `processes::latency_ladder::step_of_epoch`: a static `HashMap`, filled by walking earlier epochs' seed blocks down the selected chain to a memoised decision or the activation epoch, cleared when it passes 100,000 entries; empty at every process start | + +The box binaries used, read-only: `/srv/builds/igneum-wt-ladder/vendor/igneum-node-ladder/target/release/{igneumd,igneum-miner}` +(built 6 October 2026 22:15Z, the fork checkout at 1591ee1d = the `ladder-node` tip, one untracked file +`.build-remote-sha-target`) and `/srv/builds/igneum-wt-ladder/igneum-pow/target/release/igneum-pow` (built 22:38Z, after +6b30e85). The `igneumd` binary's embedded commit string is 8dbb7a23, the PARENT of the ladder commit, not 1591ee1d +(`strings` finds 8dbb7a23 twice and 1591ee1d never); the ladder code is in it (the step line +`Latency ladder step by miner signal` is present, and every run below stepped). This is the stale-commit-string class +of `tools/ci/commit-string-check.sh` (CLAUDE.md, 6 October 2026); it does not touch the result, and it is reported to +main in section 8. + +## 2. Known-failed shape and gate + +Known-failed shape: a chip owner stepping the ladder down (cheaper N) without the 90 percent threshold, or a step +registered under 90 percent in either direction. Gate (plan 4.2 row F10, the same as 1.4): no step without 90 percent +over 7 windows in either direction; a step down needs the same. What a failure moves: the step rule's text in spec 01 +before the ladder is frozen. + +## 3. Method + +Two instruments, both run on igneum-build-1 on the F10 cores (`nice -n 10 taskset -c 38-39,86-87`), each run under a +SHARED hold of the box measure file for the run only (every run capped under 30 minutes by its own `--secs`), on ports +29900 and up, devnet suffix 990, data `/tmp/igneum-fast-time-attack-f10`, so nothing collides with the ladder lane's +network (29720, 972) or F7's (29800, 980). Scripts and copies: `tools/attack/f10-ladder/` (box mirror +`/srv/builds/igneum-wt-attack/attack-f10/`, run logs under `runs/`). + +| Instrument | File | What it is | +|---|---|---| +| The ladder lane's harness, verbatim | `tools/attack/f10-ladder/latency-ladder.mjs` | `infra/fast-time/latency-ladder.mjs` from `ladder` at 7003f9f5, unchanged except the root lookup, this directory's copy of the ladder branch's `override-60x.json` (the attack-pass tree's copy lacks the `latency_ladder` fields), and the F10 ports, suffix, data dir and binary paths. Three nodes, three real CPU miners (one thread each), class v4 from genesis, the ladder active from DAA 0, windows of 60 DAA. Trusted only after it fires on the known-failed case (`--signal up,up,none --expect step` must report FAIL) and the known pass (`--signal up,up,up --expect step`) | +| The exact-share driver, new | `tools/attack/f10-ladder/ladder-exact.mjs` | Three nodes on the same fork with `skip_proof_of_work`; ONE producer takes node 0's template, writes the ladder bits it wants into the header version and submits the block, one block per DAA score on a linear chain, so every window of W = 100 DAA holds exactly 100 blue blocks, one of each residue modulo 100. A schedule names per DAA range the direction and how many residues carry no signal: 11 residues give 8,900 bps in every window whatever the window's alignment, 10 give 9,000. "None" blocks alternate between no bits and both bits, so the chain shows both forms read as none. The driver polls every node's template (rung, weakest up, weakest down) through the run, restarts a node mid-window on request (SIGINT, same data dir, same arguments), and at the end re-tallies the chain in JavaScript (an independent copy of the rule: the seed rule, the 7 buckets, floor rounding, admissibility, the cool-down) and compares it with what the nodes did | + +Why the second instrument: three equal miners cast 0, 33, 67 or 100 percent, and a real miner's share in any one +window scatters by several points (the lane's own runs: 5,833 to 6,333 bps weakest for a 67 percent population), so no +real-mining run can hold 8,900 to 8,999 bps in the weakest of seven windows. The rule is consensus-side and reads the +chain's headers, not the miner, so a chain whose headers carry exact shares asks it the exact question. The skip-PoW +network accepts every submitted block (each node logs `PoW rejected ... by igneum-lottery-v2-bound (daa N, nonce 0x0)` at +INFO and accepts the block; the chain-side fact is the block count on every node). + +The arithmetic of the exact-share cases (L = 60 DAA per epoch, lead 10, W = 100, 7 W = 700; genesis and the first +produced block both sit at DAA 0, then one block per DAA): the seed block of epoch e is at DAA 60 e - 11; the seven +windows are full from epoch 12 (seed 709); the oldest window of epoch e is DAA [60 e - 710, 60 e - 611]; after a step +that took effect at DAA S the next decision is the first epoch with 60 e - 710 >= S. + +| Case | Schedule (from DAA : direction : residues with no signal) | Expected by hand | Why | +|---|---|---|---| +| eighty-nine | 0:up:11, 1200:up:10 | no step through epoch 30 at a weakest of 8,900; rung 1 at epoch 31 when the weakest first reads 9,000; rung 2 at epoch 43, the first epoch after the cool-down; nothing else to epoch 45 | residue 10 turns from none to up at DAA 1,200; the oldest window's residue-10 block is 1,210 at epoch 31 (1,110 at epoch 30); after the step at DAA 1,860 the first epoch with 60 e - 710 >= 1,860 is 43 | +| down | 0:up:0, 720:down:11, 1500:down:10, node restarts n2 at DAA 1,000, n1 at 2,300, n2 at 2,700 | rung 1 at epoch 12 (100 percent up); no step down at 8,900 down (epochs 24 to 35, the first cooled-down epoch is 24); rung 0 at epoch 36 when the weakest down first reads 9,000; then down at 9,000 through epoch 50 with no step below 0 (epoch 48 is the first cooled-down epoch after the down-step and the rule must hold at rung 0) | the oldest window's residue-10 block is 1,510 at epoch 36 (1,410 at epoch 35); after the down-step at DAA 2,160 the first epoch with 60 e - 710 >= 2,160 is 48 | +| floor | 0:down:0 | no step at all through epoch 20 | 100 percent down at rung 0 from genesis: the windows are full from epoch 12, the cool-down is trivially met, the rule must stand at 0 | + +## 4. Runs + +All on igneum-build-1, 7 October 2026. Times UK (UTC+1); the logs are UTC. Every run held the measure file +shared for its own length only; the first waited behind F6's exclusive hold (its batch A, 09:15 to 09:25 UK). Log paths +are under `/srv/builds/igneum-wt-attack/attack-f10/runs/` on the box, copied to `tools/attack/f10-ladder/runs/` here +(`.log` = harness stdout, `.json` = summary, `-n{0,1,2}.log` = node logs). + +### 4.1 The harness, trusted: the known-failed case and the known pass (real CPU mining, W = 60 DAA) + +| Case | Run (UK) | Result | Numbers | Files | +|---|---|---|---|---| +| Known-failed, `--signal up,up,none --expect step` | 09:25:51 to 09:36:50 | FAIL rc=1, as it must: no step | no step over epochs 0 to 10; weakest-of-seven up share at the sink 5,833 bps from epoch 7 (5,500 at epoch 10); on the chain 385 blocks up, 221 none (6,353 bps up); 606 blocks; 0 rejected; one sink 4a7f20cc at 605/605/605; the 8 step checks failed (template_stepped_to_rung_1 ... rung1_ids_differ_from_the_same_seed_rung0_id); the lane's genesis low-byte fault did not fire (fixed in the file) | `baseline-fail.log`, `.json` | +| Known pass, `--signal up,up,up --expect step` | 09:36:50 to 09:48:00 | PASS 18 of 18 | step line on 3 of 3 nodes at epoch 8: `420 of 420 blue blocks up`, weakest up 10,000 bps, shares [10000 x 7]; template rung 1 (35 passes) from epoch 8 (DAA 480) at 538.2 s; epochs 9 and 10 at rung 1, one step line per node (no second step inside seven windows); 481 / 132 blocks across the boundary; 612 blocks up and genesis none (9,984 bps); 0 rejected; one sink 41e81944 at 612/612/612; the miners' rung-1 ids on epochs 8, 9, 10 equal the CLI's `--shadow-reps 35` id and differ from rung 0 (e8 218fa530b4c599b0 against 5c5a326a31a4795d, e9 8f30ce6666b4ea8f against c73f3c63daac3748, e10 e2ea0a1ea8b4ca44 against 626455372164a1b5) | `baseline-pass.log`, `.json` | + +Both reproduce the ladder lane's runs of 6 October (`docs/design/latency-ladder-harness/`), on the F10 cores. + +### 4.2 The exact-share cases (skip-PoW, one block per DAA, W = 100 DAA, 8 blocks per second) + +| Case | Run (UK) | Harness line | What the chain did | Files | +|---|---|---|---|---| +| eighty-nine (first run, driver v1) | 09:48:00 to 09:54:27 | FAIL rc=1 on three harness faults (section 4.3); the chain's facts are those of the re-run | identical to the re-run below | `exact-89.log`, `.json` | +| eighty-nine (re-run, driver v2) | 10:04:45 to 10:11:14 | PASS 19 of 19 | 2,701 blocks, linear; 2,418 up, 283 none (135 of them with both bits); weakest up 8,900 bps at every epoch 12 to 30 and NO step (19 epochs, "stands" on every node); epoch 31: weakest 9,000 exactly, step line on 3 of 3: `630 of 700 blue blocks up`, shares [9000 x 7], rung 1 (35 passes); epochs 32 to 42 at 9,000 with no step (cool-down: the oldest window begins 1,210 to 1,810, the step took effect at 1,860); epoch 43: rung 2 (53 passes), `630 of 700`; 44 and 45 cool-down; 0 disagreements between nodes at any poll; one sink 1dd776b4 at 2700/2700/2700; 2 step lines per node; 382 s | `exact-89b.log`, `.json`, `-n0.log` | +| floor (driver v2) | 10:01:40 to 10:04:38 | PASS 19 of 19 | 1,201 blocks; 1,200 down, genesis none; from epoch 12 every window reads 10,000 bps down at rung 0; the rule stands on every node for epochs 12 to 20 ("down signalled at rung 0: the floor"); no step line on any node; one sink a52e6a71 at 1200/1200/1200 | `exact-floor.log`, `.json` | +| down (first run, driver v1) | 09:54:27 to 10:01:40 | FAIL rc=1 on the same three harness faults | identical to the third run below, restarts included | `exact-down.log`, `.json`, `-n1.log`, `-n2.log` | +| down (second run, driver v2) | 10:11:14 to 10:18:26 | FAIL rc=1 on one harness fault (the anchor comparison at the two boundary epochs 13 and 23, section 4.3); 17 comparable epochs equal; the step lines' own weakest equal the oracle | identical to the third run | `exact-downb.log`, `.json`, `-n{0,1,2}.log` | +| down (third run, driver v3) | 10:19:13 to 10:26:25 | PASS 19 of 19 | 3,001 blocks, linear; 720 up, 2,053 down, 228 none (110 with both bits); epoch 12: rung 1 on 3 of 3 (`700 of 700 blue blocks up`, weakest up 10,000); epochs 13 to 23 cool-down (the oldest window begins 70 to 670, the step took effect at 720); epochs 24 to 35: weakest down 8,900 bps on every node, NO step down (12 epochs "stands"); epoch 36: weakest down 9,000 exactly, step line on 3 of 3: `0 of 700 blue blocks up, 630 down`, rung 0 (27 passes, from rung 1); epochs 37 to 47 cool-down; epochs 48 to 50: 9,000 down at rung 0, the rule stands (never below 0), no third step line; restarts: n2 at DAA 1,004 (1 step line before, 4 after), n1 at DAA 2,304 (2 before, 2 after), n2 at DAA 2,704 (3 before, 2 after), every line after a restart identical in epoch, rung, origin and weakest to the lines before; 0 disagreements; one sink 20c6b367 at 3000/3000/3000; step lines 2 / 4 / 5 per node; 425 s | `exact-downc.log`, `.json`, `-n{0,1,2}.log` | + +Per epoch, the down case as the nodes and the oracle saw it (from `exact-downc.json`; "rungs" = the first template of the +epoch on n0 / n1 / n2; "weakest" = the decision's number from the step line where one exists, else the template's live +sink tally, which equals the seed-anchored oracle at every epoch with no schedule boundary inside the windows): + +| Epoch | Seed DAA | Rungs n0/n1/n2 | Weakest up / down (bps) | Oracle rung | Oracle reason | +|---|---|---|---|---|---| +| 11 | 649 | 0/0/0 | partial | 0 | windows not full | +| 12 | 709 | 1/1/1 | 10,000 / 0 | 1 | up: 700 of 700 | +| 13 to 23 | 769 to 1,369 | 1/1/1 | mixed, under 9,000 both ways | 1 | cool-down (oldest window begins before 720) | +| 24 to 35 | 1,429 to 2,089 | 1/1/1 | 0 / 8,900 | 1 | stands: 8,900 is under 9,000 | +| 36 | 2,149 | 0/0/0 | 0 / 9,000 | 0 | down: 630 of 700 | +| 37 to 47 | 2,209 to 2,809 | 0/0/0 | 0 / 9,000 | 0 | cool-down (oldest window begins before 2,160) | +| 48 to 50 | 2,869 to 2,989 | 0/0/0 | 0 / 9,000 | 0 | down signalled at rung 0: the floor | + +And the eighty-nine case (`exact-89b.json`): + +| Epoch | Seed DAA | Rungs n0/n1/n2 | Weakest up (bps) | Oracle rung | Oracle reason | +|---|---|---|---|---|---| +| 12 to 30 | 709 to 1,789 | 0/0/0 | 8,900 | 0 | stands, 19 epochs | +| 31 | 1,849 | 1/1/1 | 9,000 | 1 | up: 630 of 700 | +| 32 to 42 | 1,909 to 2,509 | 1/1/1 | 9,000 | 1 | cool-down (oldest window begins 1,210 to 1,810, the step took effect at 1,860) | +| 43 | 2,569 | 2/2/2 | 9,000 | 2 | up: 630 of 700 | +| 44 to 45 | 2,629 to 2,689 | 2/2/2 | 9,000 | 2 | cool-down | + +### 4.3 Harness faults found and fixed on the way (the driver's, never the chain's) + +| Fault | Seen | Fix | +|---|---|---| +| `every_produced_block_on_every_node` compared `blockCount` with produced + 1; the node's `blockCount` excludes genesis | exact-89 first run, 09:54 UK | compare with produced (2,700 = 2,700) | +| `zero_rejected_by_nodes` grepped `ban` and matched the finality parameter line `... ban 120 ...` | same run | the word dropped; the skip-PoW INFO line `PoW rejected ... by igneum-lottery-v2-bound` excluded by its own text | +| `node_weakest_equals_oracle_weakest` compared the template's weakest with the seed-anchored oracle at every epoch; the template's number is the LIVE tally anchored at the sink (`consensus/mod.rs` `get_pow_epoch_info`, `tally_ladder(..., sink, ...)`), read at the epoch's first template, sink = seed + lead (10 DAA) | epoch 11 of exact-89 (49 of 59 at the sink against 39 of 49 at the seed); epochs 13 and 23 of the second down run (40 up in (679, 779] against 50 in (669, 769], the boundary at 720 inside both) | compared only at epochs with seven full windows and no schedule boundary inside the windows plus the lead; a new check compares the decision's own weakest (the step line) with the oracle at every stepped epoch, which passed in every run | + +The smoke run (`smoke.log`, 09:14 UK, 3 epochs) validated the template round trip (`submitBlock` reports +`{"type":"success"}`, 180 blocks on 3 of 3 nodes at 8 per second). + +## 5. What the runs show against the gate + +| Gate clause | Shown by | Numbers | +|---|---|---| +| No step up without 90 percent over 7 windows | eighty-nine: 19 epochs at 8,900 bps in every window, rung 0 held on every node; the step came at the first epoch whose weakest read 9,000, 630 of 700 blue blocks | epochs 12 to 30 stand; 31 steps | +| No step down without 90 percent over 7 windows | down: 12 cooled-down epochs at 8,900 bps down in every window, rung 1 held on every node; the step down came at the first epoch whose weakest down read 9,000, 630 of 700 | epochs 24 to 35 stand; 36 steps | +| A step down needs the same cool-down | down: epochs 13 to 23 at rung 1 with the oldest window beginning before the step took effect: the rule stood although the up share had collapsed | 11 epochs | +| Never below 0 | floor: 10,000 bps down at rung 0 for 9 epochs, no step line; down: 9,000 bps down at rung 0 for epochs 48 to 50 after the cool-down, no step line | 12 epochs across two runs | +| Monotone: one rung per decision, seven windows between decisions | eighty-nine: rung 1 at 31, rung 2 not before 43 with 9,000 in every window throughout; down: rung 1 at 12, rung 0 at 36 | the cool-down held 11 epochs each time | +| The decision computed once per seed block and reused | one or two step lines per process per stepped epoch (two when the first template and header processing walked concurrently), none afterwards | n0: 2 lines for 2 steps in every exact run | +| A node restarted mid-window reaches the same decision | three restarts in the down case: every step line after a restart repeats the lines before it in epoch, rung, origin and weakest; the restarted node's template rung equals the others' at every epoch | n2 at 1,004 and 2,704, n1 at 2,304 | +| Two nodes never disagree on the rung at the same height | 0 disagreements at every observation (every fifth block) and at every epoch's first template, in every run | 5 exact runs, 2 baseline runs | +| Both bits = none | 135 and 110 both-bits blocks counted as none by the oracle and by the nodes (the shares matched) | eighty-nine, down | +| The known-failed shape (a chip owner stepping down under 90 percent; a step registered under 90 percent) | did not occur; 8,900 held in both directions, floor rounding puts 8,999 below the line (unit test, `igneum.rs` 1161) | gate holds | + +## 6. Static reading of the rule (what the harness cannot show) + +Read in the fork at 1591ee1d before the runs. Each line is a property of the code as written, with the place. + +| Property | Where | Reading | +|---|---|---| +| Symmetry of the two directions | `igneum.rs` 676 to 686 | one closure `all(shares)` serves both bits; the up branch runs first, then `all(down) && previous.step > 0`; up and down cannot both reach 9,000 bps of one window's blocks, so the order never decides | +| The cool-down is direction-free | `igneum.rs` 674 | `first_counted_daa < previous.since_daa` returns the previous state before either branch is read; a step down waits the same seven windows after a step up as a step up does after a step down | +| Never below 0 | `igneum.rs` 681 | `previous.step > 0` guards the subtraction; a 100 percent down signal at rung 0 stands (the floor case below shows it on the chain) | +| Never past an inadmissible rung | `igneum.rs` 679 | `ladder.admissible(previous.step + 1)`; rung 3 is `admissible: false` in the file, so from rung 2 a 100 percent up signal stands (unit test `latency_ladder_rule`, `igneum.rs` 1161) | +| Floor rounding | `igneum.rs` 431 to 437 | `signal_share_bps` = floor(10,000 x signalling / total); 89 of 100 blue blocks is 8,900, 90 is 9,000; on a mainnet window of 86,400 blocks 77,759 up is 8,999 and 77,760 is 9,000 | +| Both bits set | `igneum.rs` 639 to 645 | `version & 0xc000 == 0xc000` falls to `None`; a header cannot vote both ways and cannot vote twice | +| Weakest of seven | `class_signal.rs` `SignalTally::weakest_bps` and the rule's `all` | the decision rests on the lowest of the seven windows; one bought window at 100 percent moves nothing (unit test "one bought day does not move it") | +| The windows are the seed block's own past | `class_signal.rs` `tally_window_by` | the anchor and the mergeset blues of each selected-chain block walking down, bucketed by `daa_c - daa`, stopping once `daa_cur + merge_depth < window_start`; blocks above the seed are never counted, so the seven windows are fixed once the seed block is | +| The memo is sound | `latency_ladder.rs` `step_of_epoch` | keyed by the seed block's hash; the decision is a function of that block's selected-chain past and of process-global constants installed from the file (ladder, activation, window), so two processes with the same file and the same chain compute the same value; the memo is never read across a param change because the params are fixed at start; cleared above 100,000 entries, then rebuilt by the walk | +| Concurrent first computation | `latency_ladder.rs` `memo_get` / `memo_put` | the lock is not held across the walk, so two concurrent callers may both walk and both log the step line; both write the same value, so the chain's decision is unaffected (the runs below show one or two step lines per process for the same epoch, identical in content) | +| A node without the history | `latency_ladder.rs` `step_of_epoch`, the two `warn!` returns | a node whose seed block's windows cannot be walked (synced from a pruning proof) decides RUNG 0 and logs "a ladder witness is owed". After a step up, such a node runs rung 0's program and refuses rung 1's blocks: a split between full-history nodes and proof-synced nodes. The design doc lists the witness as owed (section 9). This is not a fault of the step rule and the harness cannot reach it (every node here has the history); it is a precondition on activation: no network activates the ladder while any peer syncs from a proof without the witness. Routed to main in section 8 | + +Nothing in the reading admits a step under 9,000 bps in either direction, a step down under the cool-down, a step +below rung 0, or a decision that depends on which node computes it or when. + +## 7. Consequences per tier + +The rule holds, so a step in either direction costs 90 percent of blue blocks in each of seven consecutive days, and +the earliest second step is seven days after the first. What a WRONGFUL step would have done, had the rule admitted one +under 90 percent, is the measured per-rung table of `docs/design/latency-ladder.md` section 8 (algorithm.md 5.3a rungs, +igneum-build-1 verifier) read in each direction. Every row below is that table's number, not a new measurement. + +| Wrongful step | M5 Max (Apple tier) | RTX 5090 at 431 W | RTX 4070 at 160 W | RX 9070 XT | 8 / 12 / 16 GB cards, rigs, pools | Verifier (half-core) | f = 1 chip's per-joule edge over the 5090 | +|---|---|---|---|---|---|---|---| +| Up 0 to 1 (102,100 to 132,100 ops) under 90 percent | -3.3 points of rate, 0 W more | 0 | 0 | 0 | 0 (the shadow costs ALU, not memory; the dataset size is the schedule's, not the ladder's) | +0.2 ms | 2.1x to 1.7x at k = 1 (3.9x to 3.4x at k about 0.33) | +| Up 1 to 2 (to 199,600) under 90 percent | -6 more points | -2.7 percent | +21 W | 0 | 0 | +0.5 ms | to 1.3x (2.8x) | +| Up 2 to 3 (to 330,700): inadmissible, never entered | -21 percent | -35 percent (compute-bound at the cap) | -12 percent | +3.6 percent | 0 | +0.9 ms | 3.0x at k about 0.33 | +| Down 2 to 1, 1 to 0 under 90 percent (the chip owner's step) | the Apple tier gets its 6 then 3.3 points back | +2.7 percent then 0 | -21 W then 0 | 0 | 0 | -0.5 then -0.2 ms | the chip regains 1.3x to 1.7x to 2.1x (2.8x to 3.4x to 3.9x): every rung down hands the stored-dataset chip back the edge the miners paid for | + +Reading per tier, with the rule as it stands: + +| Tier | What the result means | +|---|---| +| Home card, 8 / 12 / 16 / 24 GB, any vendor, any OS | A step up costs rate only on the Apple tier at rungs 1 and 2, and on NVIDIA from rung 2; no step happens unless 90 percent of blocks over seven days ask for it, so a minority that would lose rate cannot be moved by a bought day or a 89 percent week, and a chip owner under 90 percent cannot move the rung down to cheapen its core. A 90 percent majority can step the chain down one rung per week to the floor (rung 0 = class v4 as it ships), which is the design's floor and not a weakness of the rule: at 90 percent of blocks the owner already orders the chain | +| Rig, pool user | The same; a pool signals per block through its node's `IGNEUM_LADDER_SIGNAL` (the app's toggle later), so a pool's share of blocks is its weight | +| Verifier (the node, the proof) | Admissibility is a genesis flag per rung; rung 3 is never entered by any signal until a quiet re-measurement before genesis moves the flag (section 4 of the design doc); the memo keeps the per-template cost to one walk per seed block per process | +| A node synced from a pruning proof | Decides rung 0 until the ladder witness lands (section 6, last row): the ladder must not activate on a network where such nodes exist before the witness. This is the one consequence the rule's text does not state and the spec line should | + +## 8. Verdict, and what goes to main + +PASS. No step without 90 percent of blue blocks in each of seven consecutive windows in either direction; a step down +needs the same 90 percent and the same seven-window cool-down; the floor holds under 100 percent down; the decision is +per seed block, memoised per process, recomputed identically after a restart, and never differs between nodes at the +same epoch. The known-failed harness case fails, the known pass passes, and three new cases (89 percent up, 89 then 90 +percent down with restarts, the floor) pass on the chain and on the harness's own 19 checks. The step rule's text in +spec 01 needs no change for the gate. + +To main, not findings against the gate: + +| Item | What | Proposed route | +|---|---|---| +| Stale commit string in the ladder lane's `igneumd` | the binary built 6 October 22:15Z from the fork at 1591ee1d carries 8dbb7a23 (its parent) and no 1591ee1d; the ladder code is in it | the commit-string-check class (CLAUDE.md, 6 October 2026); the ladder lane rebuilds with the two-step before any Devnet 2 crossing; nothing in this row depends on it | +| Proof-synced nodes decide rung 0 until the witness lands | `processes::latency_ladder::step_of_epoch` returns rung 0 with a warning when the seed block's windows cannot be walked; after a step, such a node runs the wrong program and splits from full-history peers | a precondition line for the step rule's text in spec 01 when the ladder is adopted: "the ladder activates only once every node can walk the seven windows below every seed block, or carries the ladder witness in its pruning proof"; the design doc already lists the witness as owed (section 9); node lane | +| Spec text for the ladder, when adopted (none in spec 01 today; the only ladder there is `epoch_len`'s) | the rule as run: 90 percent of blue blocks in each of 7 consecutive windows ending at the seed block, floor rounding, one rung per decision, the oldest window at or after the last step in either direction, never below rung 0, never into an inadmissible rung; the template's weakest is the live sink tally and the decision's is at the seed | the algorithm lane's spec line; this record is the test it cites | +| Three harness faults in the F10 driver | section 4.3; all three were the driver's reading of the node, fixed in `ladder-exact.mjs` v3 | none owed; recorded so the firm does not repeat them | + +Blocked: nothing. Not run: a real-mining 89 percent case (three equal miners cannot cast it; the exact-share driver +asks the rule the same question through the same submit path and the same consensus code). diff --git a/docs/analysis/attack-pass/f2-mixer.md b/docs/analysis/attack-pass/f2-mixer.md new file mode 100644 index 00000000..194880d0 --- /dev/null +++ b/docs/analysis/attack-pass/f2-mixer.md @@ -0,0 +1,214 @@ +# Attack pass F2: the mixer's round margin + +Row F2 of `docs/plans/cryptanalysis.md` section 4.2 (branch `cryptanalysis`), fed into +`docs/analysis/attack-pass-2026-10.md`. Run 7 October 2026, 09:00 to [FILL] UK, by the attack-pass sub-agent F2 on +igneum-build-1 (cores 6-11 and 54-59, nice 10, the measure file held shared in chunks under 30 minutes). + +## 1. Target + +Commit `924288d1` (worktree `igneum-wt-attack`, branch `attack-pass`). The x8 mixer of `igneum-pow/src/memhard.rs`, +`mixer` (lines 300 to 313): one application on 16 words of 32 bits is, per word, `(s[i] ^ (RC[i] + rk)) * MUL[i]` +with `MUL[i]` odd, then one ChaCha-shaped double round: four column quarter rounds with rotations `ROT[0..3]`, +four diagonal quarter rounds with `ROT[4..7]`. `ROT`, `MUL`, `RC` are drawn per day from the 64-bit SplitMix64 seed +`K[0] | K[1] << 32` by `MixParams::with_shape` (lines 237 to 258). Under class v3 and v4 (`m = 8`) an item is 8 +dependent cache reads, each preceded by 8 applications with round keys `round_key(r * 8 + j)`, and 8 more after the +last read: 72 applications per item (`derive_items_mask`, lines 517 to 550). The chip model prices one application +at 128 hoisted operations and an item at 9,360 (`docs/analysis/chip-model-v3.md` 5.2). + +The days modelled: the genesis day `2026-10-03` (`ROT = 20 20 19 4 26 3 3 27`, as `proto-metal/MEMHARD.md` line 82 +states; the harness reads the same draw from the code) and two other days, `2026-10-04` (`ROT = 28 15 9 26 2 2 22 +8`) and `2027-03-01` (`ROT = 31 16 15 15 2 9 19 4`). Their full `MUL` and `RC` are in the box files +`/srv/builds/igneum-wt-attack/target-attack-f2/params/.real.txt`. + +Known-failed shape (the plan's row): a differential or linear trail, a rotational-XOR relation, or an algebraic fold +that distinguishes or shortcuts more than 2 of the 8 applications between dependent reads. Gate: none beyond 2 of 8. + +## 2. Method + +Four searches and two checks, every one on the bit-level definition in `memhard.rs` (the harness calls +`igneum_pow::memhard::mixer` itself; the SAT models consume one op list whose value evaluator is checked against +the Rust output on 64 applications per day and variant, 9 files, all matching). + +| Piece | What it is | Exact or model | +|---|---|---| +| Differential, MSB family | XOR differences; at every multiply each word's difference is 0 or `0x80000000`. These are the only word transitions through an odd multiply with probability 1 (`(x ^ 2^31) * c = (x * c) ^ 2^31`; any other nonzero difference passes with probability at most 1/2, since its lowest active bit below the MSB leaves a carry to chance). Modular addition by Lipmaa-Moriai (exact per adder), XOR and rotation linear | exact family, trail probabilities exact per operation | +| Differential, general | The same ARX model with every word difference allowed through the multiply: XOR difference to modular difference (each set bit below the MSB is a sign choice, 2^-1 each, exact), times `MUL` (exact, a circuit on the difference variables), modular back to XOR (a carry chain, one bit per position where the difference bit and the carry differ, exact), the two conversions taken as independent | Markov trail model; its per-word cost sits 1 to 2 bits above the sampled best transition (section 4.1), so it is a trail model, slightly pessimistic for the attacker | +| Linear, low-bit family | Masks; at every multiply the output mask lies in bits 0 and 1, the only F2-linear output bits of an odd multiply (`(cx)_0 = x_0`, `(cx)_1 = x_1 ^ (c_1 & x_0)`). Modular addition by the exact carry-mask automaton (per bit a carry-mask bit; checked against brute force at n = 8 on 500 mask triples, max error 0) | exact family | +| Linear, general | The same with the multiply as its shift-and-add decomposition (one adder per set bit of `MUL`, the low known-zero bits of a shifted copy transparent), each adder under the automaton | trail model; over-optimistic for the attacker (section 4.3) | +| Rotational-XOR | Measured on the real code: for every rotation r in 1..31 and k = 1..4, the per-bit bias of `rot_r(M^k(x)) ^ M^k(rot_r(x))` over 2^20 states, the largest |z| of the 512 bits, and the count of exact rotational pairs; plus the word-level prologue `g(x) = (x ^ C) * MUL` alone: the most frequent value of `rot_r(g(x)) ^ g(rot_r(x))` over 2^20 inputs | measurement | +| The fold | The identities a chip would need to pay less than k x 128 for k applications, each tested on 2^20 random inputs, plus the algebraic argument (section 4.5) | measurement and argument | + +Search: for each (model, day, k = 1..4) the weight bound W is probed upward (SAT means a trail of weight at most W +exists, UNSAT means none does in the model), then narrowed to the minimum. A k-application trail restricted to one +application is a valid 1-application trail, so every application is held to the proven k = 1 minimum of the same +model (the Matsui floor in the tables). Solver CaDiCaL 1.9.5 through python-sat 1.9. Every trail found of +measurable weight is measured on the real code before it counts: per application and as a chain, 2^20 to 2^28 +samples (`attack-f2 verify-diff` / `verify-lin`), with the multiply-layer word transitions counted exactly over all +2^32 inputs (`verify-mults`). A trail that does not hold is blocked and the solver asked again at the same bound. +Linear trails whose correlation cancels inside one adder's hull are caught first by the exact signed sum over the +adder's carry masks. + +What "reaches k applications" means here, two readings: (a) the shortcut reading, the one with a cost consequence: +a relation of probability 1 (weight 0) over k applications, which a chip could use to skip work; (b) the +distinguisher reading: a trail of weight under 64 over k applications, the usual practical line. For the gate both +are reported. + +## 3. Harness + +| Item | Path | +|---|---| +| Crate (ground truth: parameters, vectors, verification, RX, fold) | `tools/attack/f2-mixer/` (`Cargo.toml`, `src/main.rs`), `igneum-pow` by path, own `[workspace]` | +| SAT models and the search | `tools/attack/f2-mixer/model.py` (`selftest`, `search`, `show`) | +| Box queue runner, tables | `tools/attack/f2-mixer/run_jobs.sh`, `tools/attack/f2-mixer/summarise.py` | +| Build line (from the crate directory) | `IGNEUM_AGENT=attack-f2 bash /Users/joshm/Projects/igneum/tools/build-remote.sh --artefacts "target/release/attack-f2" --out -- build --release`; binary on the box `/srv/builds/igneum-wt-attack/tools/attack/f2-mixer/target/release/attack-f2` (ELF x86-64, sha256 `150337ec...`, the third build; 12 s incremental) | +| Box scratch (states, trails, logs, venv) | `/srv/builds/igneum-wt-attack/target-attack-f2/` (`state/`, `logs/`, `params/`, `vectors/`, `venv/`). The brief's path `attack-f2/` was wiped within ten minutes by another lane's worktree-root rsync (`--delete` spares only `target-*`), so the scratch moved under a `target-` name, as F1 and F6 did | +| Run lines | `venv/bin/python3 model.py selftest --vectors vectors --params-dir params`; `bash run_jobs.sh jobs.txt 11` (each job `model.py search --kind diff|lin --family msb|general|low2 --params params/..txt --apps k --state state/.json --budget --per-app-min --verifier ` under `flock -s /srv/builds/_locks/measure`, `nice -n 10 taskset -c 6-11,54-59`); `attack-f2 rx --day D --variant V --apps 4 --log2 20`; `attack-f2 rx-word --day D --log2 20`; `attack-f2 fold --day D --log2 20` | +| Logs | `logs/---k.log` per search, `logs/rx...log`, `logs/rx-word...log`, `logs/fold..log`, `logs/summary.md` (the tables below), `logs/verify*.log` | + +## 4. Results + +### 4.1 The harness fires (known pass, known fail) + +| Case | Expected | Got | Log | +|---|---|---|---| +| Selftest: evaluator against `attack-f2 vectors`, 3 days x 3 variants, 4 applications x 16 states each | all match | 9 of 9 files, 64 of 64 applications each | `selftest` output, `logs/selftest.log` | +| Selftest: linear add automaton against brute force, n = 8 | exact | 300 random triples and 200 shifted-copy triples, max error 0.00e+00 | same | +| Selftest: Lipmaa-Moriai against brute force, n = 8; both SAT encodings against their rules at n = 32 | exact | max error 0; 0 mismatches of 40 and 40 | same | +| Selftest: the multiply model's word cost against the sampled best transition (word 3, genesis day) | MSB exact; others within a few bits | MSB: weight 0, measured 2^-0 (exact); bit 30: model 2, sampled best 2^-1.00; bits 31+5: model 8, sampled best 2^-6.03; bit 0: model 10, sampled best 2^-8.97 | same | +| Known pass, 0 applications | the identity trail, weight 0 | trivial (input = output, no weights); not run as a job | | +| Known fail, `rot0` (every rotation 0), differential, k = 1, 2, 4 | a weight-0 trail (MSB-only differences stay MSB-only when nothing rotates) | weight 0 found at k = 1, 2, 4 (both families); measured probability 1 on the real code (`verified_chain -0.0`) | `state/diff-msb-2026-10-03-rot0-k{1,2,4}.json`, `state/diff-general-2026-10-03-rot0-k{1,2}.json` | +| Known fail, `rot0`, linear, k = 1, 2, 4 | a weight-0 trail (LSB masks) | weight 0 at k = 1, 2, 4; measured correlation 1 per application and as a chain | `state/lin-low2-2026-10-03-rot0-k{1,2,4}.json`, `state/lin-general-2026-10-03-rot0-k{1,2}.json` | +| Known fail, `nomul` (MUL 1, RC 0, rk 0: the bare double round), rotational-XOR, k = 1 | a large per-bit bias | max |z| 134.2 (r = 31) against 4.2 for the real mixer; word-level: the prologue is exactly rotational (2^20 of 2^20) against 3 of 2^20 | `logs/rx.2026-10-03.nomul.log`, `logs/rx-word.2026-10-03.nomul.log` | +| Known fail, `rot0`, rotational-XOR | bias | max |z| 32.3 at k = 1, 9.0 at k = 2 | `logs/rx.2026-10-03.rot0.log` | +| Known fail, `nomul`, differential k = 1 | the bare double round's best trail, below the real mixer's | weight 7 found (model), measured 2^-5.0 on the real code | `state/diff-general-2026-10-03-nomul-k1.json` | + +### 4.2 Differential trails + +| Model | Day | Variant | k | Best trail weight found | No trail at or below (model) | Closed | Per-application floor | Verified on the real code (chain; per application) | Solver s | +|---|---|---|---|---|---|---|---|---|---| +| diff/general | 2026-10-03 | real | 1 | 12 | 11 | yes | 0 | 12.011; [11.939] | 186 | +| diff/general | 2026-10-03 | real | 2 | none | 24 | no (timebox) | 12 | | 1,739 | +| diff/general | 2026-10-04 | real | 1 | 10 | 9 | yes | 0 | 10.001; [9.999] | 321 | +| diff/general | 2026-10-04 | real | 2 | none | 20 | no (timebox) | 10 | | 663 | +| diff/general | 2027-03-01 | real | 1 | 12 | 11 | yes | 0 | 12.057; [11.907] | 175 | +| diff/msb | 2026-10-03 | real | 1 | 12 | 11 | yes | 0 | 12.206; [11.972] | 4 | +| diff/msb | 2026-10-03 | real | 2, 3, 4 | none | 512 (the family dies) | yes | 12 | | 26, 33, 22 | +| diff/msb | 2026-10-04 | real | 1 | 10 | 9 | yes | 0 | 10.001; [10.001] | 309 | +| diff/msb | 2026-10-04 | real | 2, 3, 4 | none | 512 | yes | 10 | | 20, 33, 44 | +| diff/msb | 2027-03-01 | real | 1 | 12 | 11 | yes | 0 | 12.057; [11.907] | 3 | +| diff/msb | 2027-03-01 | real | 2, 3, 4 | none | 512 | yes | 12 | | 10, 15, 21 | +| diff/general | 2026-10-03 | nomul (known fail) | 1 | 7 | 6 | yes | 0 | 5.002; [5.003] | 38 | +| diff/general | 2026-10-03 | nomul | 2 | none | 20 | no | 7 | | 1,309 | +| diff/general, diff/msb | 2026-10-03 | rot0 (known fail) | 1, 2, 4 | 0 | | yes | 0 | probability 1 | under 1 | + +The general model's k = 3 and k = 4 jobs (closed 14:3x UTC, every job at its 7,200 s cap, `logs/summary.md`): + +| Model | Day | k | Best trail found | No trail at or below (model) | Per-application floor | Solver s | +|---|---|---|---|---|---|---| +| diff/general | 2026-10-03 | 3 | none | 35 | 12 | 7,201 (cap) | +| diff/general | 2026-10-03 | 4 | none | 47 | 12 | 7,359 (cap) | +| diff/general | 2026-10-04 | 3 | none | 29 | 10 | 7,350 (cap) | +| diff/general | 2026-10-04 | 4 | none | 39 | 10 | 7,279 (cap) | +| diff/general | 2027-03-01 | 3 | none | 35 | 12 | 7,321 (cap) | +| diff/general | 2027-03-01 | 4 | none | 47 | 12 | 7,284 (cap) | +| lin/general | 2026-10-03 | 3 | none | 24 | 1 | 7,953 (cap) | +| lin/general | 2026-10-03 | 4 | none | 24 | 1 | 7,352 (cap) | +| lin/general | 2026-10-04 | 3 | none | 28 | 1 | 7,373 (cap) | +| lin/general | 2026-10-04 | 4 | none | 24 | 1 | 7,393 (cap) | +| lin/general | 2027-03-01 | 3 | none | 24 | 1 | 7,402 (cap) | + +No trail of weight under 32 at three applications (the finding line): the bound reached is 29 to 35 at three and +39 to 47 at four for differentials, 24 to 28 at three and 24 at four for linear masks, all solver-capped, so these are +effort bounds, not proofs; they grow with k as the per-application floors predict. + +### 4.3 Linear trails + +| Model | Day | Variant | k | Best trail weight found (correlation 2^-w) | No trail at or below | Closed | Verified (chain; per application) | Solver s | +|---|---|---|---|---|---|---|---|---| +| lin/general | 2026-10-03 | real | 1 | 1 | 0 | yes | 0.996; [0.997] | 5 | +| lin/general | 2026-10-03 | real | 2 | none | 20 | no (timebox) | | 1,019 | +| lin/general | 2026-10-04 | real | 1 | 1 | 0 | yes | 0.995; [0.999] | 5 | +| lin/general | 2026-10-04 | real | 2 | none | 24 | no (timebox) | | 1,669 | +| lin/general | 2027-03-01 | real | 1 | 1 | 0 | yes | 1.003; [0.996] | 5 | +| lin/general | 2027-03-01 | real | 2 | none | 20 | no (timebox) | | 1,224 | +| lin/low2 | 2026-10-03, 2026-10-04, 2027-03-01 | real | 1 | 1 | 0 | yes | 0.995 to 1.003 | 4 to 5 | +| lin/low2 | 2027-03-01 | real | 2, 3, 4 | none | 512 (the family dies) | yes | | 12, 18, 27 | +| lin/general | 2026-10-03 | nomul (known fail) | 1 | 1 | 0 | yes | 0.999; [1.003] | 4 | +| lin/general, lin/low2 | 2026-10-03 | rot0 (known fail) | 1, 2, 4 | 0 | | yes | correlation 1 | 5 to 10 | + +One application carries a weight-1 linear trail (the LSB mask through the prologue and one add, correlation 1/2), +the structural residue of 4.5; at two applications no trail at or below weight 20 to 24 exists in the general +model within the timebox, and the LSB family dies (no trail at or below 512) from k = 2. + +### 4.4 Rotational-XOR + +Per k and day, the largest |z| over all 31 rotations and 512 bits at 2^20 states (15,872 bit tests per k; the +noise ceiling of that many tests is about 4.3), and the count of exact rotational pairs. + +| Day | k = 1 | k = 2 | k = 3 | k = 4 | Exact pairs | Log | +|---|---|---|---|---|---|---| +| 2026-10-03 | 4.22 (r 19) | 4.29 (r 30) | 4.22 (r 3) | 4.62 (r 27) | 0 | `logs/rx.2026-10-03.real.log` | +| 2026-10-04 | 4.35 (r 17) | 4.00 (r 19) | 4.24 (r 25) | 4.49 (r 28) | 0 | `logs/rx.2026-10-04.real.log` | +| 2027-03-01 | 3.96 (r 7) | 4.07 (r 2) | 4.04 (r 28) | 4.17 (r 19) | 0 | `logs/rx.2027-03-01.real.log` | +| 2026-10-03, bare double round (`nomul`) | 134.24 (r 31) | 4.37 | | | 0 | `logs/rx.2026-10-03.nomul.log` | + +The word-level prologue `(x ^ C) * MUL`: over 2^20 inputs the most frequent value of `rot_r(g(x)) ^ g(rot_r(x))` +occurs at most 3 times for every word and every r on all three days (`logs/rx-word..real.log`, the +`rxw_worst` lines), against 2^20 of 2^20 without the multiply. The odd multiply by a random constant is not +rotational to any measurable degree, and one application already shows no per-bit bias. Rotational-XOR does not +reach 1 application. + +### 4.5 The fold of the multiply layer + +One application is `D o P_rk`, with `P_rk(s)_i = (s_i ^ (RC_i + rk)) * MUL_i` and `D` the double round (fixed per +day). Multiplication by an odd constant distributes over modular addition and over nothing else in `D` (XOR, +rotation); the XOR with a constant commutes with XOR and rotation and with nothing else (addition, multiply). A fold +across applications would need one of the identities below. Each was tested on 2^20 random inputs on every day +(`logs/fold..log`): + +| Identity a chip would need | Holds on | Meaning | +|---|---|---| +| `(xa ^ Ca) * ma + (xb ^ Cb) * mb = ((xa ^ Ca) + (xb ^ Cb)) * ma` for the four column pairs (0,4), (1,5), (2,6), (3,7) | 0 of 1,048,576 for every pair on every day (`MUL` distinct in every pair) | the multiply does not fold into the first add of a quarter round; it would if a column pair drew the same `MUL` (probability 2^-31 per pair per day, the weak-day class of F4) | +| `(x ^ C) * m = (x * m) ^ (C * m)`, or `= (x * m) ^ C'` for any single `C'` | 0 of 1,048,576; the best single `C'` agrees on 33 of 1,048,576 (2^-15) | the constant cannot be moved past the multiply, so application j + 1's prologue cannot share application j's multiply | +| an XOR constant on one word commuting with the bare double round (so the next prologue's constant could be folded back) | 0 of 65,536 for every word | every word's value feeds an add inside the double round | +| the MSB passing the prologue and the add for free; the LSB passing the prologue | 1,048,576 of 1,048,576 each | the structural residue: the only free passages, both moved by the rotations (the family deaths in 4.2 and 4.3) | + +So k applications cost k times one application, 128 hoisted operations each (16 multiplies, 32 adds, 32 XORs, 32 +rotations with the constants hoisted); `chip-model-v3.md` 5.2's 9,360 per item stands. The trail weights of 4.2 +and 4.3 growing with k is the quantitative side of the same fact: a composition that collapsed to one application's +shape would keep one application's trail weights. + +## 5. Gate and verdict + +Gate (plan 4.2 F2, 1.4 (1)): no distinguisher or shortcut beyond 2 of the 8 applications between dependent reads, +after the stated search. + +| Line of attack | Reach | Verdict | +|---|---|---| +| Differential, general model (Markov on the multiply, exact add rule, SAT) | one application: best trail weight 10 to 12 on three days, verified on the real code; two applications: no trail at or below weight 20 to 24 within 7,200 s per job (not closed); the MSB family dies at two applications on every day | nothing reaches 2 applications below 2^-20 | +| Linear, general model (piling-up, SAT) | one application: weight 1 (the LSB residue); two applications: no trail at or below 20 to 24 within the timebox; the LSB family dies at two | nothing reaches 2 applications below 2^-20 | +| Rotational-XOR | no per-bit bias at one application (max abs z 4.0 to 4.6 at 2^20 states, noise ceiling 4.3); 0 exact pairs; the multiply prologue is rotational on at most 3 of 2^20 inputs; the bare double round fires at 134 | does not reach 1 application | +| Algebraic fold of the multiply layer | every identity a fold needs holds on 0 of 2^20 inputs on every day; k applications cost k | no shortcut | + +Verdict: PASS with the effort bound stated: about 60 solver jobs, 2 to 29 minutes each, on three day keys; the +reduced-round margin reached is one application fully characterised (weights 10 to 12 differential, 1 linear) and +two applications with no trail under weight 20 to 24, three with none under 29 to 35 (differential) and 24 to 28 +(linear), four with none under 39 to 47 and 24, against 8 applications between reads, so the margin between what the +search reaches and what the construction uses is at least 4 applications at the solver's cap. What this does not do is in section 7; the lower bound is the paid question. + +## 6. Consequences per tier + +No shortcut, so no tier moves: a home card, a rig and a pool pay the 72 applications per item the verifier pays; +a chip with a fixed datapath pays them too (the fold test), which is what `chip-model-v3.md` 5.2's 9,360 ops per +item assumes. `mixer_mult` stays 8; the verifier measurement of F6 stands unchanged. + +## 7. What this does not do + +- It does not bound the mixer from below: the general models are trail models (Markov for the multiply's + differential, piling-up for the linear), and the family models are exact only inside their families. The firm's + job (funding.md B5 rank 1) is the effort-bounded version of the same search with their tools. +- Three days, not a census: the ROT, MUL, RC classes over 2^24 days are F4's row. One cheap addition for F4 from + this harness: the MSB-family death at k = 2 (`model.py search --kind diff --family msb --apps 2`) runs in seconds + per day, and a day where it does not die is a weak day of the kind the gate is about. +- Differential and linear only, as the row says: no boomerang, no integral or cube property, no related-key (the + round keys are public constants). diff --git a/docs/analysis/attack-pass/f3-cache.md b/docs/analysis/attack-pass/f3-cache.md new file mode 100644 index 00000000..a818af17 --- /dev/null +++ b/docs/analysis/attack-pass/f3-cache.md @@ -0,0 +1,148 @@ +# F3: the chained cache's j + 1 bound and the storage-against-recompute curve + +Attack-pass row F3 of `docs/plans/cryptanalysis.md` section 4.2 (the record is `docs/analysis/attack-pass-2026-10.md`). Run 7 October 2026, 09:10 to 09:12 UK (08:10 to 08:12 UTC in the logs), on igneum-build-1. Verdict: PASS on all three gate clauses. No line (s, j) is derivable in fewer than j + 1 block evaluations without an earlier line, by an exhaustive search over the block dependency graph extracted from the code at 64 and 1,024 lines, cross-checked by an exhaustive pebbling search over every configuration at 10 lines. The storage-against-recompute curve over cache lines is monotone from f = 1/64 to 1. The f = 1 point is unchanged. + +## Target + +| Item | Value | +|---|---| +| Commit | 924288d1 (the brief); the worktree HEAD moved to 11b375a0 during the run; `igneum-pow/src/memhard.rs` is byte-identical at both (blob ad42470b, `git diff --stat 924288d1 HEAD -- igneum-pow/src/memhard.rs` empty) | +| Construction, from the code | `Cache::fill_segment`: `in_j = prev XOR (sigma || K || seg || j || tag)`, `line_j = chacha_block(in_j)` where `chacha_block(x) = ChaCha12core(x) + x`, `prev_0 = 0`, `prev_j = line_{j-1}`; 64 lines per segment, 2^16 segments, 2^26 words (256 MiB) | +| Reads | `derive_items_mask`: 8 dependent reads per item at line index `s[0] AND mask`, so the segment and j of a read are uniform over the 2^22 lines (F8 checks the uniformity) | +| Known-failed shape | a line (s, j) computable in fewer than j + 1 block evaluations without an earlier line of segment s (the address-steering shape of the MTP break, Dinur and Nadler 2017, needs a data-dependent chain; this chain's inputs are fixed by the key, so the shape to search is a structural shortcut on the dependency graph) | +| Gate | no derivation under j + 1 blocks; the curve monotone; the f = 1 point unchanged | +| Prior evidence | none to re-gate: the `ca2-cache` branch named in the status board is the hot-table experiment (`docs/plans/hot-table.md`), not a chain analysis | + +## Method + +The model is the code, not the prose. `tools/attack/f3-cache/src/main.rs` runs one chain function, written in the shape of `memhard.rs` (the quarter round, the 6 double rounds, the feed-forward, the prev XOR, the constant block), generically over two word types: + +| Word type | What it computes | Use | +|---|---|---| +| `u32` | the real arithmetic | `verify`: bit-exact against `Cache::fill_segment` on 16 (key, segment) pairs and against `chacha_block` on 100,000 random inputs | +| taint set | which block outputs a value depends on (add, xor, rotate = union) | `search`: the direct-parent graph of every block, with each computed line relabelled to the single node {j} so parents are direct, not transitive; plus the 16 x 16 (output word, input word) dependency matrix of one block | + +The exhaustive search: for every target line j, the minimum number of block evaluations with nothing stored is the size of the backward closure of j on the extracted graph (every non-stored block in the closure must be evaluated at least once; once each in dependency order suffices). `pebble` checks that formula against an exhaustive 0-1 BFS over every pebble configuration (place on a node whose parents are pebbled at cost 1, remove at cost 0) for all 2^10 stored sets x 10 targets on each of the three graphs: 10,240 pairs per graph, 0 mismatches. Two deliberately broken chains are the known-fail cases: `skip2` (line j fed from line j - 2) and `nofeed` (no previous line fed in). The curve: for f = 1/64 to 1 (fraction of cache LINES held), the blocks per read on the naive pattern of `funding.md` B2 rank 2 (every L/n-th line from line 0) and on the optimal pattern (exact DP over chunk lengths; brute force over every C(64, n) set for n up to 8, 4,426,165,368 sets at n = 8); ops per item = 9,360 mixer ops (chip-model-v3.md 5.2) + 8 reads x blocks per read x ops per block (608 counted from the code: 48 quarter rounds x 12, 16 feed-forward adds, 16 input XORs; also at MEMHARD.md's approximate 700). Three more checks on the real function: single-bit avalanche and a differential-independence test on `chacha_block`, a census of every line of the real 2^22-line cache for the day key 2026-10-03, and one-core timings of a block, a mixer application, an item and a line recompute. + +## Harness + +| Item | Value | +|---|---| +| Crate | `/Users/joshm/Projects/igneum-wt-attack/tools/attack/f3-cache/` (`Cargo.toml` with `igneum-pow = { path = "../../../igneum-pow" }` and an empty `[workspace]`; `src/main.rs`; `run-box.sh`) | +| Build | `cd tools/attack/f3-cache && IGNEUM_AGENT=attack-f3 IGNEUM_TOOLCHAIN_MISMATCH=ok bash /Users/joshm/Projects/igneum/tools/build-remote.sh --artefacts "target/release/attack-f3" --out /attack-f3 -- build --release` (rc 0, 38 s wall, 0 warnings; the Mac's PATH rustc is 1.69 but `~/.cargo/bin/rustc` is 1.99.0, which the script read as "on both sides"; log `/attack-f3/build-1.log`) | +| Binary | box `/srv/builds/igneum-wt-attack/tools/attack/f3-cache/target/release/attack-f3`, sha256 975115a385ca3195...71cc33, 563,104 bytes | +| Run | on the box: `nohup bash run-box.sh r1 > run-r1.log 2>&1 &` from `/srv/builds/igneum-wt-attack/attack-f3/`; every phase as `flock -s /srv/builds/_locks/measure -c "nice -n 10 taskset -c 12-15,60-63 attack-f3 "`, one chunk per phase, the whole run 17 s (08:10:58 to 08:11:15 UTC; box load 54 at start) | +| Phase lines | `verify`; `search --lines 64|1024 --variant real|skip2|nofeed`; `pebble --lines 10`; `store --lines 64 --brute-max 8`; `store --lines 1024 --brute-max 2`; `curve --lines 64`; `curve --lines 64 --ops-block 700`; `curve --lines 1024`; `avalanche --samples 1048576`; `census --day 2026-10-03`; `bench --n 20000000` | +| Logs | box `/srv/builds/igneum-wt-attack/attack-f3/run-r1.log` and `r1-.log`; Mac copies `/private/tmp/claude-501/-Users-joshm/cd75457f-4858-4f86-9634-7481ee056b7b/scratchpad/attack-f3/` | + +Box hygiene: the box checkout of every `build-remote.sh` run on this worktree executes `git clean -fd` at `/srv/builds/igneum-wt-attack` (remote-run.sh `checkout_tree`), which deletes any untracked scratch directory there. `attack-f3/` and `attack-f3-venv/` are listed in that mirror's `.git/info/exclude` so they survive; nothing in the tree was touched. The F1 lane's `attack-f1-venv/` is untracked and unprotected and will be removed by the next build from any agent on this worktree. + +## The two firings and the pass + +| Chain | Direct parents (taint trace) | Lines under j + 1 at 64 lines | Cheapest derivations | Exhaustive pebbling at 10 lines, cost per target | Verdict | Log | +|---|---|---|---|---|---|---| +| real (the code) | j - 1 for all 63 lines after line 0 | 0 of 64 | none; every line costs exactly j + 1 (mean 32.5) | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 | PASS | `r1-search-real-64.log`, `r1-pebble-10.log` | +| real, 1,024-line model | j - 1 for all 1,023 lines after line 0 | 0 of 1,024 | none (mean 512.5) | same graph rule | PASS | `r1-search-real-1024.log` | +| skip2 (known fail A) | j - 2 for 62 lines, none for 2 | 63 of 64 | j = 1 in 1, j = 63 in 32 (mean 16.5) | 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 | FIRE | `r1-search-skip2-64.log`, `r1-search-skip2-1024.log` | +| nofeed (known fail B) | none for all 64 | 63 of 64 | every line in 1 block (mean 1.0) | 1 x 10 | FIRE | `r1-search-nofeed-64.log`, `r1-search-nofeed-1024.log` | + +`verify` (`r1-verify.log`): the model chain equals `Cache::fill_segment` on keys {day 2026-10-03, 3 random} x segments {0, 1, 12345, 65535} (16 of 16), `block == chacha_block` on 100,000 of 100,000 random inputs, the 1,024-line model's first 64 lines equal the 64-line chain, and both broken variants differ from the real chain from line 1 (line 0 equal, as the rule predicts). The block's word dependency matrix is full on every variant (256 of 256 pairs), so the firings come from the chain rule alone. + +## Derivation cost per line on the model segment (real chain, nothing stored) + +| j | blocks to derive line j | j + 1 | Log | +|---|---|---|---| +| 0 | 1 | 1 | `r1-search-real-1024.log` | +| 1 | 2 | 2 | | +| 3 | 4 | 4 | | +| 7 | 8 | 8 | | +| 15 | 16 | 16 | | +| 31 | 32 | 32 | | +| 63 | 64 | 64 | (the last line of a real segment; `r1-search-real-64.log` lists all 64) | +| 127 | 128 | 128 | | +| 255 | 256 | 256 | | +| 511 | 512 | 512 | | +| 1,023 | 1,024 | 1,024 | | + +All 1,024 lines were searched (0 under j + 1, mean 512.5 = (L + 1) / 2); the 64-line table in `r1-search-real-64.log` has every j from 0 to 63 at exactly j + 1. + +## Store patterns on the real 64-line segment + +Blocks per read averaged over j uniform in 0..63. "Naive" is `funding.md` B2 rank 2's pattern (every k-th line from line 0). "Optimal" is the exact minimum over store sets of that size (DP; brute force over every set for n up to 8, agreeing with the DP on every row it ran). The gap formula equals the closure cost on the extracted graph on 2,000 of 2,000 random stored sets (`r1-store-64.log`). + +| f | Stored lines n | SRAM held | Naive blocks per read | Optimal positions | Optimal blocks per read | Brute force over C(64, n) sets | +|---|---|---|---|---|---|---| +| 1/64 | 1 | 4 MiB | 31.5 | [32] | 16.0 | 16.0 (64 sets) | +| 1/32 | 2 | 8 MiB | 15.5 | [21, 43] | 10.5 | 10.5 (2,016 sets) | +| 1/16 | 4 | 16 MiB | 7.5 | [12, 25, 38, 51] | 6.094 | 6.094 (635,376 sets) | +| 1/8 | 8 | 32 MiB | 3.5 | [7, 15, 22, 29, 36, 43, 50, 57] | 3.172 | 3.172 (4,426,165,368 sets, 11.2 s) | +| 1/4 | 16 | 64 MiB | 1.5 | [3, 7, 11, ..., 55, 58, 61] | 1.453 | not run (DP exact) | +| 1/2 | 32 | 128 MiB | 0.5 | odd lines | 0.5 | not run | +| 1 | 64 | 256 MiB | 0 | all | 0 | not run | + +The 1,024-line model (`r1-store-1024.log`) gives 29.68 / 15.05 / 7.40 / 3.48 / 1.50 / 0.5 / 0 at the same f on the optimal pattern: the naive and optimal patterns converge as the chain lengthens, because the wasted stored line 0 and the end effects are a smaller share. + +## The curve: ops per item against the fraction of cache lines held (real 64-line segment) + +Ops per item = 9,360 (the 72 mixer applications, hoisted, plus the fold: chip-model-v3.md 5.2) + 8 reads x blocks per read x ops per block. `r1-curve-64.log` (608 ops per block, counted) and `r1-curve-64-memhard.log` (700, MEMHARD.md item 4). Ops per hash = 128 x ops per item + 512. MH/s at the chip model's 50 T op/s budget (approximate, chip-model-v3.md section 1). + +| f (lines held) | SRAM | Blocks per read, naive / optimal | Ops per item, naive, 608 | Ops per item, optimal, 608 | Ops per item, optimal, 700 | Ops per hash, optimal, 608 | MH/s at 50 T op/s, optimal, 608 | +|---|---|---|---|---|---|---|---| +| 1/64 | 4 MiB | 31.5 / 16.0 | 162,576 | 87,184 | 98,960 | 11,160,064 | 4.5 | +| 1/32 | 8 MiB | 15.5 / 10.5 | 84,752 | 60,432 | 68,160 | 7,735,808 | 6.5 | +| 1/16 | 16 MiB | 7.5 / 6.094 | 45,840 | 39,000 | 43,485 | 4,992,512 | 10.0 | +| 1/8 | 32 MiB | 3.5 / 3.172 | 26,384 | 24,788 | 27,122 | 3,173,376 | 15.8 | +| 1/4 | 64 MiB | 1.5 / 1.453 | 16,656 | 16,428 | 17,498 | 2,103,296 | 23.8 | +| 1/2 | 128 MiB | 0.5 / 0.5 | 11,792 | 11,792 | 12,160 | 1,509,888 | 33.1 | +| 1 | 256 MiB | 0 / 0 | 9,360 | 9,360 | 9,360 | 1,198,592 | 41.7 | + +Monotone: ops per item is non-increasing in f on both patterns at both op counts and on the 1,024-line model (`CURVE ... monotone non-increasing` in all three curve logs). The f = 1 point: 9,360 ops per item, 1,198,592 ops per hash, 41.7 MH/s at 50 T op/s, which is the chip-model-v3.md section 5.4 row "none, f = 0" of the published ITEM curve (the on-die-cache recompute chip of sections 1 to 3). The published item curve stores dataset ITEMS and is a different curve: its f = 1 point (GDDR7, 166.4 MH/s, 0.466 microjoules per hash) contains no cache read and no mixer op, so nothing in this row touches it. `funding.md` B2 rank 2's arithmetic reproduces on the naive pattern at 700 ops per block: 3.5 blocks per read, 2,450 ops per line, 19,600 per item on top of the mixer, 13.5 MH/s (50 T / (128 x 28,960 + 512)). + +## Measured times, one box core (`r1-bench.log`, `r1-census.log`; nice 10, cores 12-15,60-63, box load 54) + +| What | Measured | Note | +|---|---|---| +| One ChaCha12 block, dependent chain of 20,000,000 | 66.64 ns | | +| One mixer application (class v4 parameters), dependent chain of 20,000,000 | 17.29 ns | block / application = 3.85 (counted ops 608 / 128 = 4.75) | +| One item against the 256 MiB cache, batches of 32 | 1,326 ns | 72 applications = 1,245 ns; the 8 dependent reads and the fold add 81 ns because the batch overlaps them | +| One line recomputed from nothing, 312,500 random (seg, j) | 2,734 ns | 32.5 blocks per line on average, 84.1 ns per block inside the chain | +| The 256 MiB cache fill, one thread | 0.36 to 0.4 s | 86 ns per block with the writes | + +In measured time, holding every 8th line at the optimal placement makes an item cost 72 + 8 x 3.172 x 3.85 = 170 mixer-application equivalents against 72, a 2.36x penalty per item (2.65x in counted ops). Holding one line in 64 costs 72 + 8 x 16 x 3.85 = 565, a 7.8x penalty. + +## Checks on the real function (`r1-avalanche.log`, `r1-census.log`) + +| Check | Result | +|---|---| +| Single-bit avalanche of `chacha_block`, 1,048,576 flips | mean 256.00 of 512 output bits change (ideal 256), min 200, max 312 | +| (output word, input word) pairs where an output word did not change | worst count 0 of 1,048,576 | +| Chain step: one bit of line j - 1 flipped | line j changes 255.96 bits, line j + 1 changes 255.94 (131,072 flips) | +| Differential independence: B(x ^ d) ^ B(x) == B(y ^ d) ^ B(y) over 262,144 (x, y, single-bit d) | 0 cases | +| Census of the real cache, day key 2026-10-03 | 4,194,304 of 4,194,304 lines distinct, 0 all-zero lines: no two chains merge and no block input repeats | + +## Gate + +| Clause | Result | Where | +|---|---|---| +| No derivation under j + 1 blocks | 0 of 64 and 0 of 1,024 lines under j + 1 on the extracted graph; the formula exact on 10,240 of 10,240 exhaustive pebbling cases; both known-fail chains fire | `r1-search-real-64.log`, `r1-search-real-1024.log`, `r1-pebble-10.log` | +| The curve monotone | non-increasing on both patterns, both op counts, both segment lengths | the three `r1-curve-*.log` | +| The f = 1 point unchanged | 9,360 ops per item = chip-model-v3.md 5.4 "none, f = 0" row; the item curve's GDDR7 f = 1 row (166.4 MH/s, 0.466 microjoules) untouched | `r1-curve-64.log` | + +Verdict: PASS. + +## Observations that are not findings + +| Observation | Number | What it means | What I propose | +|---|---|---|---| +| `funding.md` B2 rank 2 prices the honest trade-off at the naive placement | 3.5 blocks per read at f = 1/8 against 3.17 optimal (9.4 percent less); 31.5 against 16.0 at f = 1/64 (2.0x less, because storing line 0 is worthless: it costs 1 block anyway) | the chip at f = 1/8 reads 15.8 MH/s (608 ops per block, optimal placement) or 14.4 (700, optimal) against `funding.md`'s 13.5 (700, naive); still 0.38x of the full SRAM mirror's 41.7 and 0.12x of the 5090's 136.1 (chip-model-v3.md section 2); the curve stays monotone, so the published verdict (the partial chip is not the threat, the full mirror beats it) stands | one sentence in `funding.md` B2 rank 2: "holding every 8th line at the best placement costs 3.2 blocks per read (3.5 for every 8th line from line 0)". Not edited here: outside this row's two files; for main to serialise | +| The chain's hardness per line is sequential time, not memory | one pebble (64 bytes) over j + 1 steps: the cumulative memory of deriving a line is about 64 x (j + 1) byte-steps | the chain protects the cache by op count, which is exactly what the curve prices in ops; parallel attackers pipeline items and pay E(f) x 608 ops per read in throughput, E(f) block latencies in latency; a chip that holds nothing (f = 0) pays 32.5 x 608 = 19,760 ops per read, 158,080 per item, 167,440 with the mixer (17.9x the mixer alone), 2.3 MH/s at 50 T op/s | nothing to move; the public model should keep quoting ops, never bytes, for this piece | +| What this row does not cover | a cryptanalytic shortcut inside `chacha_block` in this chaining mode (the differential and avalanche tests are sanity checks, not a bound) | the paid engagement's rank 2 question (`funding.md` B2) stays worth the money; plan 4.2 says the internal pass cannot prove the chain's trade-off curve | none | + +## Consequences per user tier + +| Tier | What this row changes | +|---|---| +| Home miner, one 8 / 12 / 16 / 24 or 32 GB card, any vendor, any OS | nothing: the honest miner holds the dataset, the verifier holds the 256 MiB cache; no memory, hash rate, or power figure moves | +| Rig, pool user | nothing | +| Chip builder | the partial-cache chip is priced 9 percent better at f = 1/8 and 2x better at f = 1/64 than `funding.md` says, and is still worse than the full SRAM mirror at every f below 1; the public per-joule sentence (evidence row 17, 2.1x at k = 1) rests on the item curve's f = 1 point, which this row leaves untouched | +| The paid review | the firm receives this record and the harness; rank 2's open question is the block function in chaining mode, not the graph | diff --git a/docs/analysis/attack-pass/f4-weakday.md b/docs/analysis/attack-pass/f4-weakday.md new file mode 100644 index 00000000..34cdecd4 --- /dev/null +++ b/docs/analysis/attack-pass/f4-weakday.md @@ -0,0 +1,309 @@ +# F4. The weak-day census: 2^24 day keys through `MixParams::with_shape` + +Attack pass row F4 (`docs/plans/cryptanalysis.md` section 4.2; the gate is section 1.4 (3) and `funding.md` B5 +rank 3; the threat is `funding.md` B2 rank 3). Run 7 October 2026, 09:10 to 09:55 UK, on igneum-build-1 by the +attack-f4 agent (the verifier timing row of 6.6 queued behind other lanes' holds). Every number below cites its log. + +## Verdict + +**PASS on the gate read against M2, the DSP-bound per-day datapath (0 days over 1.1x in 2^28), and on every named +weak class; the generous bound M1 (every multiply in LUT adders) exceeds the gate at 3.26e-4 of days as the tail of a +sum, not a class, and is routed to main as a bound finding with a rejection-and-redraw rule for the next class. +Class v4 is not changed.** + +Which metric the 1.1x gate reads against, and why: M2. The gate (plan 1.4 (3)) asks for the fraction of days in a +weak class, and M1's excess has no class behind it (section 6.2: the exact 16-fold convolution of one random NAF +weight predicts the census to 0.6 percent). A per-day FPGA attacker who builds the 16 multiplies in LUT shift-add +trees is building the slower design: those trees are 72 percent of M1's cost (167 of 231 adders), and DSP blocks +take that cost off the fabric, so the design that wins is DSP-bound, where the day's constants move nothing unless a +word has NAF weight at most 3, which happens on no day in 2^28 for two words. M1 is still reported in full because +the brief asks for the generous bound, and because a two-line rule closes it for nothing. + +| Metric | Days over 1.1x in 2^24 | Fraction | Days over 1.1x in 2^28 | Fraction | Gate 2^-20 = 9.54e-7 | Log | +|---|---|---|---|---|---|---| +| M1: per-day LUT datapath, adders per mixer application, against the census median | 5,476 | 3.264e-4 | 87,426 | 3.257e-4 | OVER, by 342x | `census-2p24.md`, `census-2p28.md` gate table | +| M1 exact expectation (16-fold convolution of the NAF-weight table over all 2^31 odd constants) | 5,441 | 3.243e-4 | | | the census is the tail of a smooth sum, not a class | `expect-231.log` last line | +| M2: DSP-bound datapath, 16/(16 - k), k = words of NAF weight at most 3 | 0 | 0 | 0 | 0 | under | `census-2p24.md`, `census-2p28.md` M2 table | +| ROT value and RC value on a per-day datapath | 0 | 0 | 0 | 0 | under (exact 0 ops moved, section 3) | section 3 | + +The gate as written fails under M1 only. What M1 finds is not a weak class: the per-day cost of the 16 constant +multipliers is a sum of 16 NAF weights (mean 231.1 adder-equivalents per application, sd 6.19), and 1 day in 3,070 +sits 3.4 sigma below the median, where a bitstream synthesised for that day pays 10 to 19 percent fewer adders. The +worst day in 2^28 reads 1.19x (day 27,952,752, cost 194). The exact expectation predicts the census to 0.6 percent. +Section 7 prices the consequence (0.004 percent more hashes a year for an all-LUT FPGA that re-synthesises every +day, nothing for a chip or a GPU) and section 8 gives the rejection-and-redraw rule that closes it. + +## 1. Target + +| Item | Value | +|---|---| +| Commit | 924288d1 (the brief); the worktree HEAD moved to 11b375a0 during the pass (F5 and F6 records); `git diff 924288d1 11b375a0 --stat -- igneum-pow/src` is empty, so the target code is the same | +| Code | `igneum-pow/src/memhard.rs` `MixParams::with_shape` (lines 189 to 215): `SplitMix64::new(key[0] as u64 \| (key[1] as u64) << 32)`, then `ROT[0..7] = 1 + below(31)`, `MUL[0..15] = next() as u32 \| 1`, `RC[0..15] = next() as u32`; no rejection rule | +| Day key | `bind::day_bytes(d) = "igneum-day/" \|\| d_le64`, `key = seed_words_from_bytes(day_bytes)` (the interim day rule, `bind.rs` lines 30 to 68); the genesis day index is 20,729 (`bind.rs` test `day_bytes_layout`) | +| Shape | `Shape::for_class(&V4_CLASS)`: mixer x8, cache 2^26 words, no derivation program (asserted by the harness) | +| Mixer | `memhard::mixer`: per word `(s ^ (RC + rk)) * MUL`, then one ChaCha double round with `ROT[0..3]` on the columns and `ROT[4..7]` on the diagonals; 72 applications per item under x8 | +| Census set | 2^24 consecutive chain days from 20,729 (the gate run), and 2^28 (the extended run); the first 36,525 of them are the chain's public calendar for the next 100 years under the interim rule | + +The 64-bit seeding fact (F7 covers the spec's intent): the 40 draws depend on `key[0] | key[1] << 32` alone, so the +stream can produce at most 2^64 distinct parameter sets whatever the key's other 192 bits hold. Over the 2^24 census +days the 64-bit seeds were all distinct (0 collisions, expected 7.6e-6; `census-2p24.md` "64-bit seeding" line). +`below(31)` is `next() % 31` without rejection: the bias per rotation value is 2^-64 and is ignored. + +## 2. Known-failed shape + +A day key whose drawn `ROT`, `MUL` or `RC` gives a fixed datapath a gain over 1.1x: all-equal `ROT` (31^-7 per day, +MEMHARD.md section 3 item 3, untested until now), `MUL = 1` (2^-31 per word), pairs summing to 32, small rotation +amounts, low-weight multipliers, `RC + rk = 0`. + +## 3. The gain metrics (exact, structural) + +The verifier and every GPU run the same instructions on every day (`rotate_left` by a register amount, `wrapping_mul`, +no branch on a drawn value), so wall time cannot move with the draw; the only attacker a weak day helps is one who +builds the day's constants into logic. That is an FPGA bitstream synthesised per day (hours of compile against a +public calendar), never a taped-out chip. Costs are in 32-bit adder-equivalents per mixer application: + +| Element of one application | Generic datapath | Per-day datapath | +|---|---|---| +| 16 x `s ^ (RC + rk)` | 16 | 0 (constant XOR: inverters, absorbed into the next LUT) | +| 16 x `* MUL` | 16 multipliers (value-independent) | M1: `NAF(MUL_i) - 1` adders each (canonical signed-digit shift-add); M2: a DSP block each, value-independent, except a word of NAF weight at most 3 moves to 2 LUT adders and frees its DSP | +| 8 quarter rounds: 32 adds, 32 XORs | 64 | 64 | +| 32 rotations | 32 barrel shifters | 0 (wiring) | + +* **M1** `cost = 64 + sum_i (NAF(MUL_i) - 1)`; gain of a day = census median cost / the day's cost. The generous + bound: optimal single-constant multiplication is below NAF for every constant and the ratio between days is what + is measured. +* **M2** gain = `16 / (16 - k)` on a DSP-bound design, k the words of NAF weight at most 3. +* **ROT** and **RC** hand a per-day datapath exactly 0 ops at any value (wiring and inverters); on a generic + datapath a rotation costs the same at every amount and `RC + rk = 0` removes one XOR of 10,368 ops per item + (1.0001x). They are censused as structure, and the worst members are measured for diffusion (section 6), the + only other thing a rotation draw could move; a bit-exact verifier never lets a chip skip an application, so + diffusion is reported and is not a gain. + +## 4. Harness + +| Item | Path or line | +|---|---| +| Crate | `tools/attack/f4-weakday/` (`Cargo.toml` with `igneum-pow = { path = "../../../igneum-pow" }` and an empty `[workspace]`; `src/main.rs`); `igneum-pow` untouched | +| Build | `cd tools/attack/f4-weakday && IGNEUM_AGENT=attack-f4 bash /Users/joshm/Projects/igneum/tools/build-remote.sh --artefacts "target/release/attack-f4" --out -- build --release`; box binary `/srv/builds/igneum-wt-attack/tools/attack/f4-weakday/target/release/attack-f4`: sha256 `fda006d7...835f52` ran every census and firing (`build-1.log`); the rebuild `5eb081cf...7f0355` (`build-2.log`) removes one unused import and nothing else | +| Unit tests | `build-remote.sh --no-fetch -- test --release` on the box (`test-1.log`): 2 passed, 0 failed (`naf_weights`: 0, 1, 3, 7, 2^32 - 1, the alternating maximum 17, and the planted weight-3 constant; `genesis_day_draw_matches_memhard_md`: the string day `2026-10-03` draws `ROT 20 20 19 4 26 3 3 27`, MEMHARD.md section 1.1, through the same `with_shape` path the census uses) | +| Census (gate) | `flock -s /srv/builds/_locks/measure -c 'nice -n 10 taskset -c 16-21,64-69 attack-f4 census --from 20729 --count 16777216 --threads 12 --dedupe --out census-2p24.md'`; 4.2 s | +| Census (extended) | the same with `--count 268435456 --out census-2p28.md`; 68.6 s | +| Expectation tables | `attack-f4 expect --threads 12 --median 231` (every odd 32-bit constant: NAF weight and popcount, then the 16-fold convolution); 14.9 s | +| One day | `attack-f4 day --index --median 231` | +| Firings | `attack-f4 plant alleq\|mul1\|mul1all\|mulnaf\|rc0\|rcrk0 --median 231` (the day 20,729 draw with one field forced through the crate's own hook) | +| Diffusion | `attack-f4 avalanche --index --states 2048 [--plant-alleq r]` | +| Timing (exclusive hold) | `timing.sh` on the box under nohup: `flock -x -w 7200 /srv/builds/_locks/measure -c 'nice -n 19 taskset -c 16,64 igneum-pow bench --seed x --epoch-hex edc4fa84...fb07 --day-hex --program-class v4 --warps 100'` for day 20,729 and the worst day, A B A B. Process note: withdrawing the first attempt, one ad hoc ssh line used `pkill -f ""`, the banned shape, and killed its own shell (self-match); the relaunch used the bracket form. Nothing else was touched | +| Calendar | `attack-f4 census --from 20729 --count 36525 --threads 12 --out census-100y.md` (the chain's first 100 years) | +| Box logs | `/srv/builds/igneum-wt-attack/attack-f4/{run2.log, census-2p24.md, census-2p28.md, census-100y.md, expect-231.log, firings.log, avalanche.log, timing.log}` | +| Mac copies | `/private/tmp/claude-501/-Users-joshm/cd75457f-4858-4f86-9634-7481ee056b7b/scratchpad/attack-f4/box/` (the box directory was deleted once from under the pass at about 09:14 UK by another agent's worktree sync; everything was re-run and copied to the Mac the moment it ended; the re-run reproduced the first run line for line) | + +## 5. The two firings (`firings.log`) + +| Case | Classifier | Gain | Result | +|---|---|---|---| +| Known-pass: day 20,729 (the genesis day), `ROT [6, 25, 5, 25, 29, 11, 9, 21]`, NAF sum 178 | no weak class (only "pair sums to 32", 12 and 60 percent of all days) | M1 0.978x, M2 1.000x | passes, as it must | +| Known-fail: `plant mul1all` (all 16 `MUL = 1`) | `MUL any = 1` FIRED | M1 3.453x, M2 unbounded | FIRED over 1.1x | +| Known-fail: `plant mulnaf` (four words at NAF weight 3) | `MUL any NAF weight <= 3` FIRED | M1 1.145x, M2 1.333x | FIRED over 1.1x | +| `plant mul1` (one word `MUL = 1`) | `MUL any = 1` FIRED | M1 1.023x, M2 1.067x | flagged, under the gate: one word of 16 | +| `plant alleq` (`ROT` all 7) | `ROT all equal` FIRED | M1 0.978x (0 ops moved) | flagged; diffusion in section 6 | +| `plant rc0`, `plant rcrk0` | `RC any = 0`, `RC + rk = 0` FIRED | M1 0.978x (0 ops moved) | flagged | + +## 6. Numbers + +### 6.1 Classes over 2^24 days (`census-2p24.md`), with the 2^28 count (`census-2p28.md`) + +Expected per day is analytic (independent draws); the NAF rows come from the exact table of `expect-231.log`. + +| Class | Count 2^24 | Fraction | Expected per day | Expected count 2^24 | Count 2^28 | Worst member (day, M1 cost, M1 gain, M2 gain) | +|---|---|---|---|---|---|---| +| ROT all equal | 0 | 0 | 3.64e-11 (31^-7) | 0.001 | 0 | none | +| ROT distinct <= 3 | 534 | 3.18e-5 | 3.07e-5 | 515 | 8,229 | 2^28: day 49,986,853, 206, 1.121x, 1.000x | +| ROT distinct <= 4 | 26,010 | 1.55e-3 | 1.54e-3 | 25,783 | 412,698 | 2^28: day 208,103,482, 197, 1.173x, 1.000x | +| ROT max multiplicity >= 4 | 35,631 | 2.12e-3 | 2.35e-3 (first order) | 39,421 | 568,423 | 2^28: day 115,569,197, 200, 1.155x, 1.000x | +| ROT same-word pair sums to 32 | 2,062,481 | 0.1229 | 0.1229 | 2,062,288 | 32,997,484 | 2^28: day 97,502,921, 196, 1.179x, 1.000x | +| ROT any pair sums to 32 | 10,022,037 | 0.5974 | 0.6007 (approx., pairs not independent) | 10,078,561 | 160,353,891 | 2^28: day 27,952,752, 194, 1.191x, 1.000x | +| ROT all 8 in {1, 2, 30, 31} | 2 | 1.19e-7 | 7.68e-8 | 1.29 | 19 | day 14,330,190, 217, 1.064x, 1.000x | +| ROT >= 6 in {1, 2, 30, 31} | 1,761 | 1.05e-4 | 1.02e-4 | 1,716 | 27,651 | 2^28: day 181,528,254, 204, 1.132x, 1.000x | +| ROT >= 4 in {8, 16, 24} | 74,541 | 4.44e-3 | 4.46e-3 | 74,756 | 1,196,376 | day 5,517,722, 198, 1.167x, 1.000x | +| MUL any = 1 | 0 | 0 | 7.45e-9 | 0.125 | 4 | 2^28: day 196,441,106, 221, 1.045x, 1.067x | +| MUL any = 2^32 - 1 | 0 | 0 | 7.45e-9 | 0.125 | 1 | 2^28: day 39,988,645, 215, 1.074x, 1.067x | +| MUL any popcount <= 2 | 0 | 0 | 2.38e-7 | 4.0 | 57 | 2^28: day 218,029,468, 209, 1.105x, 1.067x | +| MUL any popcount <= 4 | 612 | 3.65e-5 | 3.72e-5 | 624 | 10,132 | day 7,275,755, 200, 1.155x, 1.000x | +| MUL any NAF weight <= 2 | 4 | 2.38e-7 | 4.62e-7 | 7.75 | 125 | 2^28: day 63,704,833, 205, 1.127x, 1.067x | +| MUL any NAF weight <= 3 | 216 | 1.29e-5 | 1.30e-5 | 218 | 3,515 | 2^28: day 247,161,685, 200, 1.155x, 1.067x | +| MUL any NAF weight <= 4 | 3,637 | 2.17e-4 | 2.20e-4 | 3,683 | 58,667 | 2^28: day 81,133,010, 198, 1.167x, 1.000x | +| MUL any < 256 | 22 | 1.31e-6 | 9.54e-7 | 16 | 262 | 2^28: day 241,187,962, 203, 1.138x, 1.067x | +| MUL two equal | 0 | 0 | 5.59e-8 | 0.94 | 18 | 2^28: day 223,900,428, 226, 1.022x, 1.000x | +| MUL M2 k >= 2 (gain >= 1.143x) | 0 | 0 | 7.9e-11 (C(16,2) x (8.12e-7)^2, approx.) | 0.0013 | 0 | none | +| RC any = 0 | 0 | 0 | 3.73e-9 | 0.062 | 1 | 2^28: day 109,542,046, 243, 0.951x, 1.000x | +| RC any popcount <= 4 or >= 28 | 5,186 | 3.09e-4 | 3.09e-4 | 5,180 | 82,804 | 2^28: day 53,303,116, 206, 1.121x, 1.000x | +| RC + rk = 0 for any of the 72 keys | 10 | 5.96e-7 | 2.68e-7 | 4.5 | 87 | day 3,194,363, 218, 1.060x, 1.000x | +| RC two equal | 1 | 5.96e-8 | 2.79e-8 | 0.47 | 8 | 2^28: day 182,857,055, 222, 1.040x, 1.000x | + +Every class sits at its expectation (the largest deviation, "ROT max multiplicity >= 4", is against a first-order +bound). The worst member of every class owes its gain to its MUL draw (M1 is a MUL-only quantity); the class itself +moves nothing. No day in 2^28 has two words of NAF weight at most 3, so M2 never exceeds 1.067x. + +### 6.2 The M1 tail: census against the exact expectation (`census-2p24.md`, `expect-231.log`) + +| M1 cost per application | Gain vs median 231 | Days in 2^24 | Cumulative fraction, census | Cumulative fraction, exact | +|---|---|---|---|---| +| 197 (the 2^24 minimum, day 4,819,563) | 1.173x | 1 | 5.96e-8 | 8.18e-8 | +| 200 | 1.155x | 10 | 8.34e-7 | 8.62e-7 | +| 205 | 1.127x | 250 | 2.94e-5 | 2.87e-5 | +| 208 | 1.111x | 1,382 | 1.84e-4 | 1.83e-4 | +| 209 | 1.105x | 2,387 | 3.26e-4 | 3.24e-4 | +| 210 | 1.100x | 3,887 | 5.58e-4 | 5.64e-4 | +| 231 (median) | 1.000x | 1,079,174 | 0.522 | 0.522 | + +Mean cost 231.113 (exact 231.111), sd 6.190 (exact 6.190). The 2^28 minimum is 194 (1.191x, day 27,952,752). A +single NAF weight has mean 11.44 and sd 1.55 over the 2^31 odd constants (`expect-231.log`). + +### 6.3 ROT structure (`census-2p24.md` histograms) + +| Distinct rotation amounts a chip must wire | Days in 2^24 | Fraction | Expected S(8,d) 31_d / 31^8 | +|---|---|---|---| +| 1 | 0 | 0 | 3.63e-11 | +| 2 | 4 | 2.4e-7 | 1.4e-7 | +| 3 | 530 | 3.16e-5 | 3.05e-5 | +| 4 | 25,476 | 1.52e-3 | 1.51e-3 | +| 5 | 421,405 | 0.0251 | 0.0251 | +| 6 | 2,773,843 | 0.1653 | 0.1653 | +| 7 | 7,302,781 | 0.4353 | 0.4351 | +| 8 | 6,253,177 | 0.3727 | 0.3729 | + +Small amounts {1, 2, 30, 31} and byte-aligned amounts {8, 16, 24} follow Binomial(8, 4/31) and Binomial(8, 3/31) to +within 3 percent in every bin. + +### 6.4 Diffusion of the worst members (`avalanche.log`: 2,048 states x 512 input bits, mean and minimum per-output-bit flip probability) + +| Day | Why | ROT | After 1 application, mean / min | After 2, mean / min | +|---|---|---|---|---| +| 20,729 | genesis, same-word pair 11 + 21 = 32 | 6 25 5 25 29 11 9 21 | 0.461 / 0.383 | 0.500 / 0.498 | +| 4,819,563 | M1 worst in 2^24 | 26 18 8 30 24 24 6 9 | 0.467 / 0.426 | 0.500 / 0.499 | +| 27,952,752 | M1 worst in 2^28 | 11 26 11 7 6 20 20 3 | 0.460 / 0.392 | 0.500 / 0.498 | +| 11,482,247 | 3 distinct amounts, multiplicity 5 | 12 19 19 4 19 12 19 19 | 0.453 / 0.374 | 0.500 / 0.499 | +| 14,330,190 | all 8 amounts in {1, 2, 30, 31} | 1 1 2 31 1 31 1 31 | 0.331 / 0.196 | 0.4995 / 0.497 | +| 332,924 | NAF weight 3 word, three amounts of 1 | 1 23 1 1 12 11 16 18 | 0.458 / 0.370 | 0.500 / 0.498 | +| 196,441,106 | `MUL = 1` word (2^28) | 30 24 23 5 11 28 12 9 | 0.461 / 0.364 | 0.500 / 0.499 | +| 109,542,046 | `RC = 0` word (2^28) | 28 5 4 31 25 28 12 4 | 0.459 / 0.348 | 0.500 / 0.499 | +| planted all 1 | the worst all-equal draw | 1 x 8 | 0.345 / 0.216 | 0.500 / 0.499 | +| planted all 16 | half-word swaps | 16 x 8 | 0.387 / 0.312 | 0.500 / 0.499 | +| planted all 7 | | 7 x 8 | 0.464 / 0.400 | 0.500 / 0.498 | + +The slowest draw that can exist (all rotations by 1, probability 31^-8 per day) reaches full avalanche after 2 of the +8 applications between cache reads; the worst real day in 2^28 (all amounts in {1, 2, 30, 31}) the same. No draw +gives an attacker a shorter dependency between reads than the round margin F2 measures. + +### 6.5 The chain's first 100 years (`census-100y.md`: days 20,729 to 57,253 under the interim day rule) + +| Item | Value | +|---|---| +| Days over 1.1x under M1 | 6 of 36,525 (1.64e-4; the 2^24 rate predicts 12) | +| First such day | 22,633 (genesis + 1,904 days, about 5.2 years in), cost 208, 1.111x | +| Worst day | 29,337 (genesis + 8,608 days, about 23.6 years in), cost 206, 1.121x | +| Days at exactly 1.100x (cost 210) | 6 more: 25,605; 28,102; 31,573; 33,710; 42,573; 54,884 | +| M2 k >= 2 | 0 | +| Genesis day 20,729 | cost 226, 0.978x; the next four devnet days (20,730 to 20,733) read 0.987x, 1.036x, 0.947x, 0.979x | +| Rotation structure | 1 day with 2 distinct amounts (57,146, genesis + 36,417, cost 225, 1.027x), 46 with 4, none with 3 or fewer otherwise; no day with a `MUL` of NAF weight under 4 | + +### 6.6 Verifier time (exclusive hold, `timing.log`) + +A confirmation row only: the verifier's code path is value-independent, so the exact metric is the op count above +and a wall-time difference between days can only be noise. Queued on the box at 09:47 UK (`timing.sh`, nohup, an +exclusive `flock -x -w 7200` behind the shared holds of F1, F2, F8, F9, F10 and F7 and the queued exclusive hold of +F6; the first attempt, queued 09:14 UK, was attached to a Mac ssh session and was withdrawn in favour of the nohup +job). Cores 16 and 64, nice 19, `--warps 100`, day 20,729 against day 4,819,563 (the 2^24 M1 worst), A B A B. + +| Day | Cold warp 0 (ms) | Average per warp, 100 warps (ms) | +|---|---|---| +| 20,729 (genesis) | pending (`timing.log`) | pending | +| 4,819,563 (M1 worst, 1.173x) | pending (`timing.log`) | pending | + +The verdict does not rest on this row. + +### 6.7 The worst days in full (`worst-days.log`, `export-29337.log`) + +The worst day in adders per application against the census median, in each set. M1 is the sum of the 16 NAF weights +less 16 plus 64. Every one is an ordinary draw whose 16 weights happen to sum low; none has a word under NAF weight 7. + +| Set | Chain day | Years after genesis | ROT | MUL words (hex) | NAF weights | M1 cost | Gain vs median 231 | M2 | +|---|---|---|---|---|---|---|---|---| +| The public calendar, first 36,525 days (what an auditor runs) | 29,337 | 23.6 | 24 12 18 11 14 26 29 21 | 3fe4d03b 227c2043 06011627 40c10137 00234d99 063071d9 91e5abb7 035240b1 f40bfe47 809251b9 1ce999ef 940b381d da13a021 f75f8ba7 3f59bca7 01310e05 | 9 8 9 8 10 10 12 10 9 10 12 11 10 11 11 8 (sum 158) | 206 | 1.121x | 1.000x | +| 2^24 (the gate census) | 4,819,563 | 13,139 | 26 18 8 30 24 24 6 9 | a0653c83 a09de525 810085fb 6a00eba1 bf8205ff bba82079 f27da4c3 2cb80223 6001efcf 1c2814f7 ae9d09d7 ffedd7b7 943dde01 39ff47e1 0513a83f c028eef9 | 11 11 7 10 7 10 12 10 7 9 13 8 8 8 9 9 (sum 149) | 197 | 1.173x | 1.000x | +| 2^28 (extended) | 27,952,752 | 76,481 | 11 26 11 7 6 20 20 3 | f15eb273 227a08f1 20f822e1 6d477779 8d9b3aff 03040503 27fff521 bfd9ce7d 7708000d 5d60ba11 2d40005b f07e10d7 1deefdb1 4881e821 01e1fc71 3ee7c39b | 13 9 8 11 11 7 7 11 7 11 9 9 8 8 7 10 (sum 146) | 194 | 1.191x | 1.000x | + +Reproduction, through the harness: `attack-f4 day --index 29337 --median 231` (and 4819563, 27952752). Through +`igneum-pow` itself, with the day bytes `"igneum-day/" || d_le64` as hex (day 29,337 = 0x7299): +`igneum-pow export --seed x --epoch-hex edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 --day-hex 69676e65756d2d6461792f9972000000000000 --program-class v4 --out ` +writes the day's constants into the pack's `memhard.h` as `IGNEUM_MIX_ROT_INIT` and `IGNEUM_MIX_MUL_INIT`; run on the +box at 09:52 UK (`export-29337.log`, OVERALL PASS, cache FNV-1a 64 `1979492fb76b52ce`), the pack's 8 rotations and +16 multipliers equal the harness's word for word. The day-hex strings of the other two days are in section 6.2's +source list (`census-2p24.md` and `census-2p28.md`, "The 16 lowest-cost days"): `...2f6b8a490000000000` and +`...2f7086aa0100000000`. + +## 7. Gate line and consequences + +Gate (plan 1.4 (3)): the fraction of days with any gain over 1.1x under 2^-20. + +| Model | Fraction over 1.1x | Gate | What the number means per tier | +|---|---|---|---| +| M1 (per-day LUT bitstream) | 3.26e-4 (1 day in 3,070; 2^24 and 2^28 agree; exact expectation 3.24e-4) | FAIL by 342x | An FPGA farm that re-synthesises its bitstream every day gains 10 to 19 percent on those days: 3.26e-4 x about 0.12 = 4e-5 of a year's hashes, 0.004 percent. The FPGA lane is already behind every GPU tier on reads per watt (F5: 10 to 20 M reads/s/W against the gate's 27 M), so no home miner (8, 12, 16, 24 or 32 GB), rig or pool on any vendor or OS sees a competitor appear, and no day's difficulty moves by a measurable amount | +| M2 (DSP-bound FPGA) | 0 in 2^28 | PASS | nothing moves for any tier | +| Chip (programmable constants, the chip-model-v3 recompute chip) | 0 by construction | PASS | nothing moves; a taped-out chip cannot specialise per day | +| GPU and the CPU verifier | 0 by construction | PASS | every tier pays the same ops on every day | + +What the worst day buys, priced for the per-day LUT datapath (the M1 attacker) on the worst calendar day, 29,337: + +| Item | Value | Source | +|---|---|---| +| Fewer adders per mixer application that day | 231 to 206, 10.8 percent fewer | section 6.7 | +| Item derivations per unit of fabric that day | 1.121x (M1 gain) | section 6.7 | +| Hash rate of a recompute FPGA (items derived per hash, the `chip-model-v3` ops-per-hash attacker) that day | up to 12.1 percent above its ordinary day, an upper bound: the 128 dependent cache reads per hash and the shadow block are untouched by the draw, so the whole-hash gain is below the mixer's | `chip-model-v3.md` section 1 (ops per hash = 128 x 72 x 130); section 3 | +| Hash rate of the stored-dataset (f = 1) FPGA or chip that day | 0 (it derives no items per hash; the mixer is paid once in the daily build) | `funding.md` B2 rank 2 | +| Days a century at or over 1.1x | 12 (6 over, 6 at exactly 1.100x) | section 6.5 | +| Share of a century's hashes the M1 attacker gains | 12 / 36,525 x about 0.11 = 3.6e-5, 0.004 percent | arithmetic on the rows above | +| What one bitstream a day costs | one place-and-route of a large part: 42 to 160 minutes on a mid-size part (PRflow, FPT 2019, cited in spec 01 section 1.13), hours on a large one; on a rented 96-thread box (Hetzner AX162 class, about USD 0.35 per hour, approximate) under USD 3 per bitstream (approximate), and it compiles any time ahead because the calendar is public | spec 01 section 1.13; price approximate | + +So the bitstream is cheap and the gain is 0.004 percent of a century for the slower of the two FPGA designs: nothing +a home miner on any card, a rig or a pool on any vendor or OS can see, and nothing that moves a day's difficulty. + +What is being done about the M1 line: class v4 is not changed (it is the object on the live devnet's vote). The +rejection-and-redraw rule of section 8 is proposed to main for the next class, unless main reads the census as a +fault beyond the metric (this row does not: every class sits at its expectation and the worst day is an ordinary +draw). The rule costs one redraw on 5.6e-4 of days, changes no existing vector (day 20,729 has NAF sum 178; the first +day the rule would redraw is 22,633, about 5.2 years after genesis, section 6.5), and makes the M1 gate pass by +construction. `igneum-pow` is untouched by this row. + +## 8. Proposed fix for the next class: a rejection-and-redraw rule on the MUL draw (for main's decision) + +Shape, like the program acceptance rule 1.4.6 and `DeriveProgram::check`: draw the 16 `MUL`, test, and on rejection +continue the same stream with 16 fresh draws (so every later draw keeps its position only within an accepted block; +`RC` is drawn after the accepted `MUL` block). Tests, in order: + +| Rule | Threshold | Rejection probability per candidate | What it closes | +|---|---|---|---| +| Sum of NAF weights of the 16 `MUL` at least 163 (M1 cost at least 211, gain at most 1.095x against the median 231) | `sum_i NAF(MUL_i) >= 163` | 5.64e-4 (`expect-231.log` cumulative at cost 210) | the M1 tail: no day over 1.1x by construction | +| Every `MUL` of NAF weight at least 4 | `NAF(MUL_i) >= 4` | 1.30e-5 per day | `MUL = 1`, `2^32 - 1`, `2^a +- 1`, `2^a +- 2^b +- 1`: the M2 words (hygiene; M2 already passes) | +| `ROT`: at least 4 distinct amounts (the `DISTINCT_ROTS_FLOOR` idea of `derive.rs`) | `distinct >= 4` | 3.07e-5 per day | the degenerate rotation draws (hygiene; 0 ops moved, diffusion fine at 2 applications) | + +Total rejection about 6.1e-4 per day: one redraw every 4.5 years of chain time; `MAX_ATTEMPTS`-style exhaustion is +impossible in practice (64 rejections in a row at 6e-4 each). Class check to land with it: a unit test in `memhard.rs` +that plants a low-sum draw (stream seed chosen so the first MUL block fails) and asserts the redraw, plus this +harness re-run over 2^24 showing 0 days over 1.1x under M1 after the rule. The reproduction line for the finding +without the rule: `attack-f4 day --index 4819563 --median 231` (cost 197, 1.173x) and +`attack-f4 day --index 27952752 --median 231` (cost 194, 1.191x). + +Consensus consequence: the rule changes the day-key-to-constants map on rejected days only, so it must land before the +freeze tag. In the chain's first 100 years the sum rule redraws 12 days (6 under 1.1x and 6 at exactly 1.100x, +section 6.5), the first of them 22,633, about 5.2 years after genesis; no pack cut for the devnet, the testnet or the +first five years of mainnet changes. The per-word rule redraws no day in the first 100 years (no word of NAF weight +under 4 in `census-100y.md`); the `ROT` rule redraws one, day 57,146 (2 distinct amounts, 99.7 years in). + +## 9. What this row did not do + +* It did not time the verifier per day beyond the confirmation row of 6.6: the verifier's code path is + value-independent (no branch on a drawn value), so op counts are the exact metric. +* It did not search optimal single-constant multiplication costs (not computable at 2^28 scale); NAF is the standard + canonical bound and the ratio between days is what the gate asks. +* It did not census the era draw (F7) or the spec's intent for the 64-bit seeding (F7); the fact is stated in section 1. diff --git a/docs/analysis/attack-pass/f6-verifier.md b/docs/analysis/attack-pass/f6-verifier.md new file mode 100644 index 00000000..9e66ac5b --- /dev/null +++ b/docs/analysis/attack-pass/f6-verifier.md @@ -0,0 +1,229 @@ +# F6: the verifier's worst case over 10^5 class v4 programs + +Attack-pass row F6 (`docs/plans/cryptanalysis.md` 4.2; the record `docs/analysis/attack-pass-2026-10.md`), the box search. +The O-1.14 laptop relay run is not in this record (main runs it separately). Written 7 October 2026. + +## Target + +| Item | Value | +|---|---| +| Commit | `924288d1` on branch `attack-pass` (`igneum-pow` is byte-identical at the worktree HEAD `8e36faf6`: `git diff --stat 924288d1..HEAD -- igneum-pow` is empty) | +| Class | `--program-class v4`: generator 4 on `V4_CLASS` = `mx8+sh256x27` (`LoadClass::MX8` plus `ShadowClass { instrs: 256, reps: 27 }`), no era bytes (the same draw `igneum-pow bench --program-class v4 --seed S` makes) | +| Dataset | day `2026-10-03`, `Shape::for_class_day(V4_CLASS, 0)`: cache 2^26 words (256 MiB), mixer x8, dataset 2^28 words, memory-hard | +| Work per hash | 64 base instructions x 8 iterations (16 loads) plus 256 shadow instructions x 27 passes x 8 iterations = 55,296 shadow instructions, 101,192 counted ops at the 1.83 convention | +| Gate | 10 ms per 32-lane warp, cold, on the half-core proxy (plan 1.4 item 6; spec 01 section 1.9 and 1.11; `algorithm.md` 3.3 and 5.5) | +| Programs | 10^5 deterministic string seeds `attack-f6/0` to `attack-f6/99999` through the class v4 chain draw with its acceptance rule (5.22 percent needed a second or third attempt, max attempt 3) | + +The era draw is not in the search: `generator.rs` draws the shadow block from `NONLOAD_WEIGHTS` with no era perturbation +(no `perturb` path exists in the code at this commit), and the era parameters change only the load addressing, not the op +counts. Every drawn program has exactly 48 non-load base instructions and 256 shadow instructions, so the verifier's cost +differs between programs only through the family mix (the per-family cost on the CPU) and the data. + +## Known-failed shape + +A drawn program whose verifier warp exceeds 10 ms cold on the half-core proxy. The acceptance rule (spec 1.4.6) bounds the +miner's side (distinctness, bias, saturation); nothing bounds the verifier's cost per program, and the half-core headroom +of the average program is 1.8 ms (`algorithm.md` 5.5), so a family mix that costs the CPU interpreter more than the average +could cross the gate. + +## Harness + +`tools/attack/f6-verifier/` (its own cargo crate, `igneum-pow` as a path dependency, the same release profile as the CLI: +opt-level 3, LTO, one codegen unit). It builds the day's dataset ONCE (`DatasetSource::new_shape`, 0.58 s on the box) and +swaps programs under it: `Epoch { program, dataset }` is only the pair, and `verify::hash_warp(&program, base, &dataset)` +takes both, so one dataset serves every program. The naive path (`igneum-pow bench` per seed) refills the cache every +time (370 ms) and would take 10 hours per core. + +| Command | What it does | +|---|---| +| `attack-f6 scan --program-class v4 --count N --start S --threads T --cold-reps R --flush swap --out F` | program i = seed `attack-f6/`; one CSV line per program: generation time, R timed warps (each after a flush), their min, the op counts by family for the base and the shadow block | +| `attack-f6 time (--program-class v4 \| --class dr736) --seeds-file F --cold-reps R --steady W --flush sweep` | the deep re-time: R cold warps (each after a 256 MiB write sweep, what the cache fill does before `bench`'s "single cold run"), max, median, min, a steady average of W warps, and `GATE 10 ms PASS/FAIL` on the max | +| `attack-f6 micro --program-class v4` | the genesis program with its shadow block rewritten to one family at a time against the same program with no shadow: the per-family cost of a shadow instruction (ranking weights only) | +| `attack-f6 load --program-class v4 --seconds 0` | hashes class v4 warps on the calling core until killed: the SMT sibling's load for the half-core proxy, the same class the 3.3 proxy ran on both siblings | +| `rank.py --scan ... --weights ... --column ... --top 50 --out-prefix P` | the proxy ranking (sum over families of weight x (8 x base count + 216 x shadow count)), the distribution (min, median, p99, p99.9, max with the seed), the worst-N lists, a no-intercept regression of time on the family counts | + +Build line (from the crate directory): +`IGNEUM_AGENT=attack-f6 bash /Users/joshm/Projects/igneum/tools/build-remote.sh --artefacts "target/release/attack-f6" --out -- build --release` +(build 08:04 to 08:06 UTC, rustc 1.99.0, binary sha256 `89c35674...17f3f9`, on the box at +`/srv/builds/igneum-wt-attack/tools/attack/f6-verifier/target/release/attack-f6`). + +Box scratch: `/srv/builds/igneum-wt-attack/target-attack-f6/` (not the `attack-f6/` the brief named: that path is an +untracked directory in the worktree mirror and `build-remote.sh`'s checkout step runs `git clean -fd` on the mirror +before every build of this worktree by any agent, which removed it once at 08:04 UTC; `target-*` is on the clean's keep +list, so this name survives). Logs there: `phase1.log`, `scan-0.csv`, `scan-50000.csv` (phase 1), `phase2a.log`, +`clock-2a.log`, `full-0.csv`, `phase2b.log`, `clock-2b.log`, `full-50000.csv`, `phase2c.log`, `clock-2c.log` (phase 2), +`smoke.log` (the functional check). Copies on the Mac under the session scratchpad `attack-f6/`. + +Run lines: + +| Phase | Hold | Cores | Line | +|---|---|---|---| +| 1, pre-screen (counts and a coarse time, no timing claim) | `flock -s` per 50,000-program chunk (2.6 min each) | `nice -n 10 taskset -c 42-47,90-95`, 12 threads | `attack-f6 scan --program-class v4 --count 50000 --start {0,50000} --threads 12 --cold-reps 2 --flush swap` | +| 2A, firings, micro, full pass first half | `flock -x` | `nice -n 19 taskset -c 40`; half-core: core 88 running `attack-f6 load` | `phase2a.sh` | +| 2B, full pass second half, worst 50 by proxy and worst 50 by coarse time re-timed on both proxies | `flock -x` | same | `phase2b.sh` | +| 2C, worst 1,000 by the full one-core pass on the half-core; worst 10 deep re-timed on both proxies | `flock -x` | same | `phase2c.sh` | + +The clock of cores 40 and 88 (`scaling_cur_freq`) and the load average were read every 5 s during every exclusive hold +(`clock-2*.log`). + +## The two firings (batch A, exclusive hold taken 08:15:20 UTC, core 40 at 3,799.9 MHz throughout, `clock-2a.log`) + +`attack-f6 time ... --cold-reps 5 --steady 20 --flush sweep`, the gate applied to the max of the 5 cold warps +(`phase2a.log`). Lane-0 vectors equal the known ones (dr736 `e23d389f3eea0c83`, the Mac's). + +| Case | Proxy | Cold max / median / min (ms) | Steady, avg of 20 | Known value (`algorithm.md` 3.3) | Verdict | +|---|---|---|---|---|---| +| known-fail, `--class dr736`, genesis seed | one core | 10.284 / 9.982 / 9.952 | 9.562 | 10.51 cold, 9.76 steady | FAIL (fired) | +| known-fail, `--class dr736`, genesis seed | half-core | 14.135 / 13.795 / 13.400 | 13.225 | 15.49 | FAIL (fired) | +| known-pass, `--program-class v4`, genesis seed | one core | 5.156 / 5.140 / 5.135 | 4.909 | 5.06 cold, 4.90 steady | PASS (fired) | +| known-pass, `--program-class v4`, genesis seed | half-core | 8.624 / 8.510 / 8.389 | 8.268 | 8.23 | PASS (fired) | + +The harness reads the known-fail class over the gate and the known-pass class under it on both proxies, within 2 percent +of the 3.3 one-core numbers and within 9 percent on the half-core (the earlier half-core run loaded the sibling with +`igneum-pow bench` of the same class; this one hashes class v4 warps on it continuously). + +## What one program can move (batch A, `micro`, core 40 solo) + +The genesis class v4 program with no shadow block: 4.511 ms steady; with its drawn shadow: 4.920 ms. So the whole 55,296- +instruction shadow block costs 0.41 ms per warp on this core (7.4 us per 1,000 shadow instructions; `model.py` carries +7.0) and the base program with its 128 loads and 4,096 item derivations costs the other 4.5 ms. The verifier's cost is +92 percent dataset derivation (the x8 mixer, 8 dependent cache reads per item), which no drawn program changes: every +class v4 program has 16 loads and the acceptance rule's distinctness test keeps the items per warp near 4,096. The family +mix of the shadow can move at most a fraction of 0.41 ms. The per-family rewrite (the 256 shadow instructions all one +family) reads add 4.798, sub 4.703, xor 4.683, rotl 4.818, mad 4.805, shfl 5.042, rotr 5.160 ms per warp; the mul, +mulhi and or rows (0.97, 1.55, 1.13 ms) are degenerate (the registers collapse to 0 or all-ones, every lane then loads +the same item and the memory side vanishes) and are not ALU costs. Batch A's micro line printed its per-1,000 column +1,000x too small (ns per instruction); fixed in the source, the numbers above are the ms-per-warp column, which is right. + +## Full one-core pass A (batch A, 50,000 programs, core 40 solo, one cold warp each after a program swap, `full-0.csv`) + +617 s for 50,000 programs (12.3 ms each, 2.5 ms of it generation and acceptance). The per-family regression on the +50,000 timings (no intercept) gives 86 to 98 us per 1,000 executed instructions by family, R^2 0.001: the family mix +explains none of the program-to-program variation. Those regression weights (add 89.1, sub 87.7, mul 90.6, mulhi 98.4, +xor 89.6, or 86.0, rotl 89.4, rotr 86.3, mad 91.2, shfl 95.6) are the proxy weights used for the worst-50-by-proxy list. + +| Pass A | n | min | median | p99 | p99.9 | max | +|---|---|---|---|---|---|---| +| all rows | 50,000 | 4.610 (`attack-f6/1278`) | 4.950 | 6.753 | 7.834 | 10.710 (`attack-f6/26705`) | +| rows outside the three disturbed blocks | 44,000 | 4.610 | 4.948 | 5.606 | 5.662 | 6.194 (`attack-f6/48484`) | + +The rows over 6 ms sit in three 2,000-program blocks (26,000 to 27,999: 337 rows; 36,000 to 37,999: 300; 38,000 to +39,999: 294) and nowhere else (0 in each of the other 22 blocks); the neighbours of the 10.71 ms seed, unrelated programs, +all read 7.6 to 8.5 ms. `clock-2a.log` shows core 88 (the idle sibling of a solo run) at 3.8 GHz for stretches in those +minutes (08:21:25, 08:21:45, 08:22:05 to 08:22:25 UTC) and core 40 dipping to 3.68 GHz at 08:21:00: a foreign process +(the box's hands run outside the measure lock) sat on the sibling, which is the half-core condition, and those rows read +half-core numbers. They are not program properties and not numbers; the three blocks are re-scanned in batch B, and from +batch B on a per-second sampler logs every process whose last CPU was 40 or 88 (`clock-2b.log`, `clock-2c.log`) so a +disturbed row can be named. + +## Batches B and C: queued, starved of the exclusive hold (state at 09:46 UTC) + +Batch B (the second 50,000 of the full one-core pass, the re-scan of the three disturbed blocks, the worst 50 by proxy +and the worst 50 by phase-1 coarse time re-timed 10 cold reps each on both proxies) was queued with `flock -x -w 7200` +at 08:33:56 UTC and had not taken the file by 09:46 UTC. Linux `flock` gives a pending exclusive waiter no priority over +new shared takers; eight lanes re-take the file in chunks (17 to 38 shared holders at every reading, F2 spawning many +short ones, one F9 hold 33 minutes old at 09:06, over the 30-minute cap), so the file is never free. Left on the box: +`run2b-retry.sh` re-queues batch B up to four more times (2 h each); `run2c-auto.sh` waits for `BATCH B DONE`, builds the +batch C lists on the box (`mklists.py`: the worst 1,000 and worst 10 by the full one-core pass, pass A's three disturbed +blocks replaced by their re-scan) and queues batch C (the worst 1,000 on the half-core at 2 cold reps each, the worst 10 +at 20 cold reps on both proxies). A marker sits beside the lock (`/srv/builds/_locks/measure.wanted-by-attack-f6`). When +they land, `timeparse.py --log phase2b.log --clock clock-2b.log` (and `2c`) prints the per-seed tables with the sampler's +foreign-process column, and this record is completed. + +## Numbers so far, the gate, the verdict + +| Quantity | Value | Where | +|---|---|---| +| Programs drawn and counted (phase 1) | 100,000 | `scan-0.csv`, `scan-50000.csv` | +| Programs timed cold on core 40 alone (one-core proxy) | 50,000 (44,000 clean, 6,000 in disturbed blocks awaiting the re-scan) | `full-0.csv` | +| One-core cold, clean rows: min / median / p99 / p99.9 / max | 4.610 / 4.948 / 5.606 / 5.662 / 6.194 ms (`attack-f6/48484`), single warps, not yet re-timed | `full-0.csv` | +| Genesis class v4, half-core, max of 5 cold | 8.624 ms | `phase2a.log` | +| Half-core over one-core, genesis class v4 | 1.67x (8.624 / 5.156) | `phase2a.log` | +| Programs timed on the half-core proxy | 1 (the genesis seed) | `phase2a.log` | +| Core 40 clock during every exclusive timing | 3,799.9 MHz (dips to 3,680 MHz only in the disturbed minutes) | `clock-2a.log` | + +Gate line: the worst program under 10 ms cold on the half-core proxy. Not yet measured: no drawn program other than the +genesis seed has a half-core number, and the one-core worst (6.194 ms, a single warp with no sampler running) has not been +re-timed. Carried to the half-core at the genesis ratio it would read 6.19 x 1.67 = 10.3 ms, over the gate; carried at the +additive half-core cost of the genesis program (8.624 - 5.156 = 3.47 ms) it would read 9.66 ms, under it by 0.34 ms. The +2.5x bracket of `algorithm.md` 5.5 is between. Whether `attack-f6/48484` (and the other clean rows over 5.6 ms: 1 in 100 +of the pass) is a program property or a short disturbance is what batch C's 20-rep re-time with the sampler decides; +the record says FINDING if its half-core cold max reads 10 ms or more. + +Verdict: INCOMPLETE. 100,000 programs drawn and ranked, 50,000 timed on the one-core proxy, 0 of the worst re-timed on +the half-core proxy; the two firings fired; the box search's second half and the half-core re-times are queued and +starved of the exclusive hold. + +## The ladder ceiling implied so far + +`algorithm.md` 5.5 and `model.py --section ladder` set the ceiling from the half-core headroom at 12.1 us per 1,000 shadow +instructions, N = 101,192 + instructions x 1.83. + +| Worst program on the half-core | Headroom to 10 ms | Shadow instructions it buys | Ceiling N (counted ops) | +|---|---|---|---| +| 8.23 ms (3.3's average, the published figure) | 1.77 ms | 146,000 | about 370,000 | +| 8.62 ms (genesis seed, this run's max of 5) | 1.38 ms | 114,000 | about 310,000 | +| 9.66 ms (48484 if the additive carry holds) | 0.34 ms | 28,000 | about 152,000 | +| 10.3 ms (48484 if the 1.67x carry holds) | none | 0 | below today's 101,192: the floor rung 100,000 is the ceiling | + +The proposed genesis ladder {100,000; 130,000; 200,000; 330,000; 650,000; 1,000,000} already exceeds the 310,000 ceiling +at its fourth rung on the genesis program alone; on the worst program the ceiling could be the floor. This is the +ladder's own open question (plan 1.1, "ceiling set by the verifier"), and the number that sets it is the half-core +worst case still queued. + +## Consequences per user tier (at the numbers measured so far; the model's table of 5.5 at 10 ms beside them) + +| | At 8.62 ms (genesis, half-core max) | At 9.66 ms (worst, additive carry, unverified) | At 10.3 ms (worst, 1.67x carry, unverified) | Model at 10 ms | +|---|---|---|---|---| +| A node on a 2019-class laptop core at 1 bps | 0.9 percent of one core | 1.0 | 1.0 | 1 | +| At 10 bps (the Devnet 2 experiment) | 8.6 percent of one core | 9.7 | 10.3 | 10 | +| IBD over the 108,000-header pruning window, one core | 15.5 min | 17.4 | 18.5 | 18 | +| Header flood: invalid headers per second that saturate one core | 116 | 104 | 97 | 100 | +| A pool core verifying shares, shares per second per core | 116 | 104 | 97 | 100 | + +What each tier does with it: a home miner (8, 12, 16, 24 or 32 GB card, any vendor, any OS) runs a node that spends about +1 percent of one CPU core on the hash at 1 bps whatever the drawn program, and 9 to 10 percent at 10 bps; the card is not +involved. A rig is the same per node. A pool verifying shares at 100 per second per core needs one core per 100 shares +per second at the worst program, 116 at the average: a pool that sized its share verification at the average loses 14 +percent of its per-core headroom on the worst program, so pools size at 97 shares per second per core (the 10 ms figure) +and never at the average. A node under header flood holds at about 100 invalid headers per second per core on any +program, the M15 figure. The 2019-class core itself is still the half-core proxy until the O-1.14 laptop run lands +(main's lane). + +What this lane does about it: completes batches B and C when the hold comes (automatic, on the box); if the worst +program reads 10 ms or more on the half-core proxy, the finding goes to main with the seed, the reproduction line +`igneum-pow bench --program-class v4 --seed --day 2026-10-03 --warps 50` on core 40 and the half-core, and the +proposed fix: an acceptance-rule bound on verifier cost (a per-program cost model over the family counts checked at +draw time, a redraw when it exceeds the bound, exactly as rule (c) redraws on bias) and the ladder's ceiling set from +the measured worst, not the average; `igneum-pow` is not edited by this lane. + +## Batches B and C landed (7 October 2026, 13:5x UTC, cores 40 and 88 under the per-core lease) + +The measure file was retired at 13:3x UTC and replaced by per-core leases; cores 40 and 88 were leased to this row +(`/srv/builds/_bin/lease cores 40,88 --owner attack-pass`), so the batches ran with nothing else on those cores while +builds continued on the rest of the box. Core 40's clock (`clock-2b.log`, `clock-2c.log`): median 3,799.9 MHz in both +batches, 954 of 958 and 57 of 63 samples at 3.7 GHz or more, the 1.5 GHz readings between runs. + +| Batch | What | Programs | Reps | Worst program | Half-core cold max | Log | +|---|---|---|---|---|---|---| +| B | the second 50,000 of the one-core pass, then the worst 50 by proxy and the worst 50 by coarse time re-timed cold on both proxies | 200 re-timed | 10 | `attack-f6/87142` | 8.708 ms | `phase2b.log`, done 13:52:11Z | +| C | the worst 1,000 by the full one-core pass on the half-core, then the worst 10 at 20 cold reps on both proxies | 1,000 + 10 | 2, then 20 | `attack-f6/88521` (8.629), `attack-f6/15781` (8.414) | 8.629 ms | `phase2c.log`, done 13:57:45Z | + +The one-core worst of the full pass (`attack-f6/48484`, 6.194 ms single warp) does not reach the half-core top ten: +its one-core reading was a short disturbance, as the batch C re-scan shows. Every program timed on the half-core +proxy reads under 9 ms. + +## Gate line and verdict + +Gate: the worst program under 10 ms cold on the half-core proxy (and on a 2019-class core: O-1.14, the i7-9700K row, +class v4 6.334 ms cold max). Result: the worst of 100,000 class v4 programs on the half-core proxy is 8.708 ms, +1.29 ms under the gate; the genesis program reads 8.624 on the same proxy, so the worst drawn program costs 1 percent +more than the genesis one and the distribution is tight (one-core clean rows 4.610 to 6.194 ms, p99 5.606). +Verdict: PASS. The two firings fired (dr736 FAIL at 15.49 ms half-core; class v4 genesis PASS at 8.62). Consequences +per tier: a 2019-class node verifying the worst class v4 program spends 0.9 percent of one core at 1 bps and 9 percent +at 10 bps on the pessimistic proxy; a header flood needs about 115 invalid headers a second to saturate one such +core; a pool verifies about 115 shares a second per core; IBD of 108,000 headers is about 16 minutes of one core. +Implied ladder ceiling on the half-core proxy from the worst program: 10 - 8.708 = 1.29 ms of headroom buys about +106,000 shadow instructions, N about 300,000 counted ops at the 1.83 convention (approximate), against 370,000 +from the genesis program's headroom; the ladder's ceiling should be read from the worst program, not the genesis +one, so rung 2 (199,600) stays admissible and rung 3 (330,700) does not on this proxy. diff --git a/docs/analysis/attack-pass/f7-era.md b/docs/analysis/attack-pass/f7-era.md new file mode 100644 index 00000000..27c1f13a --- /dev/null +++ b/docs/analysis/attack-pass/f7-era.md @@ -0,0 +1,165 @@ +# F7: the era-draw bias harness and the era-seed census + +Attack-pass row F7 (`docs/plans/cryptanalysis.md` 4.2; the record `docs/analysis/attack-pass-2026-10.md`), both halves: +the fast-time re-roll harness (node lane) and the 2^20 era-seed census plus the 64-bit day-key check (hash lane). +Written 7 October 2026. Every number cites its log path on igneum-build-1. + +## Target + +| Item | Value | +|---|---| +| Commit | `924288d1` on branch `attack-pass` (`igneum-pow` is byte-identical at worktree HEAD `11b375a0`: `git diff --stat 924288d1..HEAD -- igneum-pow docs/spec infra/fast-time` is empty) | +| Spec | `docs/spec/01-lottery-hash.md` 1.13.1 (era seed and draw), 1.8.4 (the day-key mixer stream); `docs/spec/04-seeds-and-vdf.md` 4.4 (era seed pipeline) and 4.6 (T from a reference core); `docs/plans/era-layout.md` sections 1 and 8 (branch `ca2-era`); `docs/analysis/horizon/algorithm.md` 5.4 | +| Draw code | `igneum_pow::generator::era_draw` over `V3_ALLOWED = [1]` (the chain's path): the width draw (consumed, pinned at 4 bytes), the odd stride multiplier `M`, the rotation `R` in 1..31, the four interleave positions `pos` by partial Fisher-Yates | +| Day-key code | `igneum_pow::memhard::MixParams::with_shape`: `SplitMix64::new(K[0] \| (K[1] << 32))` draws ROT[0..7], MUL[0..15], RC[0..15]; `K = seed_words_from_bytes("igneum-day/" \|\| day_le64)` (node fork `consensus/pow/src/igneum.rs`, `bind::day_bytes`) | +| Node draw input (today) | `consensus/src/consensus/mod.rs` `seed_below`: `E_n` is the hash of the last selected-chain block below `15,552,000 n - 7,200` (era 0: genesis). The 1-hour VDF of spec 4.4 and the certified checkpoint it reads do NOT exist in the node (era-layout.md section 8, `proto-vdf` is a prototype) | + +## Sub-row verdicts + +| Sub-row | Verdict | Gate (plan 4.2 F7) | +|---|---|---| +| (a) re-roll harness | INCOMPLETE, with the written argument | no re-roll inside the publish window | +| (b) 2^20 era-seed census | PASS | no era class with gain over 1.1x at a fraction over 2^-20 | +| (c) 64-bit day-key seeding | PASS, within spec intent (one observation recorded) | the draw's input set as the spec states it | + +## (a) The re-roll harness (node lane) + +`tools/attack/f7-era/reroll.mjs`: a 3-node fast-time network (`infra/fast-time/override-60x.json` with +`skip_proof_of_work`, the `class-v4-signal.mjs` shape), own ports 29800 and up, own devnet suffix 980, own data dir +`/tmp/igneum-fast-time-attack-f7`. The node binary is the ladder fork `vendor/igneum-node-ladder` at `1591ee1d` +(`igneumd 2.1.0`, already built on the box; read-only). Two honest virtual miners share 1 block/s on nodes 0 and 1; the +adversary on node 2 holds a block `A` built on the tip at DAA score `S - 1` (the seed block sits there), optionally waits +a stub VDF of `--vdf-ms`, then publishes `A` to try to make its own block the epoch's seed block (the last selected-chain +block below the cut `S`). A re-roll succeeds when the epoch's reported seed becomes `hash(A)`. + +The era cut `15,552,000 n - 7,200` is 180 days of DAA score away on every profile (`POW_ERA_BLOCKS` is a chain constant, +not an override field), so the harness attacks the EPOCH cut (`60 e - 10` at 60x), which runs the identical `seed_below` +derivation at a reachable score, one cut per minute. The harness's own era draw (JS) is checked byte-for-byte against the +Rust census at start: seed `b62532bc...` draws `M 558c0543 R 4 pos [0,1,2,3]` on both (log line "draw self-check ... OK"). + +Firings (both runs 6 cuts, box cores 36-37,84-85 under the shared measure lock): + +| Run | `--vdf-ms` | Re-rolls to A | Gate | Harness | Log | +|---|---|---|---|---|---| +| known-pass | 0 (no delay, the stand-in) | 1 of 6 (epoch 11, seed = A) | FAIL | SOUND (fires) | `/srv/builds/igneum-wt-attack/attack-f7/reroll-knownpass.log` | +| known-fail | 5,000 (a delay past one block interval) | 0 of 6 | PASS | SOUND (silent) | `/srv/builds/igneum-wt-attack/attack-f7/reroll-knownfail.log` | + +Both runs: 6 of 6 adversary blocks accepted, all three sinks agree, no reorg of the honest chain. The harness fires on the +known-pass and is silent on the known-fail, so it is trusted. + +Written argument (the plan allows one for the VDF's assumptions; the VDF's own delay soundness belongs to the finality +review row of `funding.md`). The re-roll is possible ONLY when the adversary can evaluate the draw of a candidate input +inside the block publish window. Today the node has no VDF: `E_n` is a plain block hash, so the input of any candidate +block is known the instant the block is built, and the harness shows the last-block-before-the-cut is grindable with one +block of hash (1 of 6 cuts steered in fast time, `--vdf-ms 0`). With any delay past one honest block interval the +re-roll is gone (`--vdf-ms 5000`: 0 of 6). The design closes this with the 1-hour class-group VDF of spec 4.4: re-rolling +by withholding needs the 3,600 s VDF evaluated inside the 2 s window, a 1,800x evaluator, and spec 4.6's margin table +gives 300x as the horizon (`algorithm.md` 5.4; `sim/horizon/algorithm/model.py --section era`). The forge route needs +2/3 of the 30-day weight, 20 days of 100 percent hash (CLAUDE.md headline). The sub-row is INCOMPLETE because the harness +cannot demonstrate the real gate: the VDF and the certified checkpoint it reads are not in the node yet (era-layout.md +section 8 states this). What the harness DOES establish: the C_era cut rule with no delay is grindable, so the era draw's +soundness rests entirely on the VDF landing before the draw procedure is frozen, and the delay-soundness measurement is +owed to the finality lane. + +## (b) The 2^20 era-seed census (hash lane) + +`tools/attack/f7-era/` (a cargo crate with `igneum-pow` as a path dependency and an empty `[workspace]`; ELF built on the +box, sha256 `a87818d8...`). `attack-f7 census` runs `era_draw` over `V3_ALLOWED` on `2^n` seeds and classifies each draw; +`attack-f7 all` runs the plant known-fail case, the census, the spec-stream op-weight census and the day-key check. + +Known-fail / known-pass of the classifier (planted parameters through a test hook in this crate; log +`/srv/builds/igneum-wt-attack/attack-f7/census-2p20.log`): every planted weak draw fires its flag (M = 1, M = 2^32-1, +M = 2^16+1, a naf-2 multiplier, an even M, R = 0, R = 32, pos linear, pos contiguous, pos not ascending) and a sound draw +(igneum-era-test/0) raises nothing. "Plant verdict: every planted case fired and the sound draw did not." + +Census results (2^24 = 16,777,216 draws, the stronger run; `census-2p24.log`; the 2^20 run agrees, `census-2p20.log`): + +| Class | Count (2^24) | Fraction | Expected (uniform) | Chip gain | +|---|---|---|---|---| +| M even (bijection failure) | 0 | 0 | 0 | finding if present: none | +| R out of 1..31 | 0 | 0 | 0 | finding if present: none | +| pos invalid (not 4 ascending) | 0 | 0 | 0 | finding if present: none | +| M = 1 (identity stride) | 0 | 0 | 4.66e-10 | 1.0034x | +| M = 2^32 - 1 | 0 | 0 | 4.66e-10 | 1.0030x | +| popcount(M) <= 2 | 1 | 5.96e-8 (2^-24) | 1.49e-8 | 1.0030x | +| popcount(M) <= 4 | 43 | 2.56e-6 (2^-18.6) | 2.33e-6 | 1.0022x | +| popcount(M) <= 6 | 1,626 | 9.69e-5 | 9.61e-5 | 1.0014x | +| popcount(M) <= 8 | 27,749 | 1.65e-3 | 1.66e-3 | 1.0007x | +| naf(M) <= 2 | 1 | 5.96e-8 | - | 1.0030x | +| naf(M) <= 3 | 18 | 1.07e-6 | - | 1.0026x | +| M = 2^k + 1 | 1 | 5.96e-8 | 1.44e-8 | 1.0030x | +| pos linear [0,1,2,3] | 9,257 | 5.52e-4 | 5.50e-4 | 1.0000x | +| pos contiguous | 120,054 | 7.16e-3 | 7.14e-3 | 1.0000x | +| pos in the low byte | 645,856 | 3.85e-2 | 3.85e-2 | 1.0000x | + +The gain metric is the datapath energy a chip saves per hash against the base weights, over the hash's datapath energy +(19.5 nJ at 100,000 ops x 0.195 pJ, the N5 floor of `algorithm.md` 5.4 / `model.py --section era`). The stride multiply is +one of three address operations, run 128 times per hash (16 loads x 8 iterations); a low-weight `M` replaces the multiplier +with a few shift-adds, worth at most 128 x 0.52 pJ = 67 pJ, so M = 1 is the richest corner at 1.0034x. The rotation is a +wire mux and the interleave an address-line permute, 0 pJ on the modelled chip. No drawn parameter touches the memory +bound, the item derivation, the load count or N. + +Gate: no class with gain over 1.1x at a fraction over 2^-20. The richest gain in the whole classifier is 1.0034x (M = 1), +and M = 1 did not occur in 2^24 draws (expected 4.66e-10). Every class at a fraction over 2^-20 has gain 1.0000x to +1.0007x. PASS on both counts. + +Uniformity of the draw (2^24): stride rotation R over 1..31 chi-square 38.5 on 30 dof (max bucket deviation 2.07 sigma, +R = 0 or 32 seen 0 times); interleave pos 1,820 of 1,820 four-subsets seen, chi-square 1,775.7 on 1,819 dof (max deviation +3.63 sigma, 0 draws with a non-4-subset); M bit 0 always set (odd by construction), bits 1..31 each set in 0.500 of draws +(worst bit 1.81 sigma); the stride bijection never failed (0 even M). The era stream's own 64-bit seed (words 0 and 1) was +distinct on all 2^24 draws. + +Op-weight corners (spec 1.13.1 first stream, implemented in `attack-f7 spec` from the spec text because `igneum-pow` does +not draw the op-weight perturbation at this commit; 2^20 draws, `census-2p20.log`): the ten non-load weights each +perturbed by -2..+2 and renormalised to 75 move the multiply share (mul+mad+mulhi, base 22 of 75) between 15 and 31. The +richest corner for a chip is 15/75 (0.152 pJ per op, -22 percent of the base datapath), seen once in 2^20; 16/75 at +3.22e-3. The GPU's energy moves the same way (its IMAD is the chain's own op), so the chip-against-GPU gain of every +weight corner is 1.0x, with 0 memory effect. Renormalised sums were 75 on every draw (0 failures). Fold rotations: a triple +all equal 2.13e-3, both triples all equal 1.91e-6, all six equal 0; uniform over 1..31, rotation 0 never drawn; a wire +mux, 1.0x. + +## (c) The 64-bit seeding of the day-key stream (hash lane) + +`attack-f7 days` over days 0..131,072 (`census-2p20.log`). The day key `K` is `seed_words_from_bytes("igneum-day/" || +day_le64)`: a calendar function, no chain state. All 256 bits of `K` enter the cache fill (spec 1.8.3, `K[0..7]` in every +block input), so the dataset depends on the full key; the mixer-constant stream (ROT, MUL, RC) is seeded from `K[0] | +(K[1] << 32)`, 64 bits, which is the spec's stated intent (spec 1.8.4). + +| Quantity | Value | +|---|---| +| Days the chain can have | about 65,745 in 180 years at 1 block/s (2^16.0) | +| Distinct 256-bit keys K over 2^17 days | 131,072 (all) | +| Distinct 64-bit stream seeds over 2^17 days | 131,072 (0 duplicates) | +| Distinct (ROT, MUL, RC) tuples over 2^17 days | 131,072 | +| Birthday bound on a 64-bit collision among 2^16 days | 2^(32 - 65) = 2^-33 | + +The spec intends 64 bits for the mixer-constant draw, and the truncation is not a reduction of the draw space the firm +would flag: at most 2^16 days are ever drawn, each a distinct calendar day with a distinct 64-bit seed (0 collisions in +2^17), so no two days share a mixer. One observation, within spec intent and recorded for the written argument of +`funding.md` B5 rank 6: the mixer-constant stream has 64 bits of seed entropy, so at most 2^64 distinct daily mixers are +reachable (not the ~2^1,047 nominal); this is not exploitable (the days used are 2^16, all distinct) and whether any +reachable tuple is weak is the separate weak-day census of row F4. + +## Consequences per tier + +The era draw and the day-key seeding are protocol-wide and do not differ by card tier: the load width and load count are +pinned, so every era is equally memory-bound and no 8, 12, 16 or 24/32 GB card is advantaged or disadvantaged by any draw +(the measured six-era hash-rate spread is 1.3 percent on the RTX 5090, 3.2 on the RX 9070 XT, 0.8 on the M5 Max, +`algorithm.md` 5.4). No drawn era parameter or day key makes a chip cheaper against a GPU: the richest datapath corner is +1.0034x and is shared with the GPU. The one operational consequence is for the protocol, not a miner tier: the era draw's +grinding resistance is not yet demonstrable because the 1-hour VDF and its certified checkpoint are not in the node, so +the freeze of the draw procedure and the C_era cut rule must wait on the VDF landing and the finality lane's delay- +soundness measurement. + +## Gate line + +- (a) harness: INCOMPLETE. No re-roll with a one-block delay (known-fail 0 of 6); a re-roll with no delay (known-pass 1 of + 6). The real gate (no re-roll inside the 2 s window) rests on the 1-hour VDF, which is not in the node; written argument + above. +- (b) census: PASS. No era class with gain over 1.1x at any fraction (richest 1.0034x, M = 1, absent in 2^24); the draw is + a bijection on every sample and uniform in R, pos and the M bits. +- (c) 64-bit seeding: PASS within spec intent. The spec intends 64 bits for the mixer stream; 2^16 days are all distinct; + the one observation (2^64 reachable mixers) is recorded, not a flaw. + +What a failure moves (plan 4.2 F7): the draw procedure or the C_era cut rule; a redraw rule for the era stream. Nothing in +(b) or (c) moves them. (a) moves nothing in shipped code but gates the freeze of the draw procedure on the VDF. diff --git a/docs/analysis/attack-pass/f8-uniform.md b/docs/analysis/attack-pass/f8-uniform.md new file mode 100644 index 00000000..f49845c8 --- /dev/null +++ b/docs/analysis/attack-pass/f8-uniform.md @@ -0,0 +1,259 @@ +# F8. Uniformity censuses of the class v4 derivation + +Attack-pass row F8 (`docs/plans/cryptanalysis.md` section 4.2; the pass record `docs/analysis/attack-pass-2026-10.md`). +Run 7 October 2026, 08:13 to 09:3x UTC (09:13 to 10:3x UK) on igneum-build-1. Verdict: **FINDING** (AP-F8-1 below). +The line-index census is a PASS at its full sample size; the cross-hash item histogram is not uniform, and the cause +is in the base program, inside the acceptance rule's blind spot. + +## 1. Target + +| Item | Value | +|---|---| +| Commit | `igneum-pow` at 924288d1 (`attack-pass`); the box built HEAD b2a411d1, whose `igneum-pow` is byte-identical (`git diff --stat 924288d1 HEAD -- igneum-pow` is empty) | +| Class | `--program-class v4`: `V4_CLASS` = `mx8+sh256x27`, generator 4, mixer x8, the era layout drawn inside the class, the shadow block of 256 instructions x 27 reps | +| Line index | `proto-metal/MEMHARD.md` section 1.6: `a = s[0] AND 0x003fffff`, 4,194,304 lines of 64 B, 8 dependent reads per item (`memhard.rs` `derive_items_mask`, `cache.line_const(s[0])`) | +| Item index | `verify.rs` `load_index`: `y = rotl(x * M, R)`, the site's window `(y & (MASK >> k)) \| off`, then `Layout::split` removes the four interleave bits; 2^24 items at the 2^28-word dataset | +| Reads per hash | 128 loads (16 sites x 8 iterations), so up to 1,024 cache lines per hash and 32,768 per warp. The spec's analytic bound of 832 lines per hash (`docs/spec/01-lottery-hash.md` line 347, 104 loads x 8) predates generator 2's fixed 16 load slots; the current bound is 128 x 8 = 1,024 | +| Prior figures | `chip-model-v3.md` section 1: "median 128.00 distinct" items per hash (the 20,000-program census); `weak-program-census-2026-10-03.md` line 291: 127.7 distinct addresses per hash under the proposed generator | +| Day | the devnet pack's day, `bind::day_bytes(20730)` (2026-10-04), day 0 of the growth schedule: a 2^26-word cache, a 2^28-word dataset. Census 1 uses days 20730 to 20745 | +| Programs | p1 = the devnet epoch-0 derivation (epoch seed and era seed both the genesis hash `edc4fa84...fb07`, program id `c120d7963abdcd96`, attempt 0); p2 and p3 = chain-shaped seeds from tag strings (section 4), attempts 1 and 0 | + +## 2. Method and harness + +Harness: `tools/attack/f8-uniform/` (crate `attack-f8`, a path dependency on `igneum-pow`, nothing in the library +modified). Built on the box through `tools/build-remote.sh`: sha256 `590668...f913` for the firings of 2.1, +`ef8042...11b2` for sections 3, 4.1 and the first item runs (flat null, `log/c-*`, `log/d-*`), `890955...fbd3` for the +window-model runs and the seed census (`log/e-*`). The committed source carries one later label fix (the +"uniform-on-window" entropy reference in the per-site line is 16 - k_off bits; the 09:04 UTC logs print 16 - 2 k_off). Box scratch `/srv/builds/igneum-wt-attack/target-attack-f8/` +(the name `target-*` is what the box's checkout clean spared at the time; the fix at b92a5fd4 now also spares +`attack-*`). Every run: `nice -n 10 taskset -c 22-27,70-75`, 12 threads, under `flock -s /srv/builds/_locks/measure` +in chunks under 3 minutes each (the longest, phase D, under 25 minutes). + +Two mirrors, each trusted only while it agrees with the library bit for bit: + +| Mirror | What it records | Agreement check | Result | +|---|---|---|---| +| `derive_traced`: `memhard::derive_items_mask` instruction for instruction (`mixer`, `round_key_mult`, `cache.line` from the library), the line index of every round kept | 8 line indices per item | every item of every day also derived by the library's `derive_items` and compared on all 16 words | 0 mismatches on 268,435,456 items (section 3) and on 16,777,216 items per table build (section 4) | +| `Mirror::warp`: `verify::interpret_warp_init` for the class v4 op set, dataset words from a table of the day's 2^24 items, the item index and the source register of every load kept | 128 item indices per lane, the source value's saturation per position | the 32 hashes of warp 0 to 63 and of every 997th warp compared with `Epoch::hash_warp` | 0 mismatches on 95 warps per program (section 4) | + +Three censuses: + +1. `lines`: all 2^24 items of each of 16 consecutive day keys (2^28 item derivations, 2^31 line reads), the full 2^22-line + histogram per round and pooled, the 2^16-bucket histogram (64 lines, one chained segment per bucket), a uniform + SplitMix64 control of the same size. +2. `warps`: 10^6 nonces (31,250 warps) of each of three programs: distinct lines and items per hash and per warp, the + cross-hash item histogram, per-position diagnostics, an attribution pass from the hottest items back to the load + positions that read them. +3. `warps` at 2^26 nonces on p1: the one-epoch cross-hash item histogram at 512 expected reads per item. + +The tests, defined before the runs: + +- **6-sigma test**: the largest (and smallest) bucket of a histogram within 6 sigma of its expectation, sigma = + sqrt(expectation). The gate's bucket is the 64-line segment for lines and the 64-item bucket for items. The + full-resolution histograms are reported beside a uniform control of the same size, because at a small mean the + Poisson tail puts the maximum of 4 million bins above 6 sigma by chance (control at mean 8: +6.72 sigma; at mean + 32: +5.83; at mean 512: +5.61). +- **Hot-set test** (F8's definition, written for F9's reuse): sort items by read count; S_f = the share of all reads + on the top-f fraction of items, for f in {0.1%, 0.5%, 1%}; E_f = the same share on a control of the same size drawn + from the design's own null (flat uniform for lines; the window-weighted null for items, section 4.2); the excess + X_f = S_f - E_f. **A hot set exists at f when X_f >= f**: after the chance excess is removed, the top f of items + capture at least one extra proportional share, which is what an on-die copy of f of the items would have to win to + matter. X_f / f is printed as the gain in proportional shares. The acceptance-style form of the same metric (for + rule (c)'s 2,048 evaluations): per load position, the largest count of one masked address, and the count of + saturated (0 or 2^32 - 1) source values. + +### 2.1 The harness fires (known-fail and known-pass) + +| Plant | What it does | 6-sigma test | Hot-set test | Log | +|---|---|---|---|---| +| `quarter-lines` | line index masked to a quarter of its range | buckets64 largest +75.97 sigma (2,231 at mean 512), smallest -22.63: FLAGGED | X_1% = +3.54% (S 5.60% vs control 2.06%), X/f = 3.5 at every f: FLAGGED | `log/a1-lines-quarter.log` | +| `half-lines` | line index masked to a half | buckets64 largest +29.26 sigma: FLAGGED | X_1% = +1.25%, X/f = 1.25: FLAGGED | `log/a2-lines-half.log` | +| `const-item` | one constant item at the first load site (1/16 of reads) | items buckets64 largest +92,682 sigma: FLAGGED | X_0.1% = +6.38%, X/f = 63.8: FLAGGED | `log/a4-warps-const-item.log` | +| none, 2^22 items, one day | the real derivation at a small size | buckets64 largest +4.42 sigma, smallest -4.51: within 6 sigma (control +4.33) | X_f = -0.0004%, -0.0006%, -0.0010%: clear | `log/a3-lines-pass-small.log` | + +Both tests fire on every plant and neither fires on the real line derivation. Log paths are under +`/srv/builds/igneum-wt-attack/target-attack-f8/`. + +## 3. Census 1: the line index over 2^28 derivations (PASS) + +Sample reached: 16 days x 2^24 items = 268,435,456 item derivations, 2,147,483,648 line reads into 4,194,304 lines +(512 expected per line, 32,768 per 64-line segment). Mirror mismatches against `derive_items`: 0 of 268,435,456. +Log: `log/b-lines-16days.log`; histograms `out/lines-d20730-n16-i24-none-buckets64.txt` (65,536 rows) and +`out/lines-d20730-n16-i24-none-full.u32le` (4,194,304 x u32). + +| Histogram | Bins | Expected | Largest | Sigma | Smallest | Sigma | chi2/dof | Top 1% share | +|---|---|---|---|---|---|---|---|---| +| Pooled, 64-line buckets (the gate) | 65,536 | 32,768 | 33,645 | +4.84 | 31,998 | -4.25 | 1.00226 | 1.01427% | +| Pooled, full 2^22 lines | 4,194,304 | 512 | 639 | +5.61 | 402 | -4.86 | 0.99937 | 1.11979% | +| Control, 64-line buckets | 65,536 | 32,768 | 33,524 | +4.18 | 31,960 | -4.46 | 0.99930 | 1.01426% | +| Control, full 2^22 lines | 4,194,304 | 512 | 639 | +5.61 | 408 | -4.60 | 0.99952 | 1.11956% | +| Per round 0 to 7, full, pooled (64 per line) | 4,194,304 | 64 | 107 to 113 | +5.38 to +6.12 | 26 to 29 | -4.75 to -4.38 | 0.99855 to 1.00062 | 1.3483% to 1.3488% | +| One day (20730), 64-line buckets | 65,536 | 2,048 | 2,291 | +5.37 | 1,859 | -4.18 | 0.99985 | 1.05826% | +| One day, control, 64-line buckets | 65,536 | 2,048 | 2,250 | +4.46 | 1,874 | -3.84 | 1.00377 | 1.05901% | + +Per day, the gate bucket's largest value ran +4.00 to +5.37 sigma on all 16 days (control +4.46), every day within +6 sigma. Hot-set test on the pooled lines: X_0.1% = -0.00002%, X_0.5% = +0.00010%, X_1% = +0.00023% (X/f under +0.0003): clear. Round 0, whose input is the sequential item index through the init `t * MUL[i] + RC[i]` and eight +mixer applications, is as flat as rounds 1 to 7 (chi2/dof 0.99926; its +5.50 sigma maximum is below the control's ++5.61 at the pooled size). Round 5's +6.12 sigma at mean 64 is one bin of 4 million at a Poisson tail where the +control at mean 8 reached +6.72; its chi2/dof is 0.99855. + +Gate line: the largest bucket is within 6 sigma of uniform (+4.84 on the 64-line buckets, +5.61 on the full 2^22 +lines, both at or below the control), chi2/dof 0.99937, no hot set. **PASS at 2^28 derivations.** + +## 4. Census 2 and 3: distinct lines per hash and warp, and the cross-hash item histogram + +Setup per program: the day's 16,777,216 items derived once into a table with their 8 lines (6 to 9 s on 12 threads, +0 mismatches against `derive_items` on every item), then the warps interpreted from the table at 2.7 to 3.2 ms per +warp per thread. Logs: `log/c-warps-p{1,2,3}-1e6.log` (first run, flat null) and `log/e-warps-p{1,2,3}-1e6.log` +(windowed null, section 4.2); distributions `out/warps--d20730-n1000000-none-distinct.txt`, item histograms +`...-items.u32le` (16,777,216 x u32), per-position tables `...-positions.txt`. + +### 4.1 Distinct lines and items per hash and per warp (10^6 nonces each) + +| Program | Epoch seed / era seed | Lines per hash min / p1 / median / max / mean | Items per hash min / median / mean | Lines per warp min / median / max / mean | Items per warp min / median / mean | +|---|---|---|---|---|---| +| p1 `c120d7963abdcd96` (devnet epoch 0) | genesis / genesis | 1,008 / 1,023 / 1,024 / 1,024 / 1,023.867 | 126 / 128 / 127.9989 | 32,579 / 32,636 / 32,680 / 32,635.84 | 4,090 / 4,096 / 4,095.41 | +| p2 `82f0696f823e9c65` | `59cef1aa...bfdfa` / `9cba001f...1f69` | 1,014 / 1,023 / 1,024 / 1,024 / 1,023.871 | 127 / 128 / 127.9995 | 32,580 / 32,637 / 32,684 / 32,636.08 | 4,091 / 4,096 / 4,095.48 | +| p3 `e282eed7d47e425e` | `c54e2ddd...c95d` / `1b04f607...b58a` | 999 / 1,016 / 1,024 / 1,024 / 1,023.600 | 125 / 128 / 127.9656 | 32,276 / 32,481 / 32,601 / 32,480.32 | 4,050 / 4,076 / 4,075.85 | +| Uniform expectation | | 1,023.875 of 1,024 | 127.9995 of 128 | 32,640.3 of 32,768 | 4,095.50 of 4,096 | + +Per hash, every program reads its 128 items and 1,024 lines as the design intends (p1 and p2 at the uniform +expectation; p3 a shade under, 127.97 items, which is the same site-15 effect as the finding below: the saturated +site repeats an item inside a hash 3 times in 100). Per warp, 32 lanes read 32,636 distinct lines of 2^22, a 2 MiB +working set of cache lines and 256 KiB of dataset items, within 0.01% of uniform on p1 and p2. + +### 4.2 The cross-hash item histogram and the window layer + +The era layout's window layer (`docs/plans/era-layout.md` section 1.4, layer 8) makes each load site read an +aligned half or quarter of the dataset with probability 2/3. The per-site item distribution is therefore not flat by +design (the diagnostic's "worst bit" reads P(1) = 1.0000 or 0.0000 at every windowed site: the fixed top bits), and +the summed item histogram has density steps between quarters. For p1 the 16 windows (site:shrink:offset +`7:2:1 8:1:1 9:1:1 10:1:1 11:0:0 13:1:1 29:0:0 30:2:2 31:1:1 44:1:1 46:2:0 47:0:0 52:0:0 56:0:0 58:2:0 63:1:1`) give +expected reads per item by quarter of 3.25 : 2.25 : 5.75 : 4.75 in sixteenths of the flat value. Against a flat +uniform the 64-item buckets of p2 (a program without the finding) read +10.03 and -9.06 sigma, which is the window +layer and not a flaw. The item tests are therefore judged against the **window-weighted null**: the expected count of +every item from the program's 16 windows, and a control that draws each read from a uniformly chosen site's window. +A chip gains nothing from the window steps: the union of the windows is the whole dataset every hour (era-layout.md +section 7), the floor window is 2^26 words (256 MiB), and which quarter is dense changes with the program. + +#### The window model (reproducible by the firms) + +For load site s with window draw `(k_s, o_s)` at the 2^28-word dataset: `k = min(k_s, 28 - 26)`, the word window is +`[o_s << (28 - k), (o_s + 1) << (28 - k))`; the item window is `[o_s << (24 - k), (o_s + 1) << (24 - k))` of +`2^(24 - k)` items (the four interleave positions all lie below bit 16, so the top bits of the word index are the top +bits of the item index). The expected reads per item is `E[t] = sum over sites s with t in window_s of N x 8 / 2^(24 - k_s)` +for N nonces (8 iterations per site), a density constant on each quarter of the item space. The windowed control draws +each of the N x 128 reads as (site = read index mod 16, item uniform on that site's window). Both controls are drawn from +SplitMix64 with a fixed seed. The tests on items are run against E[t] (chi-square, sigma of the largest and smallest +64-item bucket) and against the windowed control (the top-f shares); the flat uniform numbers are kept beside them as +what an auditor sees first. + +#### Results, 10^6 nonces per program, 128,000,000 reads (`log/e-warps-p{1,2,3}-1e6.log`) + +| Program | Windows (k_off:offset per site) | Quarter densities (reads per item) | Buckets64 largest sigma, windowed (control) | chi2/dof windowed (control) | Top 0.1% share: real / window control / flat control | Ratio to window control at 0.1% (gate 1.2x) | Ratio to flat control | Hot set (X_f >= f) | +|---|---|---|---|---|---|---|---|---| +| p1 devnet epoch 0 | 2:1 1:1 1:1 1:1 0 1:1 0 2:2 1:1 1:1 2:0 0 0 0 2:0 1:1 | 6.20 / 4.29 / 10.97 / 9.06 | +45.77 (+4.95) | 1.2336 (0.9981) | 0.5458% / 0.2891% / 0.2429% | 1.888x BEYOND | 2.247x | yes at 0.1% (X/f 2.57) and 0.5% (1.20); not at 1% (0.46) | +| p2 | 2:0 1:0 0 0 2:3 2:2 1:0 0 2:3 0 0 0 0 1:1 2:0 0 | 9.54 / 5.72 / 6.68 / 8.58 | +4.59 (+4.64) | 1.0061 (0.9986) | 0.2716% / 0.2639% / 0.2429% | 1.029x within | 1.118x | no (X/f 0.08, 0.05, 0.05) | +| p3 | 1:0 0 0 2:2 0 0 1:0 1:0 1:0 2:2 2:2 1:1 0 0 0 0 | 7.63 / 7.63 / 10.49 / 4.77 | +12,245.66 (+5.06) | 907.67 (0.9993) | 4.5954% / 0.2792% / 0.2433% | 16.46x BEYOND | 18.92x | yes at every f (X/f 43.2, 9.9, 5.0) | + +p2 is what the class is designed to be: against the window model its largest bucket is +4.59 sigma (the control +4.64), +chi2/dof 1.006, the top 0.1% of items hold 1.029x their window-model share, and the flat-control ratio of 1.118x is +the window layer. p1 and p3 are the finding (section 5). The one-epoch histogram at 2^26 nonces (8,589,934,592 reads, +512 per item, `log/d-warps-p1-2e26.log`, flat null): p1's top 0.1% hold 0.5199% of reads against 0.1152% flat +control (X/f 4.05), the top 1% 2.4946% against 1.1198% (X/f 1.37), item 0xca5b92 78,479 reads at a mean of 512, and +site 15 feeds 6.37% of its reads into the top 0.1% in each of the 8 iterations; the excess grows with N as the +control's chance excess shrinks, which is the signature of a structural skew. Distinct lines and items per hash and +per warp at 2^26 nonces: 1,023.866 / 127.9989 / 32,635.6 / 4,095.41, unchanged from 10^6. + +## 5. AP-F8-1: a saturated load source makes a cross-hash hot set (FINDING) + +**What**: an accepted class v4 program can read one load site from a register whose last writes after its last +injecting write are `or` (and, mildly, `mul`), so the site's address has fewer than 32 bits of entropy across nonces +and the same items are read by many hashes. The per-hash figures (128 distinct items, 1,024 lines) stay intact; the +cross-hash item histogram does not. It is not the window layer (p2 shows the window layer alone is clean against its +model) and not the shadow block (iteration 0's load, which runs before any shadow block, is as hot as iterations 1 to +7: p1 6.372% vs 6.371% to 6.378%; p3 71.9% vs 72.4% to 72.6%). + +**Where it hides from rule (c)** (`accept.rs`, 2,048 evaluations of the base program): the tests are constant bits +in FINAL register values, one address in ALL 32 lanes of a unit, saturated FINAL values, output-bit bias, and distinct +addresses WITHIN a hash. A site whose address is concentrated across hashes but refreshed before the end of the +iteration passes every one. Rule (a) accepts any write, `or` included, as the refresh between two loads from the same +register (`check_stale_loads`); `Op::injects` (add, sub, xor, mad, shfl, load) is only used by rule (b), once per +register per program. + +**The index derivation at the hot site** (the "writers back to the last injecting one" lines of `log/e-warps-p*.log`): + +| Program | Hot site | Source | Writes after the last injecting write | Site's reads into the top 0.1% of items (flat expectation) | Index entropy, 256-item buckets (uniform on window) | Saturated source (x = 0 or 2^32 - 1) | Most repeated address at one position in 2,048 evaluations (uniform: 1 to 2) | +|---|---|---|---|---|---|---|---| +| p3 | site 15, instr 62 | r5 | `load@17` then `or@19`, `or@30` | 72.43% (0.10%) | 13.411 bits (16) | 1.368% | 44 of 2,048; 32 saturated | +| p1 | site 15, instr 63 | r6 | `add@51` then `rotl@53`, `or@61` | 6.93% (0.11%) | 14.985 bits (15) | 0.005% | 2 of 2,048; 0 saturated | +| p2 (clean) | every site | | injecting, or bijective (`rotl`), or `mul`/`mulhi` | 0.41% to 0.97% (0.20%; the window densities) | 13.999 / 14.997 / 15.994 bits (14 / 15 / 16) | 0.000% | 2 of 2,048; 0 saturated | + +In p3 two `or`s on r5 after its load make the source 1 with probability 7/8 per bit; x = 2^32 - 1 in 1.37% of +evaluations and the images of the near-saturated values under the stride (`y = rotl(x * M, R)`, 256 x-values per +item) pile onto a few items: 0xffdf69 takes 213,913 of the site's 8,000,000 reads (2.67%), the top 0.1% of items +72.4%, and 4.6% of ALL reads of the hash land on 0.1% of the items. In p1 one `or` after `rotl(add)` gives 3/4 per bit +on the ORed positions: no saturation to speak of (0.005%), but 6.9% of the site's reads on 0.11% of the items (the +hot items share the low 20 bits `5b92`: 0xca5b92, 0x8a5b92, 0xaa5b92, 0xba5b92, 0x825b92, 0xe65b92, 0x985b92), a +2.6x proportional excess at f = 0.1%. p2's `mul` sites (10, 13: `mul` after a load or a shuffle) read 0.65% and 0.70% +into the top 0.1% against 0.41% and 0.48% for their window class (an even multiplier zeroes low bits; the hot items +0xd6a680, 0xe44400, 0xd25600 end in zero bits), a mild effect that the window-model ratio (1.029x) absorbs. + +**How common** (the seed census, 64 chain-shaped programs p4 to p67, 262,144 nonces each, `log/e-seed-census-4-67.log`, +`out/seed-census-d20730-n262144-p4-67.txt`): CENSUS-LINE + +**Reproduction**: `attack-f8 warps --program 3 --nonces 1000000 --diag 1` (or `--program 1`); the acceptance-style +numbers come from the same run's "acceptance-style" line. The program is `Epoch::chain_program(epoch_seed, Some(era), +ProgramClass::V4, label)` with the seeds of section 4.1. + +**Proposed fix** (not applied; `igneum-pow` untouched, the Counter ASIC lane re-gates on `ca3-v4-uniform` with this +harness): + +1. Rule (a'), static: between the last injecting write of a load's source register and the load (cyclically), no + `or` and no `mul` writes that register; `rotl`, `rotr` and `mulhi` may (bijective, or measured flat: p2 site 15 + reads `mulhi` after `add` at 1.00x). This rejects p1 and p3 at draw time and costs nothing at run time. Programs + rejected are redrawn as today (`MAX_ATTEMPTS` 32); the census gives the rejection rate. +2. Rule (c'), dynamic, the same 2,048 evaluations: no load site reads a saturated source (0 or 2^32 - 1) in more + than 2 evaluations, and no address repeats more than 4 times at one position (uniform expectation 1 to 2; p3 shows + 44 and 32). This catches the strong class only; p1's class needs about 2^16 evaluations to show at a site (65,536 + nonces: largest item count 67 at a mean of 0.5), so (a') is the rule that closes it and (c') is the check that + fails loudly if (a') is ever loosened. +3. Packs re-cut for the seeds the new rule rejects (the devnet epoch-0 program p1 is one of them: its site 15 is + `or@61`), with the gate pack ids re-pinned; the chain's own epochs redraw automatically. + +**Reuse for F9**: the hot-set metric (section 2) on the per-program item histogram at 2^18 nonces, and the +acceptance-style pair (most repeated address at a position, saturated sources at a position) at 2,048 evaluations, +are both emitted by `warps --programs a..b`; a header-grinding search that steers a program to a hot set would show as +ratio-to-window-model above 1.2x at f = 0.1%. + +## 6. Consequences per tier + +| Number | What it means | Per tier | +|---|---|---| +| Line index uniform at 2^28 derivations (largest segment +4.84 sigma, chi2/dof 0.99937) | the 256 MiB cache has no hot segment: a chip or a card cannot serve the 8 dependent reads of an item from a cache smaller than the whole 256 MiB (the floor window of era-layout.md) | no change for any card; the verifier's cache stays 256 MiB in RAM on every node | +| Distinct lines per hash 1,023.87 of 1,024, items 127.999 of 128 (p1, p2); per warp 32,636 lines, 4,095 items | the per-hash working set is 64 KiB of cache lines and 8 KiB of items, per warp 2 MiB of lines and 256 KiB of items; the item-derivation chip's "128 items per hash" input (`chip-model-v3.md`) stands | the 8 GB card and up: unchanged; the recompute chip pays 128 derivations per hash, as modelled | +| p3-class programs: 4.6% of all dataset reads on 0.1% of items (1 MiB of a 1 GiB dataset); p1-class: 0.59% on 0.11% | a stored-dataset chip with 1 MiB of on-die SRAM serves 4.6% of its reads without touching DRAM on such an epoch; a GPU's L2 (96 MiB on the 5090, 64 MB Infinity Cache on the 9070 XT, vendor figures) holds the same 1 MiB, so both sides gain the same 4.6% of reads and the chip's edge from it is about 0 (the per-joule edge of `evidence.md` row 17 is a DRAM-read figure; a 4.6% read saving on both sides moves it by under 5% on such epochs). The recompute chip (f = 0, SRAM cache) caches the derived hot items and skips up to 4.6% of its 128 derivations per hash on such epochs, a 4.8% rate gain on those epochs only | home cards 8 to 32 GB, rigs, pools: no action; a few percent of epochs run a few percent faster for everyone with an L2. The verifier: `MemhardCpu::fetch` dedupes within a fetch only, so no change. The chip model: the headline 2.1x at k = 1 moves by under 5% on affected epochs and 0 on others; the fix below returns it to 0 everywhere | +| The acceptance rule's blind spot (cross-hash concentration at one site) | a program class property, not a day or era property: the same seed is hot on every day and under every era, so a chip or a pool that selects epochs cannot gain more than the epoch's own 4.6%; but the public claim "the item map is uniform per program up to the window layer" is false for the affected fraction of seeds until rule (a') lands | the fix is a generator rule plus packs re-cut: a class change under the 95% signalling rule if it lands after the flip, a plain re-cut if it lands in the class v4 cut itself (the lane's call) | + +## 7. Gate line and verdict + +| Gate (plan 4.2 F8, the same as 1.4 (4)) | Result | Status | +|---|---|---| +| The largest bucket within 6 sigma of uniform on the stated sample sizes (line index, 2^28 derivations) | +4.84 sigma on 64-line segments, +5.61 on 2^22 lines (control +4.18 / +5.61), chi2/dof 0.99937 | PASS | +| The item distribution within 6 sigma of uniform (against the window model, the design's own null) | p2 +4.59 sigma (control +4.64); p1 +45.77; p3 +12,245.66 | FAIL on p1 and p3 | +| No hot set under 1% of items among passing seeds (10^6 nonces on three programs; the 64-seed census at 2^18) | p2 none; p1 top 0.1% at 1.888x the window model (2.247x flat), X/f 2.57; p3 16.46x (18.92x flat), X/f 43.2; census: 31 of 64 seeds over 1.2x of the window model, 23 with a hot item | FAIL | +| The Counter ASIC lane's record gate: top 0.1% within 1.2x of the window-model control on every seed | p2 1.029x; p1 1.888x; p3 16.46x; census: 31 of 64 seeds over 1.2x (median 1.17x, p90 2.70x, max 13.09x on p31) | FAIL | + +**Verdict: FINDING (AP-F8-1).** The line index passes at 2^28 derivations. The cross-hash item histogram fails +the hot-set gate on 2 of the 3 named programs (one of them the live devnet epoch-0 program) and on 31 of 64 seeds (48 percent) of of +the 64-seed census, from `or` (and mildly `mul`) writes on a load's source register after its last injecting write, +outside every test of rule (c). What it moves: not the mask or the fold (the derivation is uniform) but the +acceptance rule, (a') and (c') above, and the packs re-cut. Ownership: the Counter ASIC lane (generator and rule), +re-gated with this harness on the fixed branch; the row reads FIXED-AND-PASSED when every seed of the census passes +both the hot-set test and the 1.2x gate under the new rule. + +Sample sizes reached: 2^28 derivations (lines); 10^6 nonces on three programs (distinct lines, hot set); one epoch at +2^26 nonces (cross-hash histogram); 64 seeds at 2^18 nonces (the census). + +Times UTC in the logs; the runs ran 08:13 to 09:2x UTC on 7 October 2026 (09:13 to 10:2x UK). diff --git a/docs/analysis/attack-pass/f9-grind.md b/docs/analysis/attack-pass/f9-grind.md new file mode 100644 index 00000000..5f6e3608 --- /dev/null +++ b/docs/analysis/attack-pass/f9-grind.md @@ -0,0 +1,269 @@ +# F9: acceptance edges, the hot-set search, header grinding + +Attack-pass row F9 of `docs/plans/cryptanalysis.md` section 4.2 (record: `docs/analysis/attack-pass-2026-10.md`). +Sub-agent attack-f9, 7 October 2026. Status: IN PROGRESS (rewritten as each run lands; the numbers below are the +ones already final, each with its log). + +## Target + +| Item | Value | +|---|---| +| Commit | 924288d1 (branch attack-pass, worktree igneum-wt-attack) | +| Generator | 4, class v4 `mx8+sh256x27` composed with the era draw (`LoadClass::era(V4_CLASS, E, [4 bytes])`), era seed E = the devnet epoch-0 seed `edc4fa84...fb07` (`proto-cuda/packs-ca3-v4/v4-devnet-epoch0/seeds.txt`) | +| Rule | `igneum-pow/src/accept.rs`: (a) stale load sources, (b) injecting writes, (c) the 2,048-evaluation dynamic test on the closed-form stand-in `dataset_elem` at 2^28 words with init words = seed words; redraw on rejection up to 32 attempts | +| Header binding | `igneum-pow/src/bind.rs`: init words `I = seed_words_from_bytes("igneum-block/" \|\| H \|\| nonce_hi_le32)`, one `I` per 32-lane warp, the lane nonce in the low 32 bits | +| Memory-hard dataset for the edges | the devnet day 20730 (`day_seed_hex 69676e65756d2d6461792ffa50000000000000`), class v4 shape (mixer x8, cache 2^26 words, dataset 2^28 words), `Epoch::chain_dataset_day` | +| Card | RunPod RTX 5090 (170 SMs, 32,120 MiB, driver 570.195.03, CUDA 12.8.1), pack `v4-devnet-epoch0` built there with `nvcc -O3 -arch=sm_120` | + +## Known-failed shape + +A seed grind that steers a program to a hot cache set for DRAM locality, or an edge where the closed-form stand-in +disagrees with the live verifier in the attacker's favour. + +## Gate + +Zero passing programs with a hot set under 1 percent of items among 10^6 seeds; the grinding gain under 1 percent of +rate at any search cost. What a failure moves: the closed-form stand-in replaced by the live verdict at the edges; a +locality term in rule (c). + +## Harness + +`tools/attack/f9-grind/` (crate `attack-f9`, `igneum-pow` as a path dependency, nothing in igneum-pow edited; built +on igneum-build-1 through `tools/build-remote.sh`; the binary on the box at +`/srv/builds/igneum-wt-attack/tools/attack/f9-grind/target/release/attack-f9`): + +| Sub-command | What it does | +|---|---| +| `selftest` | the firings listed below | +| `edges` | sub-row (a): every candidate of every seed through the re-implemented dynamic test twice, closed form and memory-hard, every metric of rule (c) measured to the end (no early abort) with its margin; the attempts continue until both stand-ins have accepted, so the chosen program under each is known | +| `hotset` | sub-row (b): the seed's accepted program, its 2,048 x 128 address record under the acceptance init and under a block init; per site the nonce-independent address bits, the distinct addresses and the most-read address; the histogram at bucket scales 2^8 to 2^24 words against a window-aware Poisson expectation (the era windows send a site to the dataset, a half or a quarter of it) with a Bonferroni tail; the taint count of init-determined loads | +| `inspect` | one seed's program with every site that repeats an address | +| `grind`, `table-random` | sub-row (c), CPU side: the init-determined load sites of the devnet epoch-0 program, the per-warp search over K nonce_hi values for the fewest distinct 128 B lines inside those load instructions (`--mode intra`, what the coalescer merges) or across them (`lines`, `pages`), the search cost per hit, and the per-warp init tables the card reads | +| `reference` | the 64 bound hashes the card's known-pass compares against; the file used on the pod came from the pre-built `igneum-pow hash-bound` instead (`ref.txt`, sha256 e831458a...) | +| `summarise.py` | the census summaries quoted below | + +`tools/attack/f9-grind/pod/` (the card): `make-variants.py` copies the pack's `kernel_bound.cu` into five kernels +(per-warp init table; the five init-determined loads broadcast to lane 0's address; every load broadcast; the first +such load broadcast; lane 1 reading lane 0's address at the first such load), `f9-host.cu` fills the cache and dataset +with the pack's own kernels, checks them against `vectors.h`, checks the bound hash against the reference, checks the +per-warp kernel on an all-equal table against the honest kernel, then times the eight variants in interleaved rounds; +`run.sh` builds on the pod, samples `nvidia-smi` once a second and joins the samples to the phases (`join-power.py`). + +Hot-set metric (F8's record `docs/analysis/attack-pass/f8-uniform.md` did not exist when this harness was written, so +the metric is defined here). Strict reading, "any hot bucket": a site with 7 or more nonce-independent address bits +(support at most 2^21 of 2^28 words, 0.78 percent of items; the window's own fixed bits not counted), or any bucket +of at most 2^20 words (0.39 percent of the dataset) at scales 2^8, 2^12, 2^16, 2^20 whose count has a Poisson tail +against its window-aware expectation under 10^-6 after the Bonferroni correction. Gate reading, "flagged": the reads +above expectation in those hot buckets (the hot share, what a cache of the hot set saves at most) reach 1 percent of +the program's reads, or a site has 7 constant bits. + +## Firings (the harness is trusted only after these) + +Log: `/srv/builds/igneum-wt-attack/attack-f9/selftest.log` (copy in the Mac scratchpad `f9-box/selftest.log`). + +| Check | Known-pass | Known-fail | Result | +|---|---|---|---| +| 1 | the re-implemented dynamic test against `accept::check` on 300 class v4 candidates: every verdict, the first failing condition, and distinct, saturated and bias of every accepted report equal | | PASS (300 candidates, 16 rejected by accept, all equal, 1.5 s) | +| 2 | both stand-ins forced equal (closed form twice) on 200 candidates | | PASS, 0 disagreements | +| 3 | | the memory-hard stand-in gives different words: distinct 262,117 against 262,106, bias 56 against 64 on one program | PASS (they differ) | +| 4 | 50 accepted programs, none with 7 constant bits (worst 0) | 50 plants (an accepted program rewritten to `xor a,a; add a,a,256; mulhi a,b` before a load from `a`, 48 of 50 still pass rule (c)) all flagged (support 256 words, site distinct 255 or 256) | PASS for the plant; 4 of the 50 clean programs have a hot bucket (sub-row (b): that is the finding, not a harness fault) | +| 5 | taint on the devnet epoch-0 program against the hand reading of `kernel_bound.cu`: loads 7, 8, 9, 10 read r7, r4, r2, r0 (no load before them); load 31 reads r5 = r5 x r4 from instruction 12, both untouched by any load; loads 11, 13, 29, 30 read r1, r6, r4, r3, each written by an earlier load or by `mad` from r7 after load 10 | | PASS: sites (0,7) (0,8) (0,9) (0,10) (0,31) | +| card 1 | cache FNV 448274a57f508cbc, dataset head, last word and 64 samples, the 96 pack vectors | | PASS (`pod log/host.log`) | +| card 2 | the bound hash against the 64 reference lines of `igneum-pow hash-bound` (nonce_hi 0, prehash 000102..1f) | | PASS, 0 wrong | +| card 3 | the per-warp kernel on an all-equal table equals the honest kernel on 64 lanes | the two-init table: warp 0 equal, warp 1 differs in 32 of 32 lanes | PASS both | +| card 4 | | the forced kernels change the hash: forced4 64 of 64 lanes, forcedall 64, forced1 64, pair 64 | PASS (they fire) | +| card 5 | | a deliberately locality-maximising choice shows a measurable change: `pair` (one line of 4,096 saved per warp) +0.21 percent, `forced1` (31 lines) +6.8 percent, `forced4` (155 lines) +43.3 percent, `forcedall` +181.9 percent in the smoke run | PASS (measurable from one saved line up) | + +## Sub-row (a): the edges + +Run: `attack-f9 edges` over seeds `igneum-f9/0` to `igneum-f9/99999`, 8 threads on cores 28-31,76-79 in 10,000-seed +chunks under a shared hold of the box measure lock (`run-census.sh`); output `edges.part*.tsv`, summary by +`summarise.py edges`. The first 20,000 seeds (parts 0 and 1) are summarised here; the full 10^5 replaces this table +when the run ends. + +| Quantity | First 20,000 seeds | +|---|---| +| Candidates evaluated | 21,020 | +| Verdicts agreeing | 21,007 | +| Disagreements | 13 (0.062 percent of candidates; the 3 October census had 39 in 100,000 on its generator) | +| Seeds whose chosen program differs | 13 (every disagreement moves the chosen attempt, because the next attempt was accepted by both) | +| Exhausted seeds | 0 on either stand-in | +| Rejected by the closed form / by the memory-hard dataset | 1,013 / 1,014 (850 static, the rest (c)) | +| First failing (c) condition, closed form | const_bit 80, saturated 54, distinct 24, lane_const 4, bias 1 | +| Accepted margins, closed form | saturated at most 81 of 164, bias at most 120 of 136, distinct sum at least 247,335 (bound 245,760), nearly constant final bits up to 2,047 of 2,048 | +| Accepted margins, memory-hard | saturated at most 82, bias at most 115, distinct at least 247,678 | + +The 13 disagreements: 12 are `const_bit`, a final register bit equal in all 2,048 evaluations on one dataset and in +2,044 to 2,047 of them on the other (7 where the closed form accepts, 5 where the memory-hard dataset accepts); 1 is +`bias`, output bias 155 against 93 (6.9 against 4.1 sigma, the two draws' difference 2.7 sigma of sampling noise), +where the memory-hard dataset accepts. No disagreement on saturation, lane-constant sites or the distinct count: those +metrics are the same to within 20 on both datasets. What an attacker gains from a program the closed form accepts +and the live dataset would reject: a register whose final bit is pinned in 2,047 of 2,048 hashes instead of 2,048, +which no test downstream of the fold can see (the 64 output bits stay within 120 of 1,024 on every accepted program) +and which no chip can turn into skipped work; the reverse direction loses the chain a program with one pinned bit. +Either way the chosen program moves to the next attempt, which both stand-ins accept. Nothing in the attacker's +favour: the verdict's dependence on the stand-in is a 0.06 percent coin flip on a one-bit property. + +## Sub-row (b): the hot-set search + +Run: `attack-f9 hotset` over seeds `igneum-f9/0` to `igneum-f9/999999`, 8 threads on cores 32-35,80-83 in +100,000-seed chunks; output `hotset.part*.tsv`. A 2,000-seed timing sample (`hotset-timing.tsv`, seeds 5,000,000 to +5,001,999) is summarised here; the 10^6 census replaces it when the run ends. + +| Quantity | 2,000-seed sample | +|---|---| +| Programs with any hot bucket (strict) | 161 of 2,000 (8.1 percent) | +| Programs flagged at the gate reading (hot share at least 1 percent of reads) | 21 of 2,000 (1.05 percent) | +| Worst hot share | 10.3 percent of the program's reads (seed 5,000,968) | +| Sites with 7 or more constant address bits | 0 (max 0) | +| Fewest distinct addresses at a site in 2,048 evaluations | 434 | +| Most evaluations reading one address at a site | 767 of 2,048 | +| Init-determined loads in iteration 0 (programs by count) | 1: 234, 2: 573, 3: 594, 4: 368, 5: 179, 6: 42, 7: 10; none after iteration 0 | + +FINDING F9-1 (or-saturation hot words). `inspect --seed 5000968` (`inspect-5000968.log`): the load at instruction 33 +reads r4; r4 is written by `or r4 |= r0` (20), `mulhi` (27) and `or r4 |= r6` (28), and r6 itself by `or r6 |= r7` +(7). `or` is absorbing toward all ones: after two `or` writes from independent words every bit is set with +probability 7/8 and the whole register with probability (7/8)^32 = 1.4 percent; chained across iterations the mass +grows, and on this program r4 is 0xffffffff at that site in 731 to 739 of 2,048 evaluations in iterations 1 to 7 +(36 percent). The site then reads one word, `rotl(0xffffffff x M, R) & window | offset` = 0x0ca59e4c for a full +window and 0x04a59e4c, 0x08a59e4c, ... for the windowed sites; near-all-ones values add a few hundred more. The +same word family appears in every flagged program (seeds 4,000,001, 4,000,037, 4,000,040 in the selftest: `or` +writes at 54 and 55 before the load at 60, or at 1 before the load at 3). Rule (c) does not see it: the saturation +test counts final register values only (the register is overwritten before the end), the lane-constant test needs +all 32 lanes equal, the distinct test counts per lane per hash (the hot word repeats across iterations, so it +costs one distinct of 128), and the output bias stays within tolerance. The 3 October census measured an +`or_sat_frac` per program (section 7.3, max 0.0102) but the adopted rule kept only the final-value count. + +What it is worth to an attacker: nothing asymmetric. The hot words are the same for every lane that saturates, so +the GPU's coalescer and L1 already serve them without a DRAM transaction, and a chip gets exactly the same. What it +costs the design: those programs do fewer memory-hard reads than rule (c) promises (up to 10 percent fewer on the +worst program in 2,000, at least 1 percent fewer on about 1 program in 100), so the per-hash memory work of class +v4 is not the uniform 128 random reads the chip model assumes on every epoch. Gate reading: FAIL in the strict +reading (zero passing programs with a hot set under 1 percent of items), FAIL in the share reading too (programs +with a hot set capturing at least 1 percent of reads exist at about 1 percent of epochs). Proposed fix, for the hash +lane (not applied here): a per-site line in rule (c), "every load site reads at least 2,000 distinct addresses over +the 2,048 evaluations" (uniform gives 2,048 minus 0.008 expected repeats; the saturated sites read 434 to 1,855), +computed from the addresses the test already collects (one sort of 2,048 per site, 128 sites, under a millisecond); +the redraw rate rises by about the strict-reading fraction (8 percent of candidates) unless the threshold is placed +at the share reading. The alternative, dropping the `or` family from the draw table, changes the frozen weights and +is for the lane to weigh. Class check: a chip gains nothing today, but a stand-in that lets 1 percent of epochs run +with a 1 to 10 percent lighter memory side is a published-number problem (evidence row 17's per-hash reads). + +(the 10^6 numbers and the hot-share distribution replace the sample when the census ends) + +## Sub-row (c): header grinding + +### What an attacker can steer + +Only a load whose address register has not yet absorbed a dataset word is a function of the init words and the +nonce alone (taint analysis, `init_determined_sites`). On the devnet epoch-0 program these are the loads at +instructions 7, 8, 9, 10 and 31 of iteration 0; from iteration 1 every register is tainted. Over the 2,000-seed +sample the count is 1 to 7 per program, median 3, always in iteration 0 only. Everything after depends on dataset +words the miner must fetch first. The init words themselves are an FNV hash of the header and nonce_hi, so the +attacker cannot choose them, only draw them; and one draw serves a whole warp (the shuffles couple the 32 lanes), +so a per-lane draw costs 32 hashes per lane. + +### The search (CPU) + +`grind` draws K init words per warp (nonce_hi 0 to K-1 under the fixed prehash) and keeps the one with the fewest +distinct 128 B lines among the init-determined loads. Each try costs 32 lanes x (8 init + 32 prefix instructions) = +1,280 lane-instructions; the warp's hash costs 32 x (512 + 55,296) = 1,785,856 lane-instructions, the derivation +not counted. Logs: `grind-k10-crosssite.log` (lines counted across the five sites: coincidences that are at best an +L2 hit), `grind-k10-pages.log` (2 KB pages across the sites), and the `intra` runs (lines inside one load +instruction, what the coalescer merges into one transaction) that feed the card. + +| Metric | K | Warps | Mean lines or pages saved per warp (of 4,096 loads) | Warps improved | Search per warp in hashes | +|---|---|---|---|---|---| +| lines across the sites | 2^10 | 524,288 | 0.895 (0.022 percent) | 89 percent | 0.73 | +| 2 KB pages across the sites | 2^10 | 65,536 | 1.455 (0.036 percent) | 98 percent | 0.73 | +| lines inside one instruction (intra) | 2^10 | 2^17 | (pending) | | 0.73 | +| lines inside one instruction (intra) | 2^14 | 2^17 | (pending) | | 11.7 | + +### The card (RTX 5090) + +Smoke run (1 round of 2 s per variant, `pod smoke` logs): the calibration of what one saved line is worth. + +| Variant | What changes | MH/s | Against honest | W | MH/J against honest | +|---|---|---|---|---|---| +| honest | the pack kernel, one init per dispatch | 141.76 | | 482 | | +| perwarp-random | per-warp init table, no search | 141.73 | -0.02 percent | 486 | -0.7 percent | +| perwarp-k10 | per-warp table, best of 2^10 (cross-site table) | 141.74 | -0.01 percent | 487 | -1.0 percent | +| perwarp-k14 | (the same table in the smoke run) | 141.74 | -0.01 percent | 488 | -1.1 percent | +| pair | lane 1 reads lane 0's address at load 7: 1 line of 4,096 saved | 142.05 | +0.21 percent | 490 | -1.4 percent | +| forced1 | load 7 broadcast: 31 lines saved | 151.39 | +6.8 percent | 506 | +1.8 percent | +| forced4 | loads 7, 8, 9, 10, 31 broadcast: 155 lines saved | 203.11 | +43.3 percent | 530 | +30 percent | +| forcedall | every load broadcast: 3,968 lines saved | 399.61 | +182 percent | 520 | +161 percent | + +Reading: the class v4 kernel on the 5090 is bound by its random reads (141.8 MH/s x 128 = 18.1 G reads per second, +the card's measured random-read ceiling in `docs/bench-log.md`), and a load instruction completes when its slowest +lane's transaction returns, so one saved line is worth about 0.2 percent of rate, 31 lines 6.8 percent, and the +five init-determined loads fully coalesced 43 percent. That ceiling is unreachable by search: it needs the 32 +lanes' 28-bit addresses to fall in one line at five sites, probability 2^-115 per draw. What a draw can reach is one +coalesced pair at one site (probability 5 x C(32,2) / 2^23 = 3 x 10^-4 per try, about 3,400 tries per pair); +two pairs need about 6 million tries, m pairs about 3,400^m / m! tries. One pair is worth 0.2 percent of one warp's +hash and costs 3,400 x 1,280 lane-instructions = 2.4 hashes of search. The measured per-warp tables (5 rounds of +8 s, pending) are the direct check. + +(the full run's table replaces the smoke run when it ends) + +## Consequences per tier + +(filled in with the verdict) + +## Logs + +| Log | Path | +|---|---| +| selftest, inspect, grind, census parts and drivers | `/srv/builds/igneum-wt-attack/attack-f9/` on igneum-build-1 (`selftest.log`, `inspect-*.log`, `grind-*.log`, `edges.part*.tsv`, `edges.driver.log`, `hotset.part*.tsv`, `hotset.driver.log`, `hotset-timing.tsv`, `ref.txt`, `table-*.bin`) | +| card smoke run and full run | the pod's `/workspace/f9/podjob/smoke/log/` and `log/` (`run.log`, `nvcc.log`, `host.log`, `power.csv`, `power-by-variant.txt`, `sha256.txt`), copied to `/srv/builds/igneum-wt-attack/attack-f9/pod/` at the end | + +### The card, the measurement (RTX 5090 pod ap-f9, `pod/host.log`, sha256 83b1372c..., copied to +`/srv/builds/igneum-wt-attack/attack-f9/pod/`) + +Per-warp header grinding at K = 2^14 draws per warp against the honest kernel, 5 interleaved rounds of 8 s each: +141.62 against 141.61 MH/s, +0.004 percent of rate, sd 0.003, at 11.7 hashes of search per hash. The unreachable +ceiling (the five init-determined loads fully coalesced, `forced1` extended) is +43 percent. Gate: the grinding gain +under 1 percent of rate at any search cost. Sub-row (c): PASS. Pod time about 1 h 50 min from 09:02 UK; destroyed on +the lane's done line. + +## Full censuses (box 1 parts 0 to 7 and 0 to 6; box 2 parts `edges-b2`, `hotset-b2` after the lane's move to +build-2 at 12:2x UK; `summarise.py` over all parts, 12:5x UK) + +### (a) The edges on generator 4, 10^5 seeds + +| Quantity | 100,000 seeds | +|---|---| +| Candidates evaluated | 105,064 | +| Verdicts agreeing | 105,030 | +| Disagreements | 34 (0.032 percent of candidates; the 3 October census had 39 in 100,000 on its generator) | +| By kind | `const_bit` 29 (closed form accepts 17, memory-hard accepts 12); `bias` 4 (3 and 1); `lane_const` 1 (memory-hard accepts) | +| Seeds whose chosen attempt differs | 34, every one moving to the next attempt, which both stand-ins accept; exhausted 0 on either | +| Rejected, closed form / memory-hard | 5,046 / 5,048 (static 4,201; then const_bit 424 / 428, saturated 275 / 275, distinct 112 / 112) | +| Accepted margins, closed form | saturated at most 148 of 164, bias at most 121 of 136, distinct sum at least 247,335 (bound 245,760) | +| Accepted margins, memory-hard | saturated at most 157, bias at most 126, distinct at least 247,571 | + +The 20,000-seed reading holds at 10^5: the stand-ins disagree on a one-bit property (a final register bit pinned in +2,047 of 2,048 hashes against 2,048) and on four programs' output bias within sampling noise, never on saturation +or the distinct count, and never in the attacker's favour. Gate: the 39 edge disagreements reproduced and bounded. +Sub-row (a): PASS. + +### (b) The hot-set search, 10^6 seeds + +| Quantity | 1,000,000 seeds | +|---|---| +| Programs with any hot bucket (strict) | 75,400 (7.5 percent) | +| Programs flagged at the gate reading (hot share at least 1 percent of reads, or 7 constant address bits) | 11,696 (1.17 percent) | +| Worst hot share | 17.3 percent of the program's reads (seed 842871) | +| Hot-share bands (block init) | 0: 932,106; under 0.1 percent: 11,721; under 0.5: 3,645; under 1: 41,581; 1 percent and over: 10,947 | +| Sites with 7 or more constant address bits | 0 (max 6) | +| Fewest distinct addresses at a site in 2,048 evaluations | 43 | +| Most evaluations reading one address at a site | 1,838 of 2,048 | +| Mean hot share over programs | 0.063 percent | + +Gate: zero passing programs with a hot set under 1 percent of items. FAIL on the class v4 stream by the letter: +11,696 passing programs concentrate 1 percent or more of their reads on a hot set. The mechanism is F9-1 above, +the or-saturated load source, which is the same fault class the F8 row found from the cross-hash histogram +(AP-F8-1: a load whose source's last writer is lossy); the two harnesses found it independently, one from the +program's address trace per site, one from the item histogram across hashes. F9-1 therefore merges into AP-F8-1, +and the fix is the amendment shipping in 0.3.20 (a load's source drawn only from registers whose last writer injects +or is a rotate). Sub-row (b): FINDING (AP-F8-1 class); re-gated on the amended stream with this harness below. diff --git a/docs/analysis/attack-pass/o114-i7-9700K-2026-10-07.log b/docs/analysis/attack-pass/o114-i7-9700K-2026-10-07.log new file mode 100644 index 00000000..cd997b46 --- /dev/null +++ b/docs/analysis/attack-pass/o114-i7-9700K-2026-10-07.log @@ -0,0 +1,53 @@ +# O-1.14 bench start 2026-10-07T08:49:03Z host root@ssh9.vast.ai +Warning: Permanently added '[ssh9.vast.ai]:35608' (ED25519) to the list of known hosts. +Welcome to vast.ai. If authentication fails, try again after a few seconds, and double check your ssh key. +Have fun! +# binary copied, sha256 6d28678359d3d6bd158b245f7e522d6f2a5b0704d9997c0fb50ebc3471a9ebe5 +Welcome to vast.ai. If authentication fails, try again after a few seconds, and double check your ssh key. +Have fun! +# cpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz +# cores: 8 mem: 31 GB +# glibc: ldd (Ubuntu GLIBC 2.39-0ubuntu8.9) 2.39 +# clock MHz: 4169.856 + 08:49:07 up 20 days, 10:27, 0 user, load average: 0.13, 0.07, 0.05 +## --class v2 08:49:07Z +Welcome to vast.ai. If authentication fails, try again after a few seconds, and double check your ssh key. +Have fun! +cache: fill 283.0 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e +warp base 0: single cold run 1.582 ms, 4096 items derived, lane0 42246ba99fc58e4f lane31 b08446b1f2de7793 +warp base 4096: single cold run 1.392 ms, 4096 items derived, lane0 3d3903e310ca038f lane31 61c242509efdccdd +warp base 1000000: single cold run 1.374 ms, 4096 items derived, lane0 f218c1bd58e6dfe0 lane31 6c3b2c11adfbfcac +CPU verify: 1.280 ms per 32-lane warp, avg of 50 (checksum 19297e99c7b9a55e) +## --class mx8 08:49:09Z +Welcome to vast.ai. If authentication fails, try again after a few seconds, and double check your ssh key. +Have fun! +cache: fill 276.6 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e +warp base 0: single cold run 5.394 ms, 4096 items derived, lane0 19b56348bc85304d lane31 359192708e4f754a +warp base 4096: single cold run 5.285 ms, 4096 items derived, lane0 62fb132a9943127a lane31 7d7866cb9cfca8ff +warp base 1000000: single cold run 5.293 ms, 4096 items derived, lane0 86b6cb0e13d89b03 lane31 9c004678515e44ec +CPU verify: 5.267 ms per 32-lane warp, avg of 50 (checksum 653a23f7ee1c8c63) +## --program-class v4 08:49:11Z +Welcome to vast.ai. If authentication fails, try again after a few seconds, and double check your ssh key. +Have fun! +cache: fill 276.6 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e +warp base 0: single cold run 6.334 ms, 4096 items derived, lane0 2576769ee4a14c8d lane31 c58ddcb717dd3370 +warp base 4096: single cold run 6.198 ms, 4096 items derived, lane0 1ce77a600ec573b4 lane31 03600a05ffba0055 +warp base 1000000: single cold run 6.174 ms, 4096 items derived, lane0 6b390e64bbdd91ce lane31 91c944d603539c62 +CPU verify: 6.006 ms per 32-lane warp, avg of 50 (checksum 17e36e7905b81375) +## --class dr368 08:49:14Z +Welcome to vast.ai. If authentication fails, try again after a few seconds, and double check your ssh key. +Have fun! +cache: fill 276.3 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e +warp base 0: single cold run 5.540 ms, 4096 items derived, lane0 c77c7625bbe0f452 lane31 c8e84ff2655d9934 +warp base 4096: single cold run 5.433 ms, 4096 items derived, lane0 7229bd981a5786ca lane31 1c5495083ae60453 +warp base 1000000: single cold run 5.430 ms, 4096 items derived, lane0 5533769c9cdbf0a7 lane31 2426905704457b11 +CPU verify: 5.426 ms per 32-lane warp, avg of 50 (checksum 94fcbf0a77e03bdc) +## --class dr736 08:49:16Z +Welcome to vast.ai. If authentication fails, try again after a few seconds, and double check your ssh key. +Have fun! +cache: fill 275.7 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e +warp base 0: single cold run 10.290 ms, 4096 items derived, lane0 e23d389f3eea0c83 lane31 6605db059b381bd9 +warp base 4096: single cold run 10.072 ms, 4096 items derived, lane0 fdb4b214da8ce292 lane31 db698d03437d74f7 +warp base 1000000: single cold run 10.056 ms, 4096 items derived, lane0 534671b1bf5cea36 lane31 733b123353f13d21 +CPU verify: 10.042 ms per 32-lane warp, avg of 50 (checksum bf79909c25836153) +# O-1.14 bench end 2026-10-07T08:49:19Z diff --git a/docs/analysis/block-rate-devnet2.md b/docs/analysis/block-rate-devnet2.md index 3c0524d9..8434e512 100644 --- a/docs/analysis/block-rate-devnet2.md +++ b/docs/analysis/block-rate-devnet2.md @@ -1,6 +1,6 @@ # Block rate on Devnet 2: 10 blocks per second against 1, on the rented fleet -6 October 2026, the project lead's experiment (through the coordinator, 17:5xZ): "Devnet 2 at a higher block rate for solo miners +6 October 2026, the founder's experiment (through the coordinator, 17:5xZ): "Devnet 2 at a higher block rate for solo miners (Kaspa's answer)". Branch `gpu-fleet`; the node profile on the fork branch `devnet2-bps` (1279a1d6 on release-0.3.14-node 4c6b129d): a suffixed devnet started with `IGNEUMD_DEVNET_BPS=10` (or 5) runs `BlockrateParams::new::<10>()` with the TenBps subsidy and target time, Kaspa's Crescendo-style constants for that rate (k 124, merge depth, sample rates, diff --git a/docs/analysis/ca3-v4-uniform.md b/docs/analysis/ca3-v4-uniform.md new file mode 100644 index 00000000..562e4f1f --- /dev/null +++ b/docs/analysis/ca3-v4-uniform.md @@ -0,0 +1,62 @@ +# AP-F8-1 under the windows-union model: the hot set is the load source, not the window (7 October 2026) + +Branch `ca3-v4-uniform` from master b92a5fd4, worker "v4-hash", on the attack-pass finding AP-F8-1 (`docs/analysis/attack-pass/f8-uniform.md`, branch attack-pass; logs `/srv/builds/igneum-wt-attack/target-attack-f8/log/`). Main's rulings bound this file: no generator change to class v4 on the live devnet; the analysis and its harness only. Every GPU-free number here is arithmetic on F8's logged counts or a run of the static census tool `tools/ca3-v4-uniform/` on igneum-build-1 (built through `tools/build-remote.sh`, rule R1); the chip figures are the terms of `docs/analysis/chip-model-v3.md` and are approximate. + +## 1. The null F8's numbers must be read against + +Layer 8 (`docs/plans/era-layout.md` 1.4, spec 01 1.13.1 as proposed) gives every load site a window draw `k_off = below(3)`: the site reads the whole dataset, an aligned half or an aligned quarter, at a 2^26-word floor. A quarter-window site concentrates its reads 4x on its quarter and a half-window site 2x on its half, by design; the union of the 16 windows is the whole dataset. For p1 (the devnet epoch-0 program, id c120d7963abdcd96) the 16 draws `0:2:1 1:1:1 2:1:1 3:1:1 4:0:0 5:1:1 6:0:0 7:2:2 8:1:1 9:1:1 10:2:0 11:0:0 12:0:0 13:0:0 14:2:0 15:1:1` (site:k:offset) give an expected read density by quarter of 3.25 : 2.25 : 5.75 : 4.75 sixteenths of the flat mean, which at 2^26 nonces (512 reads per item flat) is 416, 288, 736 and 608 reads per item. The top-f share of a Poisson mixture with those means (`uniform-model.txt`, exact Poisson for p1, a normal approximation for the census): + +| Share of all reads on the top f of items, 2^26 nonces | Flat Poisson (F8's control) | Windows-union model, p1 | F8 measured, p1 | Beyond the window model | +|---|---|---|---|---| +| f = 0.1 percent | 0.115 | 0.160 | 0.520 | +0.36 | +| f = 0.5 percent | 0.565 | 0.784 | 1.515 | +0.73 | +| f = 1 percent | 1.120 | 1.553 | 2.495 | +0.94 | + +So the window model moves the null from 0.115 to 0.160 percent at f = 0.1 percent (1.39x, not F8's 4.05x) and from 1.12 to 1.55 at f = 1 percent; it explains the 64-item-bucket sigma of p2 that F8 already attributed to the window layer, and it explains every per-site attribution row of p1 except one: sites with a half window over the hot region land 0.20 percent of their reads in the top 0.1 percent (sites 1, 2, 3, 5, 9 at 0.201), quarter-window sites 0 or about 0.4 (sites 0, 10, 14 at 0.000, site 7 at 0.241 straddling), whole-dataset sites 0.10. Site 15 lands 6.374 percent. The excess over the model (+0.36 at f = 0.1 percent) is one site. + +## 2. The 153x item is the load source, and the model predicts it to the item + +p1's site 15 is the load at instruction 63 (`load dst=3 src=6`, window half 1). Its source r6 was last written at instruction 61: `or dst=6 src=4` (`r6 |= r4`, `verify.rs` Op::Or), after a fresh dataset load into r6 at 47. An OR of two near-uniform registers sets each bit with probability 3/4, so the source takes the all-ones value with probability (3/4)^32 = 1.0e-4 per read and the values of popcount 31, 30, ... with 32, 496, ... times (3/4)^k (1/4)^(32 - k). The era map `y = rotl(x * 0x9ad30d99, 29)`, the half window and the interleave split (`memhard::Layout::split`, positions 0, 2, 12, 13) send x = 0xffffffff to item 0xca5b92: F8's hottest item exactly. F8's next seven items (0x8a5b92, 0xaa5b92, 0xba5b92, 0x825b92, 0xe65b92, 0x985b92, 0xbcc392) are exactly the seven one-zero-bit sources whose zero bit survives the window mask (bits 29, 28, 27, 26, 25, 24 and 15): 7 of 7. The measured count fixes the bit bias: 78,479 reads of 2^26 x 8 site-15 reads is p^32 at p = 0.7585 (r4 is slightly biased itself), and at that p the popcount model predicts 77,348 all-ones reads and 4.86 percent of site 15's reads into the top 0.1 percent of items (measured 6.37; 3.92 at p = 3/4). Per hash that is 4.86 / 16 = 0.30 percent of all reads, and 0.160 + 0.30 = 0.46 against F8's 0.520 at f = 0.1 percent; at f = 1 percent 14.5 / 16 = 0.90, and 1.55 + 0.90 = 2.46 against 2.495. + +The same arithmetic for the other lossy writers (`uniform-model.txt`): a `mul` last writer zeroes the low bits by the operands' trailing zeros, so 1.07 percent of the site's reads land on the 0.1 percent of values with 10 or more trailing zeros (p2's `mul`-sourced sites 0 and 14 measured 0.971 and 0.966 percent); a `mulhi` last writer is dense near zero, 0.79 percent on the lowest 0.1 percent of values. An `or` whose operand was itself last written by `or` compounds the bias (3/4 to 7/8 to 15/16): p3's site 15 (`or` at 30, the load at 62) puts 72.4 percent of its reads into the top 0.1 percent, 4.6 percent of all reads on 16,777 items. + +This is a fault class, not the window model: the acceptance rule's part (a) (`accept.rs` check_stale_loads) takes any write as a fresh source, and part (c)'s saturation count looks at the 16,384 final register values, not at a load's source mid-program, so an `or`, `mul` or `mulhi` as a load's last writer passes. The per-hash distinct-address check still holds (p1 127.999 items per hash; p3 127.97: a saturated site repeats its item inside a hash), and the acceptance rule's floor of 120 distinct of 128 admits exactly one site repeating its item in all 8 iterations and no more. + +## 3. How common it is: the static census (`tools/ca3-v4-uniform`, 1,024 chain-shaped class v4 programs plus F8's p1 to p3) + +For every load site, the op that last wrote its source in execution order (base instructions before it, else the shadow block of the previous iteration, else the base instructions after it): injecting (add, sub, xor, mad, shfl, load), bijective (rotl, rotr) or lossy (or, mul, mulhi). Run on igneum-build-1 (`uniform-census.txt`, binary sha256 ce9f83fe... then the narrowed chain rule). + +| Census over 1,024 programs | Count | Share | +|---|---|---| +| Load sites by last writer: injecting / bijective / lossy | 11,368 / 2,121 / 2,943 of 16,432 | 69 / 13 / 18 percent; 2.87 lossy sites per program | +| Programs with at least one lossy-sourced load | 992 | 96.6 percent | +| ... with an `or`-sourced load (p1's class, 0.30 percent of all reads per site) | 498 | 48.5 percent | +| ... with an `or`-of-`or` chain (p3's class, about 4.5 percent of all reads per site) | 50 | 4.9 percent | +| ... with a `mul`-sourced load (0.067 percent per site) / a `mulhi`-sourced load (0.049) | 751 / 661 | 73.1 / 64.4 percent | +| Predicted S_0.1 percent (window model plus the lossy sites): median / 90th / 99th / max | 0.45 / 0.88 / 5.29 / 9.82 percent | against the window model's 0.115 to 0.251 | +| p1 / p2 / p3 predicted against F8 measured | 0.579 / 0.323 / 4.72 | 0.520 / 0.272 / 4.60 | + +F8's proposed gate (the top 0.1 percent within 1.2x of the window-model control on every one of 64 seeds) fails 96.6 percent of today's programs, because any lossy-sourced site alone exceeds it (0.16 + 0.05 at the least); it is a generator change in a gate's clothing. A 2x bound fails 69.7 percent, 3x 48.9 percent; a bound of S_0.1 percent at or under 1 percent of all reads fails 6.9 percent (the `or` chains and the multi-`or` programs). The static rule "no load whose source's last writer is `or`" fails 48.4 percent; "no lossy last writer" 96.6 percent. + +## 4. What the skew is worth to a chip (chip-model-v3.md terms, approximate) + +A hot-set cache of the top 0.1 percent of items is 16,777 items x 64 B = 1.07 MB of SRAM, 0.53 mm^2 and $0.25 at 0.49 mm^2 and $0.23 per MB. It serves 0.52 percent of p1's reads (0.16 of them the window model's), 4.6 percent of p3's. The hash is latency-bound on its dependent reads, so a read served on die is time saved: a chip gains at most 1.005x on p1 and 1.048x on p3 from the cache. The ceiling under the live rule: part (c)'s 120-of-128 floor admits one site repeating its item in all 8 iterations and no more (two saturated sites fail it), so at most 8 of 128 reads, 6.25 percent, can sit on a constant item, and a chip's edge from this whole class is at most 1 / (1 - 0.0625) = 1.067x, in 64 bytes of SRAM, on the hours whose program carries such a site. The public claim rests on 2x margins (chip-model-v3.md); 1.067x does not move it, and the union of the windows is still the whole dataset every hour, so no window-level cache exists. What moves: per tier nothing in rate or watts (the honest card reads the hot item from L2 as the chip would), and the 5 percent rule of 2.0 is untouched. + +## 5. The two options for the flip, priced (main's ruling 3; nothing ships on this without the founder's word) + +| Option | What changes | Cost | Risk | +|---|---|---|---| +| A. A class amendment in 0.3.19 before the flip: the generator draws a load's source from the registers whose last writer injects (or rule (a) tightened to the same), class v4 re-pinned | a new program stream: new vectors, the seven gate packs re-exported, the six gates again (the hash side G1 to G3 and the verifier re-run here in about an hour of Mac and PC 2 time; G4 to G6 the node lane), every node before the flip by the one-box-at-a-time fleet rule | hours of gate time, a fleet rollout, the 0.3.19 ship on the line | a node that misses the build splits the chain at the flip; the fix itself is small (one draw rule) | +| B. Hold v4 at the floor as it is; the source rule in class v5 | nothing on the devnet; the attack-pass record carries the window null and the bound | a hot set on 48 percent of hours worth up to 1.005x to a chip, on 5 percent of hours up to 1.05x, 1.067x at the rule's ceiling, no chain risk | the public line must state the bound, not "uniform" | + +The number that decides it: 1.067x at the ceiling against the 2x margin of the chip claim. Recommendation: B, with the v5 item below, unless the founder wants the tail tight now. + +## 6. The acceptance bound for the next class (main's ruling 4) + +Definition: for a program, H = W_0.1(windows) + sum over load sites of h(last writer of the source), with W from the Poisson mixture of the 16 window draws (0.115 to 0.251 percent at 2^26 nonces) and h = 0.30 percent for `or`, 4.5 for an `or` chain, 0.067 for `mul`, 0.049 for `mulhi`, 0 for an injecting or bijective writer (the figures of section 2 at the measured bias). The bound: H at or under 1.2 x W, which is the static rule "every load's source was last written by an injecting op or a rotate" (any lossy writer breaks 1.2x). Its cost as a rejection rule on today's stream: 96.6 percent of candidates, about 30 attempts per seed on average. The cheaper form is a generator draw, not a rejection: draw a load's source from the registers whose last writer injects (today's rule draws from every written register), which costs no attempts and leaves rule (a) as it is. Either way the 64-seed census of F8's phase E is the gate, with the dynamic check extended to count saturated load sources over the 64 units beside the final values. + +## 7. What is unverified + +- The per-site h figures are the popcount and trailing-zeros models at the biases F8 measured on p1 and p2; p3's chain figure is F8's measurement, not a model. F8's phase E (64 seeds, dynamic) is the test of the whole table. +- The window model's top-f shares for the census use a normal approximation per quarter (p1's exact Poisson 0.160 against 0.159). +- No GPU run and no timing here; every number is a count or arithmetic. diff --git a/docs/analysis/card-lifetime-2026-10-05.md b/docs/analysis/card-lifetime-2026-10-05.md index 5d80ca55..497ae998 100644 --- a/docs/analysis/card-lifetime-2026-10-05.md +++ b/docs/analysis/card-lifetime-2026-10-05.md @@ -69,7 +69,7 @@ Proving is a separate budget (the 15.6 GB peak the 12 GB mine-and-prove question ## 4. Table 3: the public sentences against the numbers -| Where | Sentence now | What the tables give | Proposed sentence (the project lead decides the wording) | +| Where | Sentence now | What the tables give | Proposed sentence (the founder decides the wording) | |---|---|---|---| | `site/index.html` 443 | Memory: "2 GB, fixed" (RandomX) / "2 GB, growing" (Igneum) | 2 GiB at genesis, plus 0.5 GiB a year on average under either option | "2 GB, growing 0.5 GB a year". The row is right; the rate is the useful addition | | `site/index.html` 461 | "Any 4 GB card, approximate." | True at genesis (2,584 to 2,834 MiB of a 3,072 MiB budget). Ends at 1 to 1.5 years under (a), year 4 under (b) | "Any 4 GB card at launch, 8 GB for the long run, approximate." | diff --git a/docs/analysis/chip-model-v3.md b/docs/analysis/chip-model-v3.md index b35e6bc1..2ac5c9c0 100644 --- a/docs/analysis/chip-model-v3.md +++ b/docs/analysis/chip-model-v3.md @@ -294,7 +294,7 @@ low on this result; it stays a reserve family. What does move the `f = 1` rows, | Read granularity | The chip pays the same 32-byte atom the 5090 pays; the 9070 XT pays 64. Wider honest reads (w16, measured, layer 1) give the chip nothing and the 5090 nothing; w64 made the 5090 bandwidth-bound (71.9 MH/s) | w64 costs the 5090 47 percent | Not a lever; the decision to stay at 4 B stands | | Latency | A longer chain (more reads per hash) scales the chip's rate and the card's rate together; lane state is 64 B, so lanes are free to the chip | Nothing per se | Not a lever: the rate per chip is lanes / latency on both sides and the chip has more lanes per watt | | The denominator: the 5090's watts at the hash | The gain is 2.40 microjoules over the chip's 0.47; the card's 326 W is 92.9 percent utilisation spinning on loads. At a 250 W cap holding 136.1 MH/s the gain reads 3.9x (GDDR7) and 5.7x (HBM3); at 200 W, 3.2x and 4.6x | None if the rate holds under the cap; the measurement is one PC 2 job (`nvidia-smi -pl 200, 250, 326`, two minutes each, STATUS lines as the rate) | The first measurement to run; it moves every row and costs nothing. Owed (PC 1 is not released; PC 2's budget is the coordinator's) | -| Program work in the latency shadow | The hash hides 512 ops per hash behind 128 reads; the 5090 could hide 330,000 (45.2 T / 136.1 M) before compute binds, the M5 Max about 290,000 and the 9070 XT about 650,000 (their ALU budgets approximate, from memory). Work in the shadow is free in hash rate and costs the card watts it now wastes: at N ops per hash the card rises from 326 toward 575 W (linear, approximate) and the chip must add a core that runs the per-epoch random program, at k times the GPU's 5.5 pJ per op (the 5090's marginal ALU energy, (575 - 326) / 45.2 T). At N = 100,000: the card 401 W, 2.95 microjoules; the chip 1.02 at k = 1, 0.83 at k = 1.5; gain 2.9x and 3.5x. At N = 200,000: 477 W, 3.50; chip 1.57 and 1.20; gain 2.2x and 2.9x. At N = 330,000: 575 W, 4.22; chip 2.28 and 1.68; gain 1.85x and 2.5x | Hash rate none while every card stays latency-bound (under about 290,000 on the M5 Max); watts up to TGP; the verifier N x 32 ops per warp: 3.2 M at N = 100,000, under 1 ms at the 18 G op/s the x8 verifier shows (38 M ops in 2.08 ms), inside the 10 ms gate; `INSTR_COUNT` and `ITERATIONS` are prototype values to be fixed at gate 1 (spec 1.4) | The only lever that moves the f = 1 row toward 2x, and only if the chip's core is no better than a GPU's on a random program (k near 1: RandomX's argument, and the project lead's goal in the brief's words, "build a better GPU than NVIDIA"). It reaches 1.85x at the 5090's full ALU budget and k = 1, not under; combined with a 250 W cap it reads about 1.4x (approximate). It is item 2's idea applied to the program, not to the item derivation | +| Program work in the latency shadow | The hash hides 512 ops per hash behind 128 reads; the 5090 could hide 330,000 (45.2 T / 136.1 M) before compute binds, the M5 Max about 290,000 and the 9070 XT about 650,000 (their ALU budgets approximate, from memory). Work in the shadow is free in hash rate and costs the card watts it now wastes: at N ops per hash the card rises from 326 toward 575 W (linear, approximate) and the chip must add a core that runs the per-epoch random program, at k times the GPU's 5.5 pJ per op (the 5090's marginal ALU energy, (575 - 326) / 45.2 T). At N = 100,000: the card 401 W, 2.95 microjoules; the chip 1.02 at k = 1, 0.83 at k = 1.5; gain 2.9x and 3.5x. At N = 200,000: 477 W, 3.50; chip 1.57 and 1.20; gain 2.2x and 2.9x. At N = 330,000: 575 W, 4.22; chip 2.28 and 1.68; gain 1.85x and 2.5x | Hash rate none while every card stays latency-bound (under about 290,000 on the M5 Max); watts up to TGP; the verifier N x 32 ops per warp: 3.2 M at N = 100,000, under 1 ms at the 18 G op/s the x8 verifier shows (38 M ops in 2.08 ms), inside the 10 ms gate; `INSTR_COUNT` and `ITERATIONS` are prototype values to be fixed at gate 1 (spec 1.4) | The only lever that moves the f = 1 row toward 2x, and only if the chip's core is no better than a GPU's on a random program (k near 1: RandomX's argument, and the founder's goal in the brief's words, "build a better GPU than NVIDIA"). It reaches 1.85x at the 5090's full ALU budget and k = 1, not under; combined with a 250 W cap it reads about 1.4x (approximate). It is item 2's idea applied to the program, not to the item derivation | | The clock (item 4) | The f = 1 chip is a commodity-memory controller project: by the Ethash precedent, 32 months to a first chip at the largest prize, and a chip over 2x at 65 months | None | The issuance trigger and the share-pattern detector matter more than any item-derivation change | So: item 2 can wait; the power-cap measurement runs first; the program-length lever is the Counter ASIC 3.0 design diff --git a/docs/analysis/ci-failures-2026-10-06.md b/docs/analysis/ci-failures-2026-10-06.md index 18c9e45e..b273b370 100644 --- a/docs/analysis/ci-failures-2026-10-06.md +++ b/docs/analysis/ci-failures-2026-10-06.md @@ -110,7 +110,7 @@ run drops from about three minutes to about one. The hosted runner then serves o pipeline (MSVC, WebView2, Inno Setup, PowerShell 5.1, which a Linux box cannot provide). The flip is main's call after the 0.3.15 cut, per build-server.md section 7.1. -## 6. The build box's own red rows (the project lead, 22:3x UK: "also make sure we are fixing and learning from all the errors here") +## 6. The build box's own red rows (the founder, 22:3x UK: "also make sure we are fixing and learning from all the errors here") Source: `/srv/builds/_log/builds.jsonl` on igneum-build-1, 139 rows from the first build at 18:38 UK to 22:11 UK on 6 October; 34 with a non-zero exit. Until this change the box kept no output of a run (it streamed to the agent's terminal), diff --git a/docs/analysis/difficulty-2026-10-04-oscillation.md b/docs/analysis/difficulty-2026-10-04-oscillation.md index ea69e98b..a49297fb 100644 --- a/docs/analysis/difficulty-2026-10-04-oscillation.md +++ b/docs/analysis/difficulty-2026-10-04-oscillation.md @@ -288,7 +288,7 @@ the height arrives. (26640/28640, a follower, lowest risk), the seed (`/opt/igneum/v4/bin`, `infra/seed-nodes/stage-v4.sh`), Mac node 1 (26610/26611), PC 1's node (its launcher's `igneumd.exe`; PC 2 mines through the Mac node and needs nothing). Each node gets the same `"difficulty_v2_activation_daa": N` in its override file. - the project lead restarts the live processes; this entry does not. + The founder restarts the live processes; this entry does not. 3. Watch the observer's difficulty events across the height and the next epoch boundary; with a miner joining inside an epoch the floor and the bursts of section 1 must not return. diff --git a/docs/analysis/era-vdf-2026-10-07.md b/docs/analysis/era-vdf-2026-10-07.md new file mode 100644 index 00000000..989e59d1 --- /dev/null +++ b/docs/analysis/era-vdf-2026-10-07.md @@ -0,0 +1,86 @@ +# The era VDF: built, measured and gated (7 October 2026) + +Era VDF lane, 7 October 2026, from the attack pass's F7 row (`docs/analysis/attack-pass/f7-era.md`, sub-row a: the node's era seed was a plain chain block hash, grindable with one block of hash at no delay, and the 1-hour VDF of spec 04 section 4.4 did not exist in the node). Repository branch `era-vdf` (this record, the spec text, the harness `tools/era-vdf/`, the fast-time fields); node fork branch `era-vdf-node` on the 0.3.19 line (`release-0.3.19-node` dc141409). Every number below names its log on igneum-build-1 under `/srv/builds/igneum-wt-era-vdf/ev-*/`. + +## 1. What was built + +| Piece | Where | What | +|---|---|---| +| The integer | `consensus/core/src/era_vdf/bigint.rs` | a fixed-width signed integer (40 limbs, 2,560 bits) on the stack: add, sub, mul, shifts, Knuth division with floor, truncated, exact and Euclidean remainders, the extended gcd and the partial extended gcd with Lehmer's word steps (chiavdf `xgcd_partial.c`), modpow, sqrt and the fourth root, Miller-Rabin with the first 30 primes as bases; every operation checked against `num-bigint` on 20,000 random operands of the class group's sizes, the known-failed shapes first | +| The class group | `era_vdf/classgroup.rs` | `proto-vdf/src/classgroup.rs` (3 October 2026) on the fixed-width integer: NUDUPL and NUCOMP ported line by line from chiavdf's `qfb_nudupl` and `qfb_nucomp`, the plain duplication and Cohen 5.4.7 kept as the oracles the tests hold them to on random forms at 256, 512 and 1,024 bits; serialization as sign byte plus fixed width, 258 bytes a form | +| Wesolowski | `era_vdf/wesolowski.rs` | eval with serialized checkpoints (at most 2^16, 17 MB), the 12-bit-digit block prover bucketed per residue class and parallel over them, the naive prover as the oracle, verify; T + 1, another y, another pi and another input refused | +| The hash chain | `era_vdf/hashchain.rs` | scheme 1: T sequential SHA-256 applications from a tagged start; verification by recomputation; one step short refused | +| The scheme byte and the seed | `era_vdf/mod.rs` | `vdf_scheme` 0 and 1, `EraVdfProof` and its wire form, `era_vdf_input` (the chain's BLAKE2b keyed `IgneumEraVdfInput` over `chain_id || n || the day's blue hashes`), `era_seed_of` = SHA-256 of the scheme byte, the input, T and y | +| The switch | `consensus/core/src/config/params.rs`, `igneum.rs` | `pow_era_blocks` and `pow_era_lead` as override fields (the constants everywhere; in the digest when they differ), `era_vdf_activation_daa` (never), `vdf_scheme` (0), `era_vdf_t` (the reference T); the three in the digest once the activation is set (the 0.3.15 rule); installed with the PoW schedule | +| The node side | `consensus/src/processes/era_vdf.rs`, `model/stores/era_vdf.rs` | the cut rule (the chain block below the cut, memoised and re-validated by reachability), the day-of-blues input (memoised per cut block), the evaluator thread started by the virtual processor a quarter of the lead past the cut, the record store (one row per era), the header processor's wait when a header arrives before the record, the template's `era_seed` None while evaluating, `submit` for a record from outside (verified against this chain's input) | +| The template and the miner | `PowEpochInfo`, `RpcPowEpochInfo`, `rpc.proto` fields 37 to 44, `igneum-miner` | the era schedule, the VDF's state, scheme, T and input in every template; the miner holds while the node reports no era seed ("era VDF: the node is still evaluating"); `igneum-miner vdf bench|eval|verify` with the node's own code | +| The harness | `tools/era-vdf/reroll.mjs` | the F7 re-roll harness against the REAL era cut (era 120 DAA, lead 20 on the merged fast-time file; ports 30100 and up, suffix 1010), `--vdf off` the stand-in, `--vdf on` the VDF at a fast T, the adversary running the node's evaluator over its candidate before publishing | + +## 2. The parameters + +Measured 7 October 2026 on igneum-build-2 (AMD EPYC 9454P, 96 threads, Ubuntu 24.04), one core under `/srv/builds/_bin/lease cores 31` at nice 10 while the box ran other lanes' suites (load 25 to 75), with the node's own code (`igneum-miner vdf bench`, logs `ev-vdf-bench3.log`, `ev-vdf-bench5.log` in this lane's scratch) and chiavdf 7e62ce14 built on the box against GMP 6.3.0 (`ev-chiavdf`). + +| Parameter | Value | Label | +|---|---|---| +| Group | class group, 1,024-bit prime discriminant `D = -HashPrime("igneum-era-discriminant" \|\| input)`, `\|D\| = 7 mod 8` | Implemented (spec 4.2) | +| Generator, Fiat-Shamir prime, proof plan | `(2, 1, (1 - D) / 8)`; 256 bits; 12-bit digits, at most 2^16 serialized checkpoints (17 MB) | Implemented | +| Proof on the wire | 529 bytes: scheme (1), T (8), two 258-byte forms with 2-byte lengths; `y` and `pi` 258 bytes each | Measured | +| Scheme byte | `vdf_scheme` 0 = class group, 1 = hash chain; genesis 0 everywhere | Implemented | +| Reference rate, scheme 0 | 30,589 and 40,117 squarings/s in two 10-s runs on the box core (the spread is the box's load); 30,000 is the reference | Measured | +| `T_era`, scheme 0 | 3,600 x 30,000 = 108,000,000 squarings (`ERA_VDF_T_CLASS_GROUP`): 60 min at the reference, 45 at the faster run | Measured, set | +| Prove, scheme 0 | eval + prove 11.6 s at T 401,167 (eval 10.0 s): the single-thread block prover is about 14 percent of the evaluation, parallel over residue classes in the node (up to 8) | Measured | +| Verify, scheme 0 | 21.9 and 22.6 ms with the group held (mean of 20); 184 ms with the discriminant derived, the derivation being 161 to 167 ms, once per era | Measured (section 5 for the gate) | +| Reference rate, scheme 1 | 16.2 and 17.0 million SHA-256/s (SHA-NI); 16,000,000 is the reference; `T_era` = 57,600,000,000 hashes (`ERA_VDF_T_HASH_CHAIN`) | Measured, set | +| Verify, scheme 1 | recomputation: 10.1 s for T 170 million, the full hour at `T_era` | Measured | +| Discriminant search | 161 to 167 ms per era (Miller-Rabin with the first 30 primes on the fixed-width integer) | Measured | +| chiavdf on the same core | 208.8 K squarings/s (`vdf_bench square`, NUDUPL over GMP, 1,000,000 iterations); the AVX-512 IFMA path (`square_asm`) gave 127.3 K at 20,000 iterations and stalled at 300,000 and above in this build (built outside its Makefile's `FAST_MACHINE` flags), so the IFMA number is not established here | Measured; the asm path unestablished | +| Delay on the fastest prover measured | 108,000,000 / 208,800 = 517 s against the 1-s block interval (517x) and the 2-s publish window (259x); a prover 10x chiavdf's GMP path (the ceiling Chia's and the EF's hardware efforts aimed at, approximate, from memory) would still take 52 s, 26x the window | Computed from the measurements | +| The gate "at least 60x one block interval on the fastest known prover" | 517x on chiavdf's GMP path, the fastest evaluator measured on this hardware; PASS as measured, with the IFMA path unestablished (above) and the 10x hardware ceiling still 52x | PASS (measured), caveat recorded | + +The node's own evaluator is 5.2 to 6.8x slower than chiavdf's GMP path on the same core. That ratio only moves the honest side: `T_era` is set from the node's rate, so an honest node finishes in the hour; the attacker's margin is the delay at the fastest prover, above. + +## 3. The gate: the re-roll harness with the VDF off and on + +`tools/era-vdf/reroll.mjs` on igneum-build-2 (the box under other lanes' suites, nice 10; logs and per-cut JSON under `/srv/builds/igneum-wt-era-vdf/ev-harness-out/reroll-vdf-{on,off}-5.json`), three nodes of the fork at this record's commit on the fast-time file with `skip_proof_of_work`, era 120 DAA and lead 20 (cuts at `S = 120 n - 20`, one every two minutes), ports 30100 and up, suffix 1010; two honest virtual miners share 1 block/s on nodes 0 and 1; the adversary on node 2 holds a block A built on the tip at `S - 1` and tries to make it the era's cut block. With the VDF on, the adversary runs the node's own evaluator (`igneum-miner vdf eval`, the network's T) over its candidate before publishing; T is set from a 3-s bench at the start so the delay is about 5 s on one core of this box, five times the block interval. + +| Run | Switch | Cuts | A accepted | A became the cut block | Known-draw re-rolls (the seed the adversary knew before publishing is the era's seed) | Adversary's evaluation | Nodes agree on the era seed | Gate | Harness | +|---|---|---|---|---|---|---|---|---|---| +| vdf-off-5 (the stand-in, 15:08 to 15:22 UTC) | `era_vdf_activation_daa` never | 6 (eras 2 to 7) | 6 of 6 | 6 of 6 | 6 of 6: the seed is `hash(A)` every time | none needed: the draw of hash(A) is known the instant A is built | 3 of 3 on every era | FAIL (the known-pass fires) | SOUND | +| vdf-on-5 (the era VDF, 14:54 to 15:08 UTC) | activation 0, scheme 0, T 189,650 (5 s on this core) | 6 (eras 2 to 7) | 6 of 6 | 0 of 6 | 0 of 6 | 5.38 to 6.64 s, during which the honest chain advanced 3 to 10 blocks; A arrived behind them and never became the cut block | 3 of 3 on every era, the record ready (state 3) at every era start | PASS (silent) | SOUND | + +What the two runs say. Under the stand-in a miner with one block of hash at the right second owns the era draw outright on this network: holding the block at `S - 1` and publishing it the moment the chain reaches `S - 1` makes it the cut block in every one of six cuts (the attack lane's epoch-cut run saw 1 of 6, with the honest block often landing first; here the adversary is faster to the second), and its draw is known the instant the block is built. Under the VDF the same adversary cannot know any candidate's draw before T steps have run; while it ran them the honest chain moved 3 to 10 blocks, so its block arrived behind the cut and the seed came from the delay over the day of blues ending at the honest cut block, the same on all three nodes. The second half of the written argument of F7 (a) holds in the node, not only on paper: the re-roll needs the draw inside the window, and the window is 1 s against a delay of 5 s here and 517 s at the fastest prover measured on the production T (section 2). + +The known-pass and the known-fail ran on the same binaries, file and ports, the VDF switch the only difference. A first VDF-off run with a lookup defect in the harness (the adversary's block not found, so the verdict read "held") was discarded once the node's own log showed the adversary's hash as the cut block in 5 of 5 cuts; the harness now reads A from that log line. Two runs that overlapped on the box through a stale node of an earlier run were discarded as well (their nodes disagreed because they were two networks); the two runs above ran alone. + +## 4. The cut rule and the certified checkpoint (for the finality lane) + +The node names `C_era(n)` as the last selected-chain block below the cut on the header's own chain (the block the stand-in used), which is the checkpoint block the lead rule names under the O-4.3 decision of 3 October 2026 (certified or not). Three facts decide it: + +1. Determinism. A header's validity must be a function of its own past. "The certificate carried by a block in the header's past, for the highest-index checkpoint with DAA score at most the cut" is such a function, but a certificate that lands after the era starts flips the reading between headers of one era (a header before the carrier reads the fallback, a header after it reads the certificate), so the certified binding needs a second rule: the carrier must sit at most half a lead above the cut (3,600 DAA s, the merge depth) and be a chain ancestor of the header; under that rule every honest header of the era reads the same certificate once the network merged the carrier, and a header on a chain that never merged it reads the fallback, consistently with its own past. The chain-block reading needs no second rule. +2. Liveness. A finality pause across the cut (a third of weight leaving in an hour is a 30-day pause under rule v3) leaves the certified binding without a checkpoint for the era; the chain-block reading always has one, which is the reason O-4.3 was decided the way it was for the epoch. +3. The defence. The grinding defence is the delay: no candidate's draw is knowable for T steps, whichever block is the cut. The binding moves which block a withholder would have to be the author of, not whether withholding pays; both readings leave the withholder with a coin flip it cannot see. + +The change, if the finality lane wants the certified binding: `EraVdfManager::cut_block` (one function; the input, the delay and the seed are unchanged), plus the second rule above and a test with a certificate carried late. + +## 5. The verify gate on a 2019-class core + +The gate was "the VDF verifies in under 10 ms on a 2019-class core". Measured: 21.9 and 22.6 ms with the group held on the box core (above), which the F6 row's calibration puts at about 1.19x on an i7-9700K (the O-1.14 run: 6.0 ms on the 2019 core against 5.06 ms on the box proxy), so about 26 ms on a 2019 core, labelled a proxy: no 2019 host was rented this lane (Vast rentals are a purchase; not made without the founder's word). NOT MET, by 2.2x on the box and about 2.6x on the proxy. + +Where the time goes and what closes it: a verification is two 256-bit exponentiations, about 770 group operations at 28 ยตs each; the operation is NUDUPL on the fixed-width integer, whose cost is the extended gcd (Lehmer rounds on 8-limb numbers) and the reduction. Three rounds of this lane moved it from 345 ยตs (a Lehmer convention defect that fell back to plain division every round) to 28 ยตs (the convention, i64 word division, 34 limbs, the x86-64 128-by-64 division); the next 2.2x is chiavdf's Pulmark reducer (reduce only when `a` exceeds 8 limbs, O-4.6) and a limb-level NUDUPL that keeps the partial gcd's intermediates in words, or GMP through `rug` behind a feature on the x86-64 Linux and Windows builds (chiavdf's 208 K/s is 6x this evaluator, which would put the verification near 4 ms as the prototype measured), with the fixed-width path the fallback for wasm and macOS. Owed, not blocking: the verification runs once per era (180 days) on a node that imports a record rather than evaluating; every mining node evaluates and never verifies. + +## 6. Consequences per tier (the standing rule of 5 October 2026) + +| Tier | What the era VDF costs | What it means | +|---|---|---| +| A home miner, any card (8, 12, 16, 24 or 32 GB), any vendor, Windows, Linux or macOS | one CPU core for about 60 min once per 180 days at the reference rate (a 2019-class desktop core about 72 min by the F6 calibration), 17 MB of host RAM for the prover's checkpoints during it, 0 bytes on the card; the node starts it a quarter of the lead past the cut and holds the record from then | nothing changes on the card or in the hash rate; the 2-hour lead covers a core half the reference speed; a node that was off across the cut evaluates on arrival and its miner holds until the record lands (the miner says so every 10 s) | +| A rig (one node, several cards) | the same one core on the rig's host, once per era | nothing per card | +| A pool user | the pool's node evaluates; the member's miner takes `era_seed` from the template as today | nothing | +| A light client or a syncing node | verifies an imported record in 22 ms (26 ms on a 2019 core, proxy) plus the 165-ms discriminant derivation, once per era; under scheme 1 it recomputes the hour | the 10-ms gate is missed (section 5); operationally one verification per 180 days | +| The protocol | the era draw's input is unknowable for 517 s on the fastest prover measured, against a 1-s block interval: the stand-in's one-block grind is closed (section 3) | the freeze of the draw procedure and the C_era cut rule no longer waits on the VDF's existence; it waits on the two decisions of `ledger-decisions.md` | + +## 7. What is owed + +- The P2P relay of an era record to a syncing peer and the RPC import (spec 4.5, O-4.10), before era 1 of any network with the switch set. +- The external review of the class-group port (O-4.1): the port is a second implementation checked against the textbook algorithms and `num-bigint`, not a review. +- The attack pass's F7 status row (branch `attack-pass`, `docs/analysis/attack-pass-2026-10.md`) reads INCOMPLETE pending this lane; the line for it, from section 3: "F7 (a): the era VDF is in the node (fork `era-vdf-node`); the re-roll harness against the real era cut fires with it off (6 of 6 cuts, the seed the adversary's block) and is silent with it on (0 of 6 across six cuts, the adversary's 5-s evaluation against a 1-s block interval, three nodes agreeing on every era seed); PASS, the delay 517 s on the fastest prover measured at the production T." +- The decisions of `docs/plans/ledger-decisions.md` (the activation per network, the cut's binding). diff --git a/docs/analysis/hashrate-decay-2026-10-03.md b/docs/analysis/hashrate-decay-2026-10-03.md index c2ebda0a..3f8d0080 100644 --- a/docs/analysis/hashrate-decay-2026-10-03.md +++ b/docs/analysis/hashrate-decay-2026-10-03.md @@ -1,6 +1,6 @@ # Per-identity hash rate "decay" on the RTX 5090: diagnosis and fix -3 October 2026, miner-community-lead. Source data: the uploaded logs of the project lead's PC (`node tools/logs.mjs +3 October 2026, miner-community-lead. Source data: the uploaded logs of the founder's PC (`node tools/logs.mjs nvidia-DESKTOP-KMCV30N-1-20261003-222331 --all` and the other identities, the launcher log `igneum-DESKTOP-KMCV30N-20261003-222331`), the three serve loops, the miner's worker mode, and five runs of the Metal worker on the Mac against private test nodes (ports 27500 and up, `/tmp/igneum-decay-test`). Figures @@ -9,7 +9,7 @@ from the logs are exact; the two labelled approximate are from memory. ## 1. Finding in one paragraph There is no per-job growth in any worker or in the miner's memory. Two separate things produce the picture -the project lead saw. First, the STATUS line's two rates are cumulative averages since the miner started +The founder saw. First, the STATUS line's two rates are cumulative averages since the miner started (`hashes_total / elapsed` and `hashes_total / gpu_ms_total` in `mine_worker`), so a fast first interval decays as 1/t by construction; nvidia-1 was alone on the card for its first seconds and every later interval ran at a flat 17.8 MH/s wall, while the last-started identity, nvidia-8, shows the mirror image, a cumulative @@ -28,7 +28,7 @@ The miner prints `hash=A MH/s wall (B MH/s inside jobs)` with `A = hashes_total `dH / dt` and `dH / dG` with `H = A x t` and `G = H / B`. Every table below is that calculation (`rates.py` in the bench-log entry). -### 2.1 The segment the project lead quoted: 22:57 to 23:04 UTC, after the epoch-3 restart (DAA 10,801 on) +### 2.1 The segment the founder quoted: 22:57 to 23:04 UTC, after the epoch-3 restart (DAA 10,801 on) nvidia-1, jobs of 2^24 nonces, STATUS every 30 s: @@ -95,7 +95,7 @@ card going idle, which is what a growing CPU-side gap in every worker does. ## 3. Code audit: what is allocated per job, and what is freed -### 3.1 `proto-cuda/host.cu`, `runServe` (HEAD, the binary the project lead ran, built by the launcher at 22:09:57) +### 3.1 `proto-cuda/host.cu`, `runServe` (HEAD, the binary the founder ran, built by the launcher at 22:09:57) | Allocation | When | Size | Freed | |---|---|---|---| @@ -113,7 +113,7 @@ version (the hot-swap agent's) adds `CudaPair` (at most two resident, the old on job on the new pair, `releasePair` frees dataset, cache and both modules) and `PrepareTask` (deleted after the load); still nothing per job. `cudaDeviceSynchronize` per dispatch under the default `cudaDeviceScheduleAuto` spins the host thread when the process holds fewer contexts than the machine has -cores, which is always true here (one context per process): that is the 6.2% CPU per worker the project lead saw (one +cores, which is always true here (one context per process): that is the 6.2% CPU per worker the founder saw (one of 16 threads). The hot-swap working tree sets `cudaSetDeviceFlags(cudaDeviceScheduleBlockingSync)` before the context is created (host.cu, main), which is the right call and the right place; the thread then sleeps on the dispatch. It does not change the hash rate directly, but eight spinning threads plus eight OpenCL @@ -250,7 +250,7 @@ because it is measured from the template fetch, which precedes the walk). ## 5. The time-slicing hypothesis: one worker versus eight, at two fixed difficulties -the project lead's Task Manager reading (GPU memory flat at 14.7 GB, the card 99% busy, each CUDA worker at 6.2% CPU) and the +The founder's Task Manager reading (GPU memory flat at 14.7 GB, the card 99% busy, each CUDA worker at 6.2% CPU) and the hypothesis that eight contexts time-slicing one card with "longer jobs as blocks get rarer" explain the decay. A job is a fixed 2^24 nonces, so its length does not depend on the target, but the four runs below test the hypothesis as stated: one Metal worker and eight, each at a fixed low difficulty (2^25, 0.25 founds per job) @@ -483,20 +483,20 @@ warnings; also saved as `docs/analysis/hashrate-decay-2026-10-03.patch`). acceptance figure is table 2.2 flattening: the gap per job no better than 0.10 s at DAA 10,800 (3,600 into an epoch) with eight identities. -## 7. Two things for the project lead to check on the PC +## 7. Two things for the founder to check on the PC 1. Dedicated GPU memory over time. Task Manager, Performance, GPU 0, "Dedicated GPU memory usage", or `nvidia-smi --query-gpu=timestamp,memory.used,utilization.gpu,temperature.gpu,clocks.sm,clocks.mem,power.draw,clocks_throttle_reasons.active --format=csv -l 10 > gpu.csv`. Expected: flat from the moment the eighth worker prints `ready` (each CUDA worker holds 1 GiB dataset + 256 MiB cache + 32 MiB output + the context, about 1.6 GiB; eight of them 13 to 15 GiB of the 32 GiB, - which matches the flat 14.7 GB the project lead read). A line that climbs while the hash rate falls would mean a leak + which matches the flat 14.7 GB the founder read). A line that climbs while the hash rate falls would mean a leak in the worker; none exists in the code and the Mac RSS traces are flat. 2. GDDR7 memory junction temperature and the throttle reasons. HWiNFO64, Sensors, under the GPU: "GPU Memory Junction Temperature", "GPU Thermal Limit", "GPU Power Limit", "GPU Reliability Voltage Limit" (each "Yes" or "No"), "GPU Effective Clock" and "GPU Memory Clock"; or the `clocks_throttle_reasons.active` column above (`0x0000000000000004` is SW power cap, `0x0000000000000020` SW thermal slowdown, `0x0000000000000040` HW thermal slowdown, `0x0000000000000080` HW power brake). Approximate thresholds, - from memory: the core starts to pull clocks around 83 C (the project lead's 55 to 59 C is far below it); GDDR6X on the + from memory: the core starts to pull clocks around 83 C (the founder's 55 to 59 C is far below it); GDDR6X on the previous generations throttles from about 95 C junction and the hard limit is 105 C; GDDR7 figures are not published, so treat anything above 90 C junction as the zone to watch and a "Yes" on any limit row as the signal. The decisive sign of throttling is "GPU Effective Clock" falling while utilization stays at 99%; diff --git a/docs/analysis/horizon-2026-10.md b/docs/analysis/horizon-2026-10.md index e64b6c9e..a6c0ce62 100644 --- a/docs/analysis/horizon-2026-10.md +++ b/docs/analysis/horizon-2026-10.md @@ -1,8 +1,8 @@ # Horizon, October 2026: the ranked research across every system -Written 6 to 7 October 2026 by the Horizon coordinator (branch `horizon`, worktree `igneum-wt-horizon`) on the project lead's ask of 6 October 2026, 20:4x UK: "deep backward and forward predictive research and modelling for our algo and all of our systems: is there room for improvement, room for more coin utility, algo improvements, security improvements, anything we can do to make a 51% attack impossible, basically creating a level of polish that has not been seen before." Later the same evening: "research anything else that we can research too, predictions, forward thinking, what we can actually do that has not been done or applied, think outside the box", "be revolutionary", and "if we create a new way of hashing or a new way of proof of work to revolutionise the space then that's absolutely fine, I want you to deploy everything to create something that has not ever been done before." +Written 6 to 7 October 2026 by the Horizon coordinator (branch `horizon`, worktree `igneum-wt-horizon`) on the founder's ask of 6 October 2026, 20:4x UK: "deep backward and forward predictive research and modelling for our algo and all of our systems: is there room for improvement, room for more coin utility, algo improvements, security improvements, anything we can do to make a 51% attack impossible, basically creating a level of polish that has not been seen before." Later the same evening: "research anything else that we can research too, predictions, forward thinking, what we can actually do that has not been done or applied, think outside the box", "be revolutionary", and "if we create a new way of hashing or a new way of proof of work to revolutionise the space then that's absolutely fine, I want you to deploy everything to create something that has not ever been done before." -The bar main set, and the bar this document holds every claim to: "impossible" is not available to any proof-of-work chain. The bar is that a majority of hash buys nothing: it cannot reverse what finality locked, cannot forge a proof the nodes re-execute, cannot change a rule without 95 percent signalling, and loses more than it earns. Every claim here is a number with a model or a simulation behind it, priced in rented hash at the measured 6 October rate (`docs/bench-log.md`, "Rental cost of hash, 6 October 2026": USD 0.0117 per MH/s-hour, so USD 11.7 per GH/s-hour and about USD 281 per GH/s-day; the live devnet at 1.16 GH/s), with its consequence per tier and what to build. Hours are agent hours (the project lead's rule: Claude-side work takes hours, never weeks). +The bar main set, and the bar this document holds every claim to: "impossible" is not available to any proof-of-work chain. The bar is that a majority of hash buys nothing: it cannot reverse what finality locked, cannot forge a proof the nodes re-execute, cannot change a rule without 95 percent signalling, and loses more than it earns. Every claim here is a number with a model or a simulation behind it, priced in rented hash at the measured 6 October rate (`docs/bench-log.md`, "Rental cost of hash, 6 October 2026": USD 0.0117 per MH/s-hour, so USD 11.7 per GH/s-hour and about USD 281 per GH/s-day; the live devnet at 1.16 GH/s), with its consequence per tier and what to build. Hours are agent hours (the founder's rule: Claude-side work takes hours, never weeks). ## The lanes @@ -17,11 +17,11 @@ The bar main set, and the bar this document holds every claim to: "impossible" i | 7 frontier | `docs/analysis/horizon/frontier.md`; model `sim/horizon/frontier/frontier_model.py` | landed, commit 5ff7393 | | 8 new-proof-of-work | `docs/analysis/horizon/new-pow.md`; prototypes `proto-newpow/` | landed, commits cbcff47 and 92db7c5 (two prototypes measured on rented 4090s, verdicts in section 6) | -## 1. One page for the project lead +## 1. One page for the founder Closed 6 October 2026, 22:3x UK, every lane landed. -**Standing decisions (the project lead, 6 October 2026, 22:3x UK).** Verbatim: "Fees cannot fund security for a decade" and "Miners need to be the security". Meaning: miners are the security always, no time bound, no stake, no outside checkpoints or committee. The chain pays its own miners from emission plus fees; nobody pays upkeep, not the founder, not a treasury, not a dev fund. Self-sustaining means the emission curve keeps mining worth doing on its own for as long as fees are small, which lane 4 measures as a decade or more, so emission never decays on a schedule that assumes fees take over. Fee revenue is never assumed as the security budget in any model or public sentence. His third line, "Wrong constants and claims in our own text", acknowledges lane 6's finding; the nine ledger rows in item 9 are the fix. +**Standing decisions (the founder, 6 October 2026, 22:3x UK).** Verbatim: "Fees cannot fund security for a decade" and "Miners need to be the security". Meaning: miners are the security always, no time bound, no stake, no outside checkpoints or committee. The chain pays its own miners from emission plus fees; nobody pays upkeep, not the founder, not a treasury, not a dev fund. Self-sustaining means the emission curve keeps mining worth doing on its own for as long as fees are small, which lane 4 measures as a decade or more, so emission never decays on a schedule that assumes fees take over. Fee revenue is never assumed as the security budget in any model or public sentence. His third line, "Wrong constants and claims in our own text", acknowledges lane 6's finding; the nine ledger rows in item 9 are the fix. 1. **The proving pool was the one line where a majority earned more than it spent**: 11,636 IGN an hour at 51 percent of blocks. Fix: the proof verified in consensus. **In 0.3.16** (fork exec-sync-0313 da2d17ec), switch `proving_consensus_verify_daa` off by default. **Decision owed:** when it activates. @@ -41,11 +41,11 @@ Closed 6 October 2026, 22:3x UK, every lane landed. 9. **Polish.** Pause wording in Ember and the API: **in 0.3.16** (ember-tune b726ce4). `fork_is_close` and the publisher's activation guard: **in 0.3.16** (1357d28, 08f5276). Export-disk cap: **done** (gpu-fleet f9ad70e). Nine text corrections (M32, M33, F26, E19, E20, G15, P24, E21, P25) plus X31 to X33: **done**. Unsigned installers: **decision owed**. Relay fixes (28c028b) on fud-close: merge owed. -**Decisions owed from the project lead:** the cryptanalysis spend (USD 80,000 to 160,000); the testnet date word; the N ladder at genesis; the verification switch activation. +**Decisions owed from the founder:** the cryptanalysis spend (USD 80,000 to 160,000); the testnet date word; the N ladder at genesis; the verification switch activation. -## Decisions ready for the project lead (added 6 October 2026, 23:1x UK) +## Decisions ready for the founder (added 6 October 2026, 23:1x UK) -Two one-page verdicts landed after the close. Each is quoted verbatim from its file and each is **the project lead's decision owed**. The files sit on their branches and are not merged; read them there. +Two one-page verdicts landed after the close. Each is quoted verbatim from its file and each is **the founder's decision owed**. The files sit on their branches and are not merged; read them there. ### Emission: the tail @@ -188,7 +188,7 @@ The residual risks stated plainly: the first 20 days (no weight table yet); a pa ### Lane 6, polish 1. The pause had no cause on any surface: no `finality_reason` or frozen-table share in the node's report, the observer, the API, Ember or the hub (Q1, Q2, Q6); the 0.3.16 Ember carrier is on ember-tune. 2. Every update was urgent once an activation height was behind the node (`fork_is_close`, Q4), and nothing refused an activation height at or below the live DAA; both fixed on ember-tune for 0.3.16. -3. Unsigned installers on both desktops (Q3, the project lead's certificates) and the relay's security fixes still on fud-close (Q8). +3. Unsigned installers on both desktops (Q3, the founder's certificates) and the relay's security fixes still on fud-close (Q8). ### Lane 8, new-proof-of-work 1. Scheme A (mining is proving) is a bound, not a design: 2.9 MB of openings per block or a 32 to 40 ms verify against the 10 ms gate; the useful fraction is 8 percent at 1 GH/s and 0.08 percent at 100 GH/s. diff --git a/docs/analysis/horizon/algorithm.md b/docs/analysis/horizon/algorithm.md index 5b2f879a..cc04d157 100644 --- a/docs/analysis/horizon/algorithm.md +++ b/docs/analysis/horizon/algorithm.md @@ -305,11 +305,11 @@ Recommendation with numbers: hold the schedule as decided (2 GiB genesis, doubli | 5a | A class v5 candidate `mx8 + sh256x35` with a shuffle-heavy shadow weight table (shfl 14, shfla 8 of 75), measured on the four owned cards before any cut: the first rung of proposal 5's ladder, plus the k-floor lever | the Mac's 5 percent point is 130,000; the shuffle mix raises the k floor 0.32 to 0.46 (approx) | section 5.3 | 4 to build the weight-table knob and packs, 1 Mac measure session (about 6 min under the lock, miner paused), 3 PC jobs | M5 Max -4.8 percent of rate at 37 W; 5090 -0.3 percent at its 431 W cap (a rig +23 percent electricity); 4070 0 at about 118 W; 9070 XT 0; verifier +0.23 ms Mac, +0.85 half-core | every owned card within 5 percent; bit-exact on three vendors; half-core verifier under 10 ms; the 5090's marginal pJ on the new mix read on three rungs | | 6 | Hold the dataset schedule (2 GiB, years 4, 12, 28); write the prover footprint into the card-lifetime sentence | Steam shares and the measured prover peaks; one HBM3 stack holds every step | section 5.6 | 1 | 8 GB: mines to year 12, proves alone; 12 GB: mine-and-prove compressed to year 4, core-only to year 12, mines to year 28; 16 GB: compressed to year 4, core-only to year 12; 24 and 32 GB unconstrained to year 28 | the litepaper sentence matches the table; `docs/evidence.md` row "card lifetime" labelled designed | | 7 | Make the Ember tune the shipped default per card model (the honest card's watts are the lever that moves every chip row) | the 4070 at 3.65 uJ untuned and 2.57 tuned (-30 percent); the 5090 2.65 bench against 2.34 app | section 5.1 | 2 (defaults table in the app from the fleet priors; already measured) | every NVIDIA tier gains 10 to 30 percent per joule; the chip's edge over the mid-tier falls from 8x to 13x toward 5x to 9x at v3 | MH per W per card model on the fleet night against the untuned baseline | -| 8 | Fund the k question: the item 3 cryptanalysis brief gains a chip-design line (a 14,000-lane SIMD array's energy per op on a random 32-lane program with shuffles, at N5 and at 28 nm) | every chip row at class v4 turns on k; nothing in the project measures it | section 5.3 | 0 agent hours; the project lead's money (part of the USD 80,000 to 160,000 brief) | none until the number lands; it decides whether 2x is reachable | a reviewed estimate of k with its range | +| 8 | Fund the k question: the item 3 cryptanalysis brief gains a chip-design line (a 14,000-lane SIMD array's energy per op on a random 32-lane program with shuffles, at N5 and at 28 nm) | every chip row at class v4 turns on k; nothing in the project measures it | section 5.3 | 0 agent hours; the founder's money (part of the USD 80,000 to 160,000 brief) | none until the number lands; it decides whether 2x is reachable | a reviewed estimate of k with its range | Paragraphs. -1. The verifier gate is the one place tonight produced a measurement instead of a rule. The box proxy brackets a 2019 laptop from both sides (a 2022 server core at full boost; the same core with its sibling busy), and dr736 fails both brackets while class v4 passes both with 1.8 ms to spare. The measurement is one Windows build and one bench on the laptop the project lead already owns; until it lands, the half-core row replaces the "2.5x" from memory in every status file. +1. The verifier gate is the one place tonight produced a measurement instead of a rule. The box proxy brackets a 2019 laptop from both sides (a 2022 server core at full boost; the same core with its sibling busy), and dr736 fails both brackets while class v4 passes both with 1.8 ms to spare. The measurement is one Windows build and one bench on the laptop the founder already owns; until it lands, the half-core row replaces the "2.5x" from memory in every status file. 2. The FPGA lane's upper row was built on an activate rate (8 per 12 ns per channel) that the JEDEC HBM2 cycle table does not support (4 per 28 ns); the measured Shuhai rate sits exactly on the JEDEC ceiling. That reading can be wrong (the ICCAD table's clock interpretation, the half-bank count, the board watts are all approximate), which is why the F2 hour is the proposal and not the conclusion. It is cheap and it turns a public ceiling claim into a measured one. diff --git a/docs/analysis/horizon/consensus-security.md b/docs/analysis/horizon/consensus-security.md index d6be2eb1..6906a1e6 100644 --- a/docs/analysis/horizon/consensus-security.md +++ b/docs/analysis/horizon/consensus-security.md @@ -176,7 +176,7 @@ Ways not in the seven, with the bound this lane gives: | The 30-day window edges | (a) the frozen table expires at exactly day 30.00 after the last lock: both sides of a long split lock alone at once (M3); (b) a departed set leaves the sliding table over 30 days and the frozen one at the cliff (M4); (c) the first 30 days have no lock at all (3.8, `min_daa` = window); (d) new honest cohorts are under-weighted t/60 for 30 days (G) | stated in 3.7 items 2, 7, 9 | a partition or departure longer than one window ends with the fork of 3.7 item 9 and a manual F5 | | | | The pause as a liveness attack | a silent set at or above 1/3 pauses every lock for as long as it stays silent (J, L1; sweep S) at zero marginal cost since it keeps earning | none in the rule; the node reports the pause; exchange guidance treats the chain as PoW with a 12-h depth | the 1/3 veto: 20 days at 51% | 0 once held | nothing directly; enables the 12-h PoW double spend below | | What an attacker can do during a pause | plain proof of work: reorg up to the finality depth 43,200 DAA (12 h) with a heavier chain; beyond merge depth the honest blocks are abandoned (the 229-block shape); every certified checkpoint before the pause still binds | finality depth; the exchange guidance of 3.9 | 12 h of >50% hash | USD 146 / 1.5k / 15k / 146k | a deposit credited at the PoW depth; 559k IGN of subsidy as a miner | -| Tonight's departure (confirmed, lane 3 `finality-and-weight.md` 3.1 and 4.1) | 20 keys holding 42.7% of the frozen table stopped mining 18:27 to 18:30Z (the rehearsal job); the last lock 6842 at 18:39:40Z; 6843 determined with 53.1% of total signing and never locked; under v2 the stayers' sliding share crossed two thirds at 6912 (19:14:53Z, a 35-min pause) but Q5 held them at 57.3% of the frozen table; expected first lock when that table expires at DAA 216,402, about 20:40Z, or when 9.4 points of departed keys return. Sweep C agrees: 51% leaving pauses 10.5 days (v2) or 30.0 days (v3) at mainnet scale; at tonight's 46.9% (observer's view) 7.7 days under v2, 30 under v3 (lane 3, 4.1) | by design (F21: the project lead chose the pause over the fork); a view cannot tell a departure from a partition | anything over 1/3 of the table leaving at once pauses finality for a window | 0 | 0; what an attacker can do during it is the row above | +| Tonight's departure (confirmed, lane 3 `finality-and-weight.md` 3.1 and 4.1) | 20 keys holding 42.7% of the frozen table stopped mining 18:27 to 18:30Z (the rehearsal job); the last lock 6842 at 18:39:40Z; 6843 determined with 53.1% of total signing and never locked; under v2 the stayers' sliding share crossed two thirds at 6912 (19:14:53Z, a 35-min pause) but Q5 held them at 57.3% of the frozen table; expected first lock when that table expires at DAA 216,402, about 20:40Z, or when 9.4 points of departed keys return. Sweep C agrees: 51% leaving pauses 10.5 days (v2) or 30.0 days (v3) at mainnet scale; at tonight's 46.9% (observer's view) 7.7 days under v2, 30 under v3 (lane 3, 4.1) | by design (F21: the founder chose the pause over the fork); a view cannot tell a departure from a partition | anything over 1/3 of the table leaving at once pauses finality for a window | 0 | 0; what an attacker can do during it is the row above | ### 4.4 Miner signalling (P2) diff --git a/docs/analysis/horizon/finality-and-weight.md b/docs/analysis/horizon/finality-and-weight.md index ec6a1c76..2c86b848 100644 --- a/docs/analysis/horizon/finality-and-weight.md +++ b/docs/analysis/horizon/finality-and-weight.md @@ -6,7 +6,7 @@ What was read: `docs/spec/03-finality.md` (whole, 3.11 included), `04-seeds-and- ## 1. The three findings first -1. **Tonight's pause was the rule, not the aggregation path, and it was the frozen table that held it past 19:14Z.** The 20 keys that left the live chain between 17:20Z and 18:30Z held 3,026 of 7,083 blue blocks of the table frozen at the last lock (42.7 percent; the 13 that left with the 18:27 to 18:30Z rehearsal job alone 36.5 percent). The first unlocked checkpoint, 6843 at DAA 209,233 (about 18:40Z), had 75 of 93 voters' votes and 53.1 percent of total weight on the observer's node, under the two-thirds floor; certificates had kept forming for 18 checkpoints while node 1 and the observer were down (6824 to 6842, 18:30 to 18:39Z, 83 signers, 78.5 to 79.7 percent of total). From 19:14:53Z (checkpoint 6912) the stayers held 74.9 percent of the sliding table and still did not lock, because they hold 57.3 percent of the frozen table of lock 6842, which stands until DAA 216,402 (about 20:40Z). Under rule v2 the first lock would have come at 6912, 35 minutes after the last; under v3 the pause is one window, 2 hours on the devnet and 30 days on mainnet (spec 3.7 item 2, the price the project lead took on 4 October). +1. **Tonight's pause was the rule, not the aggregation path, and it was the frozen table that held it past 19:14Z.** The 20 keys that left the live chain between 17:20Z and 18:30Z held 3,026 of 7,083 blue blocks of the table frozen at the last lock (42.7 percent; the 13 that left with the 18:27 to 18:30Z rehearsal job alone 36.5 percent). The first unlocked checkpoint, 6843 at DAA 209,233 (about 18:40Z), had 75 of 93 voters' votes and 53.1 percent of total weight on the observer's node, under the two-thirds floor; certificates had kept forming for 18 checkpoints while node 1 and the observer were down (6824 to 6842, 18:30 to 18:39Z, 83 signers, 78.5 to 79.7 percent of total). From 19:14:53Z (checkpoint 6912) the stayers held 74.9 percent of the sliding table and still did not lock, because they hold 57.3 percent of the frozen table of lock 6842, which stands until DAA 216,402 (about 20:40Z). Under rule v2 the first lock would have come at 6912, 35 minutes after the last; under v3 the pause is one window, 2 hours on the devnet and 30 days on mainnet (spec 3.7 item 2, the price the founder took on 4 October). 2. **Of the four candidate rules, only the departure announcement keeps the one-third bound.** In the simulator (3 seeds, mainnet scale) the decaying denominator and the hysteresis floor both restore liveness after tonight's departure in under an hour and both reopen the partition double lock (fast decay: both sides of every 360-minute partition lock alone from minute 120, 467 to 473 conflicting locks in the 50/50 honest split and 17 to 264 in the poisoned eclipse; slow decay: both sides of the 12-day splits lock alone at day 0.5 to 0.9, 31,545 to 32,246 conflicts; hysteresis: a 20 percent equivocator conflicts from minute 60, 565 to 597 locks, the 13.3 percent bound of 3 October back). The leave rule locks 1 hour after the departure (0.04 days; under 4 devnet minutes) with 0 conflicting locks in every partition, eclipse and equivocator row, and an attacker who buys keys to make them leave gains nothing it would not get by signing with them (w + L must still reach 2/3). The two-tier report never conflicts in its final tier by construction and shows 516 to 1,062 conflicting PROVISIONAL locks in every 360-minute partition and about 34,000 in the 12-day splits, so it is a reporting layer with a health warning, not a rule. 3. **Weight costs USD 8,424 x N x W / (1 - W) to rent for the full window** at the measured USD 11.7 per GH/s-hour: a veto (34 percent) against a 1 GH/s network is USD 4,300 over 30 days (0.52 x N of hash, a +52 percent step on the chart from day 1), against 1 TH/s USD 4.3 M; locking alone (67 percent) is 2.03 x N for 30 days (USD 17,100 per GH/s of network, USD 17 M at 1 TH/s), and faster is dearer (22 days: 10.6 x N). Buying old keys costs the seller's own rental equivalent, decays to nothing in 30 days (sim K), and nothing in the protocol makes weight unbuyable; what keeps the price at the rental cost is that the seller keeps a copy and one equivocation strips the key. diff --git a/docs/analysis/horizon/frontier.md b/docs/analysis/horizon/frontier.md index db333c5d..af7d261c 100644 --- a/docs/analysis/horizon/frontier.md +++ b/docs/analysis/horizon/frontier.md @@ -1,6 +1,6 @@ # Horizon lane 7: frontier. Predictions to 2030, what no proof-of-work chain has shipped, and what Igneum can -6 October 2026, evening UK. Lane 7 of the Horizon programme. Worktree `/Users/joshm/Projects/igneum-wt-horizon` (branch `horizon`, at origin/master 3f4f719). the project lead's words: "research anything else that we can research too, predictions, forward thinking, what we can actually do that has not been done or applied, think outside the box", and "be revolutionary". Main's bar: not features, but ideas that change what a proof-of-work chain is or what a GPU owner is to the world, each with its evidence, cost, gate, and the attack a Monero or Kaspa core developer would mount. I argue each attack as the project's own four personas would hear it (`.claude/agents/cryptographer.md`, `consensus-engineer.md`, `execution-engineer.md`, `miner-community-lead.md`). +6 October 2026, evening UK. Lane 7 of the Horizon programme. Worktree `/Users/joshm/Projects/igneum-wt-horizon` (branch `horizon`, at origin/master 3f4f719). The founder's words: "research anything else that we can research too, predictions, forward thinking, what we can actually do that has not been done or applied, think outside the box", and "be revolutionary". Main's bar: not features, but ideas that change what a proof-of-work chain is or what a GPU owner is to the world, each with its evidence, cost, gate, and the attack a Monero or Kaspa core developer would mount. I argue each attack as the project's own four personas would hear it (`.claude/agents/cryptographer.md`, `consensus-engineer.md`, `execution-engineer.md`, `miner-community-lead.md`). Nothing in this file is a prediction of the coin's price, an offer to sell anything, or a change to any consensus parameter. Every chip figure is arithmetic on cited memory and logic figures; every GPU figure names its bench entry; a figure from memory says approximate. No em dashes. @@ -12,7 +12,7 @@ Nothing in this file is a prediction of the coin's price, an offer to sell anyth ## 0. Everything ranked by payoff over difficulty -Payoff 1 to 5 is what the idea does for the chain's security, the coin's utility or the GPU owner's position in the world, if it works. Difficulty is Claude-side hours to a measurable prototype (the project lead's rule: hours, never weeks). Verdicts: do now, prototype, watch, never. The "never" rows carry a sharp reason so the rest are not fantasy. +Payoff 1 to 5 is what the idea does for the chain's security, the coin's utility or the GPU owner's position in the world, if it works. Difficulty is Claude-side hours to a measurable prototype (the founder's rule: hours, never weeks). Verdicts: do now, prototype, watch, never. The "never" rows carry a sharp reason so the rest are not fantasy. | Rank | Idea | Payoff | Hours | Verdict | One line why | |---|---|---|---|---|---| @@ -322,7 +322,7 @@ The at-risk amount is five to nine orders of magnitude above the designed coin b ### 3.6 Treasury-less audit funding: bounties from the burn, a review escrow on upgrades -**The idea.** the project lead removed the dev fund (spec 5.5) and the project pays audits from the Ember dev fee and founders' mined coins (litepaper). The question: money for audits that comes from users paying for something, with no standing address. Two mechanisms. (a) **Burn redirect.** The base fee burns to nobody. A reproducible break submitted under spec 0.5 and accepted by 60 percent of blue blocks over a window redirects the base-fee burn of the next 7 (execution) or 30 (consensus) days to the submitter's address, once, then returns to burning. No address exists between events. (b) **Review escrow.** An upgrade proposal under 5.7 must escrow IGN in a contract that pays reviewers named in the proposal on a 60 percent "review complete" signal, or refunds on failure; the proposer pays, which is a user paying for a thing (the right to propose code). +**The idea.** the founder removed the dev fund (spec 5.5) and the project pays audits from the Ember dev fee and founders' mined coins (litepaper). The question: money for audits that comes from users paying for something, with no standing address. Two mechanisms. (a) **Burn redirect.** The base fee burns to nobody. A reproducible break submitted under spec 0.5 and accepted by 60 percent of blue blocks over a window redirects the base-fee burn of the next 7 (execution) or 30 (consensus) days to the submitter's address, once, then returns to burning. No address exists between events. (b) **Review escrow.** An upgrade proposal under 5.7 must escrow IGN in a contract that pays reviewers named in the proposal on a 60 percent "review complete" signal, or refunds on failure; the proposer pays, which is a user paying for a thing (the right to propose code). **Why nobody shipped it.** Zcash funds development from the block subsidy (NU6: 8 percent to Zcash Community Grants, 12 percent to a protocol lockbox, ZIP 1015); Decred from a 10 percent treasury spent by stakeholder vote, capped at 4 percent of balance a month since January 2026 (DCP-0013); Monero from the CCS, donations off-chain; Optimism from an 850 M OP reserve for retro funding. Bug bounties pay 10 percent of funds at risk (Immunefi's standard) from the protocol's own treasury; Code4rena runs contests at zero platform fee since 2025. Nobody funds audits from a burn redirect, because a burn redirect is a subsidy to a payee by rule, and the chains that wanted that built a treasury. New in form; a dev fund in substance (see the Monero attack). @@ -341,7 +341,7 @@ At launch traffic a 30-day redirect is under one audit contest; at half-full blo **Hours.** 20 for the escrow contract and the redirect rule as a proposal kind; 0 for the honest alternative, which already exists. -**The gate.** None that a simulator settles; the gate is the project lead's: does a per-event, miner-approved payee with no standing address pass the test that removed the dev fund? +**The gate.** None that a simulator settles; the gate is the founder's: does a per-event, miner-approved payee with no standing address pass the test that removed the dev fund? **Per tier.** Miners vote on each payout with their blocks and can refuse all of them; holders see supply that would have burned paid to a named person; a prover, rig, pool user and rollup customer see nothing unless a break affects them; the node operator gains a proposal kind. @@ -415,7 +415,7 @@ At launch traffic a 30-day redirect is under one audit contest; at half-full blo **The idea as asked.** Make the leader election depend in part on proving work, so the energy that picks the block maker is useful. -**Why every attempt failed, cited.** Primecoin (2013) found Cunningham and bi-twin prime chains that nobody uses (Bitcoin Magazine, July 2013). Gridcoin pays for BOINC work and stops if BOINC stops (gridcoin.us; the 2022 "Challenges of PoUW" survey, arXiv 2209.03865). Ball, Rosen, Sabin and Vasudevan (eprint 2017/203) gave proofs of useful work from fine-grained problems (Orthogonal Vectors, 3SUM, APSP) and state the conditions: the problem must be sampleable at a tunable hardness with instances the miner cannot choose, and the verifier must be cheaper than the work. Ofelimos (Fitzi, Kiayias, Panagiotakos, Russell, CRYPTO 2022, eprint 2021/1379) got a provably secure protocol by making the work a doubly efficient local search whose usefulness is a side effect and small. The 2026 "Economics of Proof-of-Useful-Work" (arXiv 2606.06700) and the empirical study of Pearl's cuPOW (arXiv 2606.04819, "The Usefulness Gap") find the same gap between the work paid for and the work anyone wanted. I found no Coinbase paper on the subject (searched 6 October 2026); if the project lead has one in mind, its title is needed. Aleo ran proving as the consensus work and the fastest prover won (CLAUDE.md: the Aleo lesson; litepaper precedents table, approximate). Boundless's PoVW (docs.boundless.network/zkc/mining/overview) pays ZKC pro rata to cycles proven per epoch with a stake that scales with the work, which is a reward for proving, not a leader election, and it is on a proof-of-stake chain. +**Why every attempt failed, cited.** Primecoin (2013) found Cunningham and bi-twin prime chains that nobody uses (Bitcoin Magazine, July 2013). Gridcoin pays for BOINC work and stops if BOINC stops (gridcoin.us; the 2022 "Challenges of PoUW" survey, arXiv 2209.03865). Ball, Rosen, Sabin and Vasudevan (eprint 2017/203) gave proofs of useful work from fine-grained problems (Orthogonal Vectors, 3SUM, APSP) and state the conditions: the problem must be sampleable at a tunable hardness with instances the miner cannot choose, and the verifier must be cheaper than the work. Ofelimos (Fitzi, Kiayias, Panagiotakos, Russell, CRYPTO 2022, eprint 2021/1379) got a provably secure protocol by making the work a doubly efficient local search whose usefulness is a side effect and small. The 2026 "Economics of Proof-of-Useful-Work" (arXiv 2606.06700) and the empirical study of Pearl's cuPOW (arXiv 2606.04819, "The Usefulness Gap") find the same gap between the work paid for and the work anyone wanted. I found no Coinbase paper on the subject (searched 6 October 2026); if the founder has one in mind, its title is needed. Aleo ran proving as the consensus work and the fastest prover won (CLAUDE.md: the Aleo lesson; litepaper precedents table, approximate). Boundless's PoVW (docs.boundless.network/zkc/mining/overview) pays ZKC pro rata to cycles proven per epoch with a stake that scales with the work, which is a reward for proving, not a leader election, and it is on a proof-of-stake chain. **The sampleability problem, plainly.** A lottery needs a puzzle whose instances are drawn at random from a distribution the miner cannot steer, whose hardness is tunable by a target, and whose solution is verifiable in milliseconds. zkVM proving has none of these: the instances (segments, jobs) are chosen by users and producers, the hardness is whatever the program is, and the verifier is tens of milliseconds to seconds. Any blend ("a miner's lottery target eases in proportion to its proven cycles last hour") gives the fastest prover more blocks, which is Aleo with a cap, and a cap small enough to be safe is a reward too small to be useful. @@ -640,10 +640,10 @@ Smaller than the sections above; each with hours and a gate. | I6 | A finality-pause page on the site that shows the connected weight fraction live, so the "node reports the pause" sentence has a public face | 4 | Shows tonight's 18:42Z pause from the observer's data | Tonight's incident | | I7 | Equivocation-evidence bounty paid in sortition slots: the key that first carries valid evidence inherits the stripped key's shard assignments for 30 days (no coins move; weight is reassigned, not created) | 12 | Two signers under one key on the fast-time harness; the evidence carrier wins the stripped key's draws | Makes watching for equivocation pay without a treasury | | I8 | Mandatory proofs activation height set from a measured coverage share (spec 7.8 item 10) | 4 | Coverage above 99 percent for 7 days on the devnet | The rule is written and off | -| I9 | The exclusive window at 25 s and the claim timeout at 120 s on the phase 4 devnet (decided by the project lead, P9) with the economy simulator re-run at the measured shard times from `prover-tiers-real-cards.md` instead of the 20-s target | 6 | The 3060 class's shard share within 5 points of its weight share | The inputs changed today | +| I9 | The exclusive window at 25 s and the claim timeout at 120 s on the phase 4 devnet (decided by the founder, P9) with the economy simulator re-run at the measured shard times from `prover-tiers-real-cards.md` instead of the 20-s target | 6 | The 3060 class's shard share within 5 points of its weight share | The inputs changed today | | I10 | `eth_getProof`, `debug_traceTransaction`, `eth_subscribe` (D5 step 2) before any outside team | 24 | Foundry's debugger and the Blockscout fork run against a devnet node | The light client and every tool depend on `eth_getProof` | | I11 | Register chain ids 4461 to 4463 on ethereum-lists/chains before the public testnet (spec 7.1) | 1 | The PR merged | Wallets | -| I12 | Publish the 2028 tier table (section 2.6) on the miner page with its three rates, so no card owner buys on a promise | 2 | Live | the project lead's consequences rule | +| I12 | Publish the 2028 tier table (section 2.6) on the miner page with its three rates, so no card owner buys on a promise | 2 | Live | the founder's consequences rule | | I13 | A spec sentence in 03 and 05: "no coin stake; the only thing at stake is 30 days of public work" (4.1) | 1 | Text | Before 3.2 is prototyped | | I14 | The litepaper's income table gains the proving-market arithmetic of 3.11 in one line | 1 | Text | Ledger P6 asked for honesty; the number makes it concrete | | I15 | A ledger entry beside E4 recording 3.6 as considered and rejected on E4's ground | 1 | Text | So the question is not re-asked | diff --git a/docs/analysis/horizon/network.md b/docs/analysis/horizon/network.md index 5ecbfe85..9229d396 100644 --- a/docs/analysis/horizon/network.md +++ b/docs/analysis/horizon/network.md @@ -6,7 +6,7 @@ What was read: `docs/spec/02-consensus.md` (2.1 parameters, 2.3 the difficulty r ## 1. The question and the answer in one paragraph -the project lead asked for Kaspa's answer to solo-miner variance: a higher block rate. Run A ran Devnet 2 at 10 blocks per second through one seed and produced 77 percent red blocks, 321 tips and a 55-block reorg. The propagation model says the links and the star did not do that: with the measured latencies it predicts under 0.1 percent red at 10 bps in a star and in a mesh. What did it is the seed's CPU per block, measured at 61 ms (narrow DAG) to 345 ms (mergeset 150 to 200), against a budget of 100 ms per block at 10 bps; with that cost in the model the star gives 47 to 88 percent red and queueing waits of 26 to 1,769 s, which are the "Accepted 100 blocks via relay" batches in the log. The difficulty rule then read blue work over a chain step capped at 2 s and hardened until the DAG ran at 2 s / (chain-step spacing) of target (model 4 blocks/s at a 5-s spacing, record 3.3 to 3.6), while a narrower-but-still-wide DAG would have made it ease (the direction main reported); counting every mergeset block over the real span, as Kaspa's window does, is unbiased in both regimes. The block rate for the public testnet is 1 bps; 10 bps is a gated step that needs the per-block node cost under 50 ms on a laptop core at a mergeset of 248, the checkpoint interval and the clock cap re-denominated in DAA seconds, and vote aggregation, because with C1 in blue blocks 8,192 voters at 10 bps are 66 GB per node per day of votes. +The founder asked for Kaspa's answer to solo-miner variance: a higher block rate. Run A ran Devnet 2 at 10 blocks per second through one seed and produced 77 percent red blocks, 321 tips and a 55-block reorg. The propagation model says the links and the star did not do that: with the measured latencies it predicts under 0.1 percent red at 10 bps in a star and in a mesh. What did it is the seed's CPU per block, measured at 61 ms (narrow DAG) to 345 ms (mergeset 150 to 200), against a budget of 100 ms per block at 10 bps; with that cost in the model the star gives 47 to 88 percent red and queueing waits of 26 to 1,769 s, which are the "Accepted 100 blocks via relay" batches in the log. The difficulty rule then read blue work over a chain step capped at 2 s and hardened until the DAG ran at 2 s / (chain-step spacing) of target (model 4 blocks/s at a 5-s spacing, record 3.3 to 3.6), while a narrower-but-still-wide DAG would have made it ease (the direction main reported); counting every mergeset block over the real span, as Kaspa's window does, is unbiased in both regimes. The block rate for the public testnet is 1 bps; 10 bps is a gated step that needs the per-block node cost under 50 ms on a laptop core at a mergeset of 248, the checkpoint interval and the clock cap re-denominated in DAA seconds, and vote aggregation, because with C1 in blue blocks 8,192 voters at 10 bps are 66 GB per node per day of votes. ## 2. Method diff --git a/docs/analysis/horizon/new-pow.md b/docs/analysis/horizon/new-pow.md index 78a30fae..ee72b3c6 100644 --- a/docs/analysis/horizon/new-pow.md +++ b/docs/analysis/horizon/new-pow.md @@ -2,7 +2,7 @@ 6 October 2026, evening UK, lane `new-proof-of-work`, worktree `/Users/joshm/Projects/igneum-wt-horizon` (branch `horizon` from master, at 3f4f719). Output of this lane: this file and `proto-newpow//`. Nothing here touches the shipped hash, `igneum-pow`, the node, the manifest or the live devnet; every prototype is a benchmark beside the worker, never inside it. -the project lead's mandate, verbatim: "if we create a new way of hashing or a new way of proof of work to revolutionise the space then that's absolutely fine, I want you to deploy everything to create something that has not ever been done before." +The founder's mandate, verbatim: "if we create a new way of hashing or a new way of proof of work to revolutionise the space then that's absolutely fine, I want you to deploy everything to create something that has not ever been done before." ## 0. Progress (kept current for the coordinator) @@ -286,7 +286,7 @@ Reading: B moves the f = 1 chip's edge by 1.4x to 2x at `k = 1` and by 1.1x at ` ## 7. Ranked next steps -Hours are agent hours (the project lead's rule: Claude-side work takes hours). Each gate is a measurable pass line. Consequence per tier is the row's own. Ranks 2, 3 and 4 were written as B's gates before the ladder landed; after section 5 they are WITHDRAWN (B is not carried forward as class content; the rows stay so the reasoning is visible) and the live order is 1, 5, 6, 7, 8. +Hours are agent hours (the founder's rule: Claude-side work takes hours). Each gate is a measurable pass line. Consequence per tier is the row's own. Ranks 2, 3 and 4 were written as B's gates before the ladder landed; after section 5 they are WITHDRAWN (B is not carried forward as class content; the rows stay so the reasoning is visible) and the live order is 1, 5, 6, 7, 8. | Rank | Proposal | Evidence | Model | Hours | Consequence per tier | Gate | |---|---|---|---|---|---|---| @@ -314,11 +314,11 @@ One paragraph each. | Question | Why it could not be closed tonight | What closes it | |---|---|---| | The 2019-class core (O-1.14) | no such core in the fleet; igneum-build-1 is Zen 4, the Mac is M5 Max; the 2.5x rule stands in | rank 7 | -| The AMD WMMA fragment layout | PC 1 is the project lead's desk and the AMD rows were owed all day (status file); no AMD card on RunPod or Vast tonight (fleet agent) | rank 2 | +| The AMD WMMA fragment layout | PC 1 is the founder's desk and the AMD rows were owed all day (status file); no AMD card on RunPod or Vast tonight (fleet agent) | rank 2 | | The Apple emulation of mm8 | a Metal emulation kernel is a 3-hour job and the Mac measure lock was free; not started because the AMD gate decides first whether B proceeds | rank 3 | | The canonical state serialisation for C | a design item that touches the exec layer (`igneum/exec`), out of this lane's files | rank 1 | | The pruning-proof witness for C's historical PoW | spec 02 and 10 items | rank 1 | -| Whether the tensor path's marginal energy on the 5090 differs from the 4090's | one card measured (box 1); the 5090 is on the project lead's desk | a PC 2 job with the same `run.sh` | +| Whether the tensor path's marginal energy on the 5090 differs from the 4090's | one card measured (box 1); the 5090 is on the founder's desk | a PC 2 job with the same `run.sh` | | The Ampere row (3060, 3080, 3090) | every Ampere card of the fleet was mining and proving the live devnet; a loaded 3090 was offered and declined (a loaded card's rate is not a number) | one quiet Ampere pod | | The verifier with a byte-dot instruction (VNNI, NEON udot) | the C reference is scalar | 1 h: an AVX-VNNI and a NEON path in `verify_ref.c`, measured on both cores | | Scheme C's leaf array on an 8 GB card at the 2 GiB design size | the build holds dataset + cache + leaves on the device (4.3 GiB) unless chunked | the chunked build (section 5.2 says whether it is trivial) | diff --git a/docs/analysis/horizon/polish.md b/docs/analysis/horizon/polish.md index 7cf2f844..7644bc3b 100644 --- a/docs/analysis/horizon/polish.md +++ b/docs/analysis/horizon/polish.md @@ -2,7 +2,7 @@ Date: 6 October 2026, evening UK (written 20:00 to 21:00Z, while the live devnet's finality was still paused). Lane 6 of the Horizon programme. Worktree `/Users/joshm/Projects/igneum-wt-horizon` (branch `horizon`, HEAD c3aa502). Output: this file only. No file outside it was edited; nothing was built, deployed, posted or started; the installed app and every live service were read, never touched. -the project lead's bar: "a level of polish that has not been seen before." This file is the honest audit: what each shipped system does, the named comparator and the exact screen or feature it has that we lack or do worse, the ledger rows that close the gap (95 rows, ids Q1 to Q105 with gaps, each with a file or screen, a severity, hours of agent work, an owner and a gate), and what is already better than the comparator. The first ten rows are what the project lead will notice first when he opens each product tomorrow. +The founder's bar: "a level of polish that has not been seen before." This file is the honest audit: what each shipped system does, the named comparator and the exact screen or feature it has that we lack or do worse, the ledger rows that close the gap (95 rows, ids Q1 to Q105 with gaps, each with a file or screen, a severity, hours of agent work, an owner and a gate), and what is already better than the comparator. The first ten rows are what the founder will notice first when he opens each product tomorrow. ## 0. What was read and run @@ -14,26 +14,26 @@ Run (nothing live, nothing built): `node --test app/igneum-app/ui/*.test.mjs` (3 Comparator claims are from memory unless a repository or page is named, and are labelled approximate. -## 1. The ten rows the project lead will notice first +## 1. The ten rows the founder will notice first | Rank | Id | Row | File or screen | Severity | Hours | Owner | Who it hits | Gate | |---|---|---|---|---|---|---|---|---| -| 1 | Q1 | The pause has no cause anywhere. The node reports `finality_reason` (`active`, `window filling, N of M`, `paused`) but not the frozen-table share that held tonight's pause; the observer drops even the reason; the API has no reason field; `/live` computes "N% of weight silent" from the sliding table, which read 11 percent silent at 20:05Z while finality was paused (held by the table frozen at lock 6842, 57.3 percent signing). Add `finality_reason`, `held_by` (frozen index, its signing share, its expiry DAA) and lane 3's `finality_provisional` end to end | `vendor/igneum-node-0310/consensus/src/processes/finality.rs:1794-1807` (reason exists, no frozen share), `tools/observer/observer.mjs:720` (copies `finalityActive` only), `site/api/live.mjs:114-122` (no reason), `site/live.html:520` (the silent percentage) | the project lead-visible | 4 (node 1.5, observer and API 1, `/live` and `/api/stats` copy 1, spec 3.9 row 0.5) | consensus-engineer (node), site owner (observer, API, page) | holder, exchange: the one line that says whether a pause is a silent third, a split, or a table waiting to expire; miner: nothing lost, but the app can finally explain itself; operator: a pause with a cause and an end time | A forced pause on Devnet 2 (one slice over a third stopped): `/api/live.finality` carries `reason`, `held_by`, `provisional`; `/live` reads "finality paused since 18:40Z: 89% of the sliding weight is signing, held by the table frozen at lock 6842 (57% signing) until DAA 216,402 (about 20:40Z)"; the exchange guidance in spec 3.9 carries the provisional row (lane 3 proposal 4) | -| 2 | Q2 | Ember never says "paused". The engine keeps only `last_lock`, `last_lock_at`, `age_s`, `votes`; the node line showed "#6842 ยท 1 h ago" under "a point the miners agreed can never be undone" for the whole two-hour pause. No `finality_active` anywhere in the app | `app/igneum-app/src/state.rs:247-248`, `src/engine.rs:3495-3497, 3896-3902` (lock parsed from the miner's `LOCK checkpoint` line only), `ui/app.js:1350-1351` | the project lead-visible | 3 | app owner (engine reads `getFinalityCheckpoints` or the miner's `FINALITY` line, spec 3.9 row; UI state and view test) | home miner and rig: the app tells them finality is paused and why, instead of a lock age that climbs; pool user: the same on the pool page later | Mock state with `finality_active:false, reason:"paused"` renders "Finality paused since 18:40Z, 89% of the 30-day weight signing, held by the frozen table until about 20:40Z" on the node line and in the pill; `view.test.mjs` case; seen on the Devnet 2 forced pause | -| 3 | Q3 | A fresh machine's first 60 seconds start with a warning. macOS: ad hoc signature, no Developer ID, no notarisation; the engine strips `com.apple.quarantine` from its own bundle on start; the README's "Right-click > Open" bypass is gone on macOS 15 (approximate). Windows: installer and exes unsigned, SmartScreen "Windows protected your PC", then a UAC prompt for the firewall rule 20 to 50 s in before any screen explains it | `packaging/mac/build-dmg.sh:118-122`, `packaging/mac/README.md:51-53`, `app/igneum-app/src/main.rs:78`, `packaging/windows/README.md:76, 111`, `packaging/windows/build-installer.ps1:188`, `app/igneum-app/src/ota.rs:981-1015` | the project lead-visible | 6 (Developer ID signing plus `notarytool` and stapling in `build-dmg.sh` 3; Authenticode `signtool` step in `windows.yml` and `build-installer.ps1` 2; firewall step after `setup_done` with a sentence on the Cards screen 1), plus the project lead: an Apple Developer account and an OV or EV code-signing certificate (purchases, hours of the project lead's time, approximate) | app owner; the project lead (the certificates) | every new miner on every tier, Windows and macOS: Signal's and Tailscale's installers open with no warning (approximate); today ours open with two | Fresh macOS 15 VM: the DMG's app opens on double-click, `spctl -a -vv` says accepted and notarised; fresh Windows 11 VM: no SmartScreen interstitial, `signtool verify /pa` passes; the first UAC prompt appears after the Cards screen names it | -| 4 | Q4 | Every update is "urgent" once an activation height is behind the node: `fork_is_close(Some(198000), 209000)` is true, so the manifest's stale `activation 198000` turned the 0.3.14 update into "the node is 0 blocks away. Installing now", stripped Later and skipped every safe-moment guard (the PC 1 install under a measurement job at 17:52:54Z). The same path has no finality input: an update applies through a pause | `app/igneum-app/src/manifest.rs:350-355` (`daa + 1800 >= h`), `:322-347` (`safe_to_apply`, no finality), `src/ota.rs:484`, `ui/app.js:217`, `docs/plans/release-0.3.14.md:79`, `release-0.3.15.md` section 6 | the project lead-visible | 3 (close means within 1,800 blocks ahead and not behind 1; the publisher drops a passed `activation_height` 0.5; `Moment.finality_paused` holds a non-urgent update 1.5) | app owner | home miner, rig: no surprise restart mid-measurement or mid-pause; operator: an activation that has passed is not an emergency | Unit tests: behind by any amount is not close; `publish-manifest.sh` refuses a passed height; a paused mock holds the update with the words "finality is paused; installing when it resumes"; the 0.3.16 manifest carries no stale height | -| 5 | Q5 | The homepage says "final" while finality is paused, in a 90-word hero, under a share card that renders as a thumbnail. The chain scene takes `src.locked` and draws the dashed line labelled "final" at the newest locked block with no `finality.active` check (R4.6.3 was fixed on `/live` only); the hero is one 90-word paragraph with two bench deep links and "ships when the packaging row lands"; the miner section still says "a 24 GB NVIDIA card proves as well" while the hero and litepaper say 8 GB; home, litepaper, live and explorer share a 256 px `og-small.png` with `summary` cards | `site/index.html:810, 838` (final label), `:345` (hero), `:494` and `site/miner.html:7, 13, 21, 265, 382` (24 GB), `site/index.html:15-19` (OG) | the project lead-visible | 3 (final label 0.5, hero 1, 24 GB sentence 0.5, four 1200x630 cards 1) | site owner | every visitor; a holder or exchange reading "final" during a pause is the worst of them | No "final" label while `finality.active` is false (the `/live` rule); hero under 40 words with one link; one proving-tier sentence on every page; every page's shared card is 1200x630 | -| 6 | Q6 | The hub shows stale data as live, said "active" for the first 20 unlocked checkpoints, says "paused" with no since or cause, and buried the pause under 401 miner_quiet and miner_back events. After the first load an API failure only changes the header to "offline: ..."; tiles, cards and "Refreshed" keep the old values. `finality_active` flips only after `presence_window` (20 on the devnet) indices without a lock, so 18:42 to 18:50Z read "active" in white with no lock forming. No `finality_paused` or `finality_resumed` event exists; `live_events` between 18:25Z and 21:30Z holds 203 `miner_quiet`, 198 `miner_back`, 55 `difficulty`, 30 `checkpoint_locked` (the last at 18:42:10Z) and no pause row. The checkpoint table also showed "% of active" above 100 (102.4 to 113.4 percent on indices 6900 to 6930) | `relay/ui.html:564` (stale), `:367-368` (tile), `relay/api/console.mjs:214`, `vendor/igneum-node-0310/consensus/src/processes/finality.rs:1794` and `vendor/igneum-node/consensus/core/src/finality.rs:63, 75` (`presence_window` 240 mainnet, 20 devnet), `tools/observer/observer.mjs:652` (the only finality event), `live_checkpoints.fraction_active` rows 6900 to 6930 | the project lead-visible | 4 (dim every panel with "last data N min ago" 1; amber on the first under-2/3 checkpoint 0.5; `finality_paused` and `finality_resumed` events, and collapse quiet/back pairs under 5 min 1.5; clamp or explain the active fraction 1) | fleet agent (hub), consensus-engineer (observer events, the fraction) | operator: the hub is the project lead's first screen; a holder reading the public API gets the same events | Cut the network: every hub panel dims within 15 s; a forced Devnet 2 pause turns the tile amber on the first checkpoint under two thirds, posts one `finality_paused` event with the cause and one `finality_resumed` with the duration; no fraction above 100 percent on any row for 24 h | -| 7 | Q7 | Discord said nothing. The bot has no credentials file and its timer is not installed, so tonight's pause produced zero posts; had it been live, a condition already firing at the watcher's first look is marked `preexisting` and never opens an incident; the pulse hides the pause in its description with no colour or title change | `docs/community/discord-hooks.md:81-82`, `tools/community/discord-hooks.mjs:481-485`, `:252-255`, `:158`, `infra/build-server/discord-hooks/install.sh` | the project lead-visible | 3 (install and one live pulse 1; a pre-existing condition opens with "since at least " 1; paused pulse in a distinct colour with a title suffix 1) | miner-community-lead (community owner) | every Discord reader, which on launch day is every miner | `check` prints three "set"; one live pulse in #numbers; a fixture where the pause predates the first tick opens an incident; the paused pulse renders amber with "(finality paused)" in the title | +| 1 | Q1 | The pause has no cause anywhere. The node reports `finality_reason` (`active`, `window filling, N of M`, `paused`) but not the frozen-table share that held tonight's pause; the observer drops even the reason; the API has no reason field; `/live` computes "N% of weight silent" from the sliding table, which read 11 percent silent at 20:05Z while finality was paused (held by the table frozen at lock 6842, 57.3 percent signing). Add `finality_reason`, `held_by` (frozen index, its signing share, its expiry DAA) and lane 3's `finality_provisional` end to end | `vendor/igneum-node-0310/consensus/src/processes/finality.rs:1794-1807` (reason exists, no frozen share), `tools/observer/observer.mjs:720` (copies `finalityActive` only), `site/api/live.mjs:114-122` (no reason), `site/live.html:520` (the silent percentage) | the founder-visible | 4 (node 1.5, observer and API 1, `/live` and `/api/stats` copy 1, spec 3.9 row 0.5) | consensus-engineer (node), site owner (observer, API, page) | holder, exchange: the one line that says whether a pause is a silent third, a split, or a table waiting to expire; miner: nothing lost, but the app can finally explain itself; operator: a pause with a cause and an end time | A forced pause on Devnet 2 (one slice over a third stopped): `/api/live.finality` carries `reason`, `held_by`, `provisional`; `/live` reads "finality paused since 18:40Z: 89% of the sliding weight is signing, held by the table frozen at lock 6842 (57% signing) until DAA 216,402 (about 20:40Z)"; the exchange guidance in spec 3.9 carries the provisional row (lane 3 proposal 4) | +| 2 | Q2 | Ember never says "paused". The engine keeps only `last_lock`, `last_lock_at`, `age_s`, `votes`; the node line showed "#6842 ยท 1 h ago" under "a point the miners agreed can never be undone" for the whole two-hour pause. No `finality_active` anywhere in the app | `app/igneum-app/src/state.rs:247-248`, `src/engine.rs:3495-3497, 3896-3902` (lock parsed from the miner's `LOCK checkpoint` line only), `ui/app.js:1350-1351` | the founder-visible | 3 | app owner (engine reads `getFinalityCheckpoints` or the miner's `FINALITY` line, spec 3.9 row; UI state and view test) | home miner and rig: the app tells them finality is paused and why, instead of a lock age that climbs; pool user: the same on the pool page later | Mock state with `finality_active:false, reason:"paused"` renders "Finality paused since 18:40Z, 89% of the 30-day weight signing, held by the frozen table until about 20:40Z" on the node line and in the pill; `view.test.mjs` case; seen on the Devnet 2 forced pause | +| 3 | Q3 | A fresh machine's first 60 seconds start with a warning. macOS: ad hoc signature, no Developer ID, no notarisation; the engine strips `com.apple.quarantine` from its own bundle on start; the README's "Right-click > Open" bypass is gone on macOS 15 (approximate). Windows: installer and exes unsigned, SmartScreen "Windows protected your PC", then a UAC prompt for the firewall rule 20 to 50 s in before any screen explains it | `packaging/mac/build-dmg.sh:118-122`, `packaging/mac/README.md:51-53`, `app/igneum-app/src/main.rs:78`, `packaging/windows/README.md:76, 111`, `packaging/windows/build-installer.ps1:188`, `app/igneum-app/src/ota.rs:981-1015` | the founder-visible | 6 (Developer ID signing plus `notarytool` and stapling in `build-dmg.sh` 3; Authenticode `signtool` step in `windows.yml` and `build-installer.ps1` 2; firewall step after `setup_done` with a sentence on the Cards screen 1), plus the founder: an Apple Developer account and an OV or EV code-signing certificate (purchases, hours of the founder's time, approximate) | app owner; the founder (the certificates) | every new miner on every tier, Windows and macOS: Signal's and Tailscale's installers open with no warning (approximate); today ours open with two | Fresh macOS 15 VM: the DMG's app opens on double-click, `spctl -a -vv` says accepted and notarised; fresh Windows 11 VM: no SmartScreen interstitial, `signtool verify /pa` passes; the first UAC prompt appears after the Cards screen names it | +| 4 | Q4 | Every update is "urgent" once an activation height is behind the node: `fork_is_close(Some(198000), 209000)` is true, so the manifest's stale `activation 198000` turned the 0.3.14 update into "the node is 0 blocks away. Installing now", stripped Later and skipped every safe-moment guard (the PC 1 install under a measurement job at 17:52:54Z). The same path has no finality input: an update applies through a pause | `app/igneum-app/src/manifest.rs:350-355` (`daa + 1800 >= h`), `:322-347` (`safe_to_apply`, no finality), `src/ota.rs:484`, `ui/app.js:217`, `docs/plans/release-0.3.14.md:79`, `release-0.3.15.md` section 6 | the founder-visible | 3 (close means within 1,800 blocks ahead and not behind 1; the publisher drops a passed `activation_height` 0.5; `Moment.finality_paused` holds a non-urgent update 1.5) | app owner | home miner, rig: no surprise restart mid-measurement or mid-pause; operator: an activation that has passed is not an emergency | Unit tests: behind by any amount is not close; `publish-manifest.sh` refuses a passed height; a paused mock holds the update with the words "finality is paused; installing when it resumes"; the 0.3.16 manifest carries no stale height | +| 5 | Q5 | The homepage says "final" while finality is paused, in a 90-word hero, under a share card that renders as a thumbnail. The chain scene takes `src.locked` and draws the dashed line labelled "final" at the newest locked block with no `finality.active` check (R4.6.3 was fixed on `/live` only); the hero is one 90-word paragraph with two bench deep links and "ships when the packaging row lands"; the miner section still says "a 24 GB NVIDIA card proves as well" while the hero and litepaper say 8 GB; home, litepaper, live and explorer share a 256 px `og-small.png` with `summary` cards | `site/index.html:810, 838` (final label), `:345` (hero), `:494` and `site/miner.html:7, 13, 21, 265, 382` (24 GB), `site/index.html:15-19` (OG) | the founder-visible | 3 (final label 0.5, hero 1, 24 GB sentence 0.5, four 1200x630 cards 1) | site owner | every visitor; a holder or exchange reading "final" during a pause is the worst of them | No "final" label while `finality.active` is false (the `/live` rule); hero under 40 words with one link; one proving-tier sentence on every page; every page's shared card is 1200x630 | +| 6 | Q6 | The hub shows stale data as live, said "active" for the first 20 unlocked checkpoints, says "paused" with no since or cause, and buried the pause under 401 miner_quiet and miner_back events. After the first load an API failure only changes the header to "offline: ..."; tiles, cards and "Refreshed" keep the old values. `finality_active` flips only after `presence_window` (20 on the devnet) indices without a lock, so 18:42 to 18:50Z read "active" in white with no lock forming. No `finality_paused` or `finality_resumed` event exists; `live_events` between 18:25Z and 21:30Z holds 203 `miner_quiet`, 198 `miner_back`, 55 `difficulty`, 30 `checkpoint_locked` (the last at 18:42:10Z) and no pause row. The checkpoint table also showed "% of active" above 100 (102.4 to 113.4 percent on indices 6900 to 6930) | `relay/ui.html:564` (stale), `:367-368` (tile), `relay/api/console.mjs:214`, `vendor/igneum-node-0310/consensus/src/processes/finality.rs:1794` and `vendor/igneum-node/consensus/core/src/finality.rs:63, 75` (`presence_window` 240 mainnet, 20 devnet), `tools/observer/observer.mjs:652` (the only finality event), `live_checkpoints.fraction_active` rows 6900 to 6930 | the founder-visible | 4 (dim every panel with "last data N min ago" 1; amber on the first under-2/3 checkpoint 0.5; `finality_paused` and `finality_resumed` events, and collapse quiet/back pairs under 5 min 1.5; clamp or explain the active fraction 1) | fleet agent (hub), consensus-engineer (observer events, the fraction) | operator: the hub is the founder's first screen; a holder reading the public API gets the same events | Cut the network: every hub panel dims within 15 s; a forced Devnet 2 pause turns the tile amber on the first checkpoint under two thirds, posts one `finality_paused` event with the cause and one `finality_resumed` with the duration; no fraction above 100 percent on any row for 24 h | +| 7 | Q7 | Discord said nothing. The bot has no credentials file and its timer is not installed, so tonight's pause produced zero posts; had it been live, a condition already firing at the watcher's first look is marked `preexisting` and never opens an incident; the pulse hides the pause in its description with no colour or title change | `docs/community/discord-hooks.md:81-82`, `tools/community/discord-hooks.mjs:481-485`, `:252-255`, `:158`, `infra/build-server/discord-hooks/install.sh` | the founder-visible | 3 (install and one live pulse 1; a pre-existing condition opens with "since at least " 1; paused pulse in a distinct colour with a title suffix 1) | miner-community-lead (community owner) | every Discord reader, which on launch day is every miner | `check` prints three "set"; one live pulse in #numbers; a fixture where the pause predates the first tick opens an incident; the paused pulse renders amber with "(finality paused)" in the title | | 8 | Q8 | The relay's security fixes are not on master. Commit 28c028b (X23 to X28: `relay/lib/guard.mjs`, `handler.mjs`, run signatures, retention) is on `fud-close` and the `ledger-*` branches only; `git merge-base --is-ancestor 28c028b master` is false. On this tree any of the three intake keys (one ships in every miner app) can post, sync and delete console items, read the whole feed, and every client puts the token in the URL path | `relay/api/console.mjs:249, 283-301`, `relay/lib/relay.mjs:34-46`, `relay/clients/agent.sh:10`, `igneum-agent.ps1:13, 72, 154`, `tools/relay.mjs:24` | operator-visible (security) | 2 (merge or cherry-pick with the 47 relay tests 1; handler test that an intake key gets 403 on post, sync, delete 1) | fleet agent (relay owner) | operator: the console is the control plane for every PC and the fleet; a miner app's intake key is in every install | `28c028b` is an ancestor of the deploying branch; 47 relay tests pass; the handler test above passes; `relay/README.md` names the deployed commit | -| 9 | Q86 | The fleet page is blind to the standing fleet, to death and to finality. `page.py:43-47` publishes `standing` and `devnet2` and the page references neither (grep 0); a dead box reads "running" for up to 20 minutes because state comes from `boxes.json` and `standing.jsonl` is never read; no fleet or console script reads `finality_active`, so tonight's pause was found by hand. The night the project lead ruled that 22 boxes stay up permanently, the page that shows them cannot show them | `igneum-wt-gpu-fleet/tools/fleet/page.py:38-47`, dlsite `index.html:243`, `lib/standing.py:54-56`, `tools/console.mjs:124` | the project lead-visible | 6 (standing block with USD per day and the Devnet 2 gate line 2; "unreachable since HH:MM" within 2 minutes 2; finality on the page and in `console.mjs chain` 2) | fleet agent | operator (the project lead reads this page every evening); every tier indirectly: a dead standing box is weight that left silently, the class of tonight's pause | the page shows the standing count and spend; a box killed by hand reads "unreachable since" within 2 minutes; a forced Devnet 2 pause reads on the page and in the console | -| 10 | Q9 | The wallet page describes 0.1.4 while UI 3 (five state words, light mode, pounds line) sits unmerged on `wallet-ui-3`; the MetaMask guide's testnet RPC is still labelled a placeholder, its devnet RPC port is the miner's node (26790) while a wallet-only Mac runs 26800, and `wallet_addEthereumChain` carries `blockExplorerUrls: []` and no `iconUrls`, so MetaMask shows no explorer link and a blank icon | `site/wallet.html:261-266, 312`, `igneum-wt-wallet-ui/docs/plans/wallet-ui-3.md:93, 112`, `site/metamask.html:197, 225, 233, 285-286`, `igneum-wt-wallet/app/igneum-wallet/src/node.rs:24-26` | the project lead-visible | 6 (wallet 0.1.5 with UI 3 published and the page rewritten 4; guide: both ports, the placeholder line removed, explorer URL and icon in the request 2) | app owner (wallet), site owner (guide) | holder: the first non-miner product; a wrong port or a placeholder RPC is a dead end at the first step | `downloads.json` wallet-mac 0.1.5; the page names pending, included, executed, proven, finalised; `eth_chainId` returns 0x116e from the guide's URL; MetaMask shows the explorer link after the add-chain button | +| 9 | Q86 | The fleet page is blind to the standing fleet, to death and to finality. `page.py:43-47` publishes `standing` and `devnet2` and the page references neither (grep 0); a dead box reads "running" for up to 20 minutes because state comes from `boxes.json` and `standing.jsonl` is never read; no fleet or console script reads `finality_active`, so tonight's pause was found by hand. The night the founder ruled that 22 boxes stay up permanently, the page that shows them cannot show them | `igneum-wt-gpu-fleet/tools/fleet/page.py:38-47`, dlsite `index.html:243`, `lib/standing.py:54-56`, `tools/console.mjs:124` | the founder-visible | 6 (standing block with USD per day and the Devnet 2 gate line 2; "unreachable since HH:MM" within 2 minutes 2; finality on the page and in `console.mjs chain` 2) | fleet agent | operator (the founder reads this page every evening); every tier indirectly: a dead standing box is weight that left silently, the class of tonight's pause | the page shows the standing count and spend; a box killed by hand reads "unreachable since" within 2 minutes; a forced Devnet 2 pause reads on the page and in the console | +| 10 | Q9 | The wallet page describes 0.1.4 while UI 3 (five state words, light mode, pounds line) sits unmerged on `wallet-ui-3`; the MetaMask guide's testnet RPC is still labelled a placeholder, its devnet RPC port is the miner's node (26790) while a wallet-only Mac runs 26800, and `wallet_addEthereumChain` carries `blockExplorerUrls: []` and no `iconUrls`, so MetaMask shows no explorer link and a blank icon | `site/wallet.html:261-266, 312`, `igneum-wt-wallet-ui/docs/plans/wallet-ui-3.md:93, 112`, `site/metamask.html:197, 225, 233, 285-286`, `igneum-wt-wallet/app/igneum-wallet/src/node.rs:24-26` | the founder-visible | 6 (wallet 0.1.5 with UI 3 published and the page rewritten 4; guide: both ports, the placeholder line removed, explorer URL and icon in the request 2) | app owner (wallet), site owner (guide) | holder: the first non-miner product; a wrong port or a placeholder RPC is a dead end at the first step | `downloads.json` wallet-mac 0.1.5; the page names pending, included, executed, proven, finalised; `eth_chainId` returns 0x116e from the guide's URL; MetaMask shows the explorer link after the add-chain button | Next in line, outside the ten: Q10 (the explorer knows nothing about finality and swallows failures after first load, 3 hours), Q80 (the Windows installer pipeline is dead on GitHub billing) and Q12 (Ember's light mode fails contrast on the private key and every live state). -Hours for the ten: 40 agent hours, plus the project lead's certificate purchases for Q3. +Hours for the ten: 40 agent hours, plus the founder's certificate purchases for Q3. -## 2. Tonight's pause on every surface (the ledger row the project lead asked for) +## 2. Tonight's pause on every surface (the ledger row the founder asked for) Times UTC. The chain-side facts are lane 3's (`finality-and-weight.md` 3.1) and the observer's own rows, read tonight: last lock 6842 at about 18:39:40Z; 6843 proposed at 53.1 percent of total; `finality_active` false from about 18:50Z (index 6862, twenty indices without a lock, `presence_window` 20); no lock after 6842 as of 20:05:03Z (`live_state.updated_at`), `total_weight` 7,167, `active_weight` 6,346 (88.5 percent signing on the sliding table), `voters` 85, and no `finality_reason` key in the stored JSON. Sampled `live_checkpoints` rows: @@ -82,20 +82,20 @@ The row for the ledger: **Q1 plus Q2, Q5, Q6, Q7, Q10**: every public and operat | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| -| Q2 | Finality pause invisible (section 1) | `state.rs:247`, `engine.rs:3495`, `app.js:1350` | the project lead-visible | 3 | app owner | section 1 | -| Q4 | Every update urgent once the activation is behind; no finality hold (section 1) | `manifest.rs:322-355` | the project lead-visible | 3 | app owner | section 1 | +| Q2 | Finality pause invisible (section 1) | `state.rs:247`, `engine.rs:3495`, `app.js:1350` | the founder-visible | 3 | app owner | section 1 | +| Q4 | Every update urgent once the activation is behind; no finality hold (section 1) | `manifest.rs:322-355` | the founder-visible | 3 | app owner | section 1 | | Q11 | Offline start says "The update failed." A check error with no held manifest calls `set_error`; the cached manifest is loaded for the floor but never into `self.manifest`; the notice offers "Try again", which POSTs install | `ota.rs:649-654, 195-200`, `app.js:29, 1198` | user-visible | 1 | app owner | notices test: a check-class error renders "Could not check for updates" with no install action | | Q12 | Light mode fails contrast on every live text: molten `#B8731F` on white 3.81:1, on bone 3.38:1; ember `#E04A14` on bone 3.61:1; molten carries the private key, the address and the live states. `miner-ui-3.md` section 9 claims 4.5:1 everywhere | `app.css:29, 263, 338, 431` | user-visible | 1 | app owner | a node script over the token pairs asserts 4.5:1 for every text token in both schemes; CI runs it | -| Q13 | Per-card rejects and faults never reach the row (the lolMiner line) | `app.js:291-309, 1290` | the project lead-visible on PC 1 | 2 | miner-community-lead | row meta "N rejected ยท M faults" when nonzero; view test | +| Q13 | Per-card rejects and faults never reach the row (the lolMiner line) | `app.js:291-309, 1290` | the founder-visible on PC 1 | 2 | miner-community-lead | row meta "N rejected ยท M faults" when nonzero; view test | | Q14 | Windows first run raises a UAC prompt (firewall rule) before any screen explains it (part of Q3) | `ota.rs:981-1015`, `index.html:71-99` | user-visible | 1 | app owner | the firewall step runs after `setup_done`; the Cards screen names it | -| Q15 | Mac first-run instruction stale: "Right-click > Open" (part of Q3) | `packaging/mac/README.md:51`, `site/miner.html` download copy | the project lead-visible on a fresh Mac | 1 | app owner | the download page and README name the Privacy and Security step until the certificate lands | +| Q15 | Mac first-run instruction stale: "Right-click > Open" (part of Q3) | `packaging/mac/README.md:51`, `site/miner.html` download copy | the founder-visible on a fresh Mac | 1 | app owner | the download page and README name the Privacy and Security step until the certificate lands | | Q16 | No rate history: a 10-minute strip against HiveOS's hours | `app.js:1052-1127` | user-visible | 4 | app owner | one-hour per-card sparkline from an engine ring buffer; view test | | Q17 | Hill climb hidden until `tune_climb` is on the state | `index.html:341`, `app.js:1407` | operator-visible | 1 | app owner | the row shows whenever the engine answers `tune/goal` | | Q18 | Earnings never shows mined IGN; "ยฃ0.00 earned" is the first number on the tab | `index.html:250`, `app.js:1364` | user-visible | 2 | miner-community-lead | the tab shows blocks mined and the subsidy they earned in IGN, with the devnet line under it | | Q19 | 10 px type in five places, 9 px ruler | `app.css:196, 271, 272, 302, 491, 531` | cosmetic | 1 | app owner | no `font-size` under 11 px except the ruler | | Q20 | Design screens in `docs/design/app-screens/*.png` are a UI 1 app (v0.3.0 tiles, a Finality card the shipped UI lacks) | `docs/design/app-screens/` | cosmetic | 1 | app owner | screens regenerated from `?screen=` on the current build | | Q21 | AMD step line prints a percent as watts ("owed: a unit word for AMD") | `ember-tune.md:278` | operator-visible | 1 | miner-community-lead | the line reads "70% (an offset)"; unit test | -| Q22 | A code comment naming the founder ships in the UI bundle (`// The list is ordered by performance (the project lead, 6 October 2026)`), and the ui bundle is outside every forbidden-string check | `app/igneum-app/ui/app.js:270`, `tools/ci/identity-check.sh` | cosmetic (identity rule) | 0.5 | app owner | `tools/ci` forbidden-string check covers `app/igneum-app/ui/`; the comment reads "(ruling of 6 October 2026)" | +| Q22 | A code comment naming the founder ships in the UI bundle (`// The list is ordered by performance (the founder, 6 October 2026)`), and the ui bundle is outside every forbidden-string check | `app/igneum-app/ui/app.js:270`, `tools/ci/identity-check.sh` | cosmetic (identity rule) | 0.5 | app owner | `tools/ci` forbidden-string check covers `app/igneum-app/ui/`; the comment reads "(ruling of 6 October 2026)" | | Q23 | Two copy-law slips: "Votes lock the chain; leave it on." (aphorism), "The window can close. The miner keeps going" (two-beat) | `index.html:404, 81` | cosmetic | 0.5 | app owner | reworded; the grep below stays at 0 em dashes | **Error states, exact strings.** Engine down (four failed polls): pill "engine away" (`app.js:1437`); node line "Node: no answer", sub "the engine is not answering; the window reconnects by itself"; Mac host "The engine stopped. Quit and open Igneum Miner again." (`IgneumMiner.swift:233`); Windows "The engine stopped. Close this window and open Igneum Miner again." (`host.cpp:301`). Node down: "the node could not start" or the engine message, "the node is not running", "the external node went away" (`app.js:452-453`, `engine.rs:3163`); pill "node failed" (`app.js:1428`); digest box "the node is not running" (`app.js:1341`); canvas "waiting for the node to sync" (`app.js:1099`). Finality paused: nothing (Q2). Update server unreachable: "The update failed. Manifest signature: ." (`app.js:1360`, Q11). GPU lost: "Card removed: . Its worker stopped." (`app.js:95`), row word "removed", sub "unplugged; its worker stopped. The row goes in five minutes." (`app.js:303`); Code 43: ": not usable (Code 43). No worker runs on it." with "reboot with the card attached; if it persists, reinstall the driver with the card attached" (`app.js:100`, `detect.rs:55`). @@ -123,10 +123,10 @@ The row for the ledger: **Q1 plus Q2, Q5, Q6, Q7, Q10**: every public and operat | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| -| Q9 | Page vs UI 3; guide placeholders; empty explorer URL (section 1) | `site/wallet.html`, `site/metamask.html` | the project lead-visible | 6 | app owner, site owner | section 1 | +| Q9 | Page vs UI 3; guide placeholders; empty explorer URL (section 1) | `site/wallet.html`, `site/metamask.html` | the founder-visible | 6 | app owner, site owner | section 1 | | Q30 | Private key export copies to the clipboard with no clear and no timer | `igneum-wt-wallet/.../ui/app.js:129, 463` | user-visible | 2 | app owner | clipboard cleared after 60 s by the host; unit test on the timer | | Q31 | Wallet OTA trusts one key; no `revoked_keys`, unlike the miner's updater | `docs/plans/consequences-2026-10-05.md:36`, `updater.rs:7, 729` | operator-visible | 3 | app owner | updater carries `OTA_PUBLIC_KEYS [K1, K2]` and honours `revoked_keys`; the three-key test of the rig installer | -| Q32 | No hardware wallet path despite spec 8.5 "MUST offer" | `docs/spec/08-client-security.md:36` | the project lead-visible | 24, or 0.5 to relabel | app owner (cryptographer reviews) | a Ledger signs one devnet transfer through the Ethereum app at chain id 4463; or the spec row reads "Designed, not shipped" | +| Q32 | No hardware wallet path despite spec 8.5 "MUST offer" | `docs/spec/08-client-security.md:36` | the founder-visible | 24, or 0.5 to relabel | app owner (cryptographer reviews) | a Ledger signs one devnet transfer through the Ethereum app at chain id 4463; or the spec row reads "Designed, not shipped" | | Q33 | Single account, no second address, no address book | `engine.rs:343`, `ui/index.html:177` | user-visible | 6 | app owner | two derived accounts, a saved recipient reused in Send | | Q34 | `confirm()` on Remove wallet never renders in the Mac host | `app.js:598`, `wallet-ui-3-audit.md:189` | user-visible | 1 | app owner | in-page two-step card; snapshot test | | Q35 | Finality pause: node card "paused" or "not checkpoint-checkable without a node", rows stay "in block N"; public-RPC mode never says what to do | wallet `app.js:185, 321, 340` | user-visible | 2 | app owner | the row reads "in block N; finality is paused on the network" and the public-RPC line names the fix with a button | @@ -157,11 +157,11 @@ The row for the ledger: **Q1 plus Q2, Q5, Q6, Q7, Q10**: every public and operat | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| -| Q6 | Stale shown as live; "active" for 20 unlocked indices; "paused" with no since or cause; event flood (section 1) | `relay/ui.html:564, 367-368` | the project lead-visible | 4 | fleet agent, consensus-engineer | section 1 | +| Q6 | Stale shown as live; "active" for 20 unlocked indices; "paused" with no since or cause; event flood (section 1) | `relay/ui.html:564, 367-368` | the founder-visible | 4 | fleet agent, consensus-engineer | section 1 | | Q8 | X23 to X28 fixes off master; intake key can write and delete; token in URL (section 1) | `relay/api/console.mjs:249`, `relay/lib/relay.mjs:44` | operator-visible | 2 | fleet agent | section 1 | -| Q40 | A hung job reads "running" forever: no elapsed time, no timeout, no "last line N min ago" | `relay/api/console.mjs:182-187`, `relay/ui.html:317` | the project lead-visible | 2 | fleet agent | a killed job reads "no report for 12 min" in red within one refresh | +| Q40 | A hung job reads "running" forever: no elapsed time, no timeout, no "last line N min ago" | `relay/api/console.mjs:182-187`, `relay/ui.html:317` | the founder-visible | 2 | fleet agent | a killed job reads "no report for 12 min" in red within one refresh | | Q41 | Jobs tab trusts the unsigned `igneum-jobs.json` while the apps verify the signed file | `relay/api/console.mjs:170` vs `tools/jobs.mjs:57-60` | operator-visible | 1.5 | fleet agent | the tab shows the envelope's signature state per file | -| Q42 | Job result modal: no error lines first, no duration, no anchors (the Actions view) | `relay/ui.html:319`, `console.mjs:184-187` | the project lead-visible | 3 | fleet agent | a failed run opens with its five error lines and its duration first | +| Q42 | Job result modal: no error lines first, no duration, no anchors (the Actions view) | `relay/ui.html:319`, `console.mjs:184-187` | the founder-visible | 3 | fleet agent | a failed run opens with its five error lines and its duration first | | Q43 | Machines: no per-machine key state, no revoke, roles never enforced (the Tailscale view) | `relay/lib/relay.mjs:14`, `relay/api/relay.mjs:191-195` | operator-visible | 3 (after Q8) | fleet agent | a revoked PC's register is 403 and its card says "key revoked" | | Q44 | Builds: no rollback, no manifest history (the Vercel view) | `relay/ui.html:328-341` | operator-visible | 4 | fleet agent | one tap republishes the previous manifest and the fleet's OTA state shows it | | Q45 | "live feed unreachable: live feed unreachable" doubled; the whole Chain tab empties, losing Events and the infra card | `relay/api/console.mjs:210`, `relay/ui.html:357` | cosmetic | 0.5 | fleet agent | live feed down: tiles grey, Events and infra card still render | @@ -200,7 +200,7 @@ The row for the ledger: **Q1 plus Q2, Q5, Q6, Q7, Q10**: every public and operat | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| -| Q5 | "final" during a pause; 90-word hero; 24 GB contradiction; thumbnail OG (section 1) | `index.html:345, 494, 810, 838, 15-19` | the project lead-visible | 3 | site owner | section 1 | +| Q5 | "final" during a pause; 90-word hero; 24 GB contradiction; thumbnail OG (section 1) | `index.html:345, 494, 810, 838, 15-19` | the founder-visible | 3 | site owner | section 1 | | Q50 | No downloads page: hashes only for HiveOS, no signing key, no verify line, the Linux button goes to an anchor | `index.html:363-365, 516`, `miner.html:549-558` | user-visible | 2 | site owner | `/download` lists every artefact with version, size, sha256, the OTA key fingerprint and a verify command per OS; the Linux button links the file | | Q51 | `/miners` has 6 rows, two card models, "not measured" MH/W on every row; the eleven-card fleet table is not ingested | `miners.html:183`, `miner-bench.json` | user-visible | 2 | miner-community-lead | rows carry W and MH/W for every measured card | | Q52 | `dl\.igneum` in `forbidden-strings.txt` matches the public download host on three built pages; the scrub guards only bench and miners | `forbidden-strings.txt:25`, `build.mjs:171, 401` | operator-visible | 0.5 | site owner | the pattern becomes `dl\.igneum\.network/dl/(?!public/)` or `/dl/[0-9a-f]{12,}`, and the check runs on every built page | @@ -210,10 +210,10 @@ The row for the ledger: **Q1 plus Q2, Q5, Q6, Q7, Q10**: every public and operat | Q56 | Light-section contrast fails (3.08:1 links, 3.97:1 eyebrow) | `index.html:119-120, 130` | cosmetic | 0.5 | site owner | AA on every token pair; the same node script as Q12 | | Q57 | "devnet v0" eyebrow on the live page; the chain is v4 | `live.html:219` | cosmetic | 0.1 | site owner | matches the app's network word | | Q58 | Heading order (h3 before the first h2) | `index.html:359` | cosmetic | 0.2 | site owner | h2 or a styled div | -| Q59 | No team, custody or key-powers page (Reddit round 4 artefact 8); "0 admin keys in consensus" tile unchanged | `index.html:611`; no file | user-visible | 1 (plus the project lead's policy call) | site owner | the key-powers table published; the tile links it | -| Q60 | Roadmap dates versus the testnet: the journey says "Public testnet, Aug to Oct 2027" and the litepaper "Pools and the public testnet are August 2027", while `docs/plans/testnet-go.md` has `igneum-testnet-1` seeds up and a go checklist dated 5 October 2026 | `index.html:679`, `litepaper.html:719`, `docs/plans/testnet-go.md:1-4` | the project lead-visible | 0.5 (after the project lead decides which is true) | site owner | one date for the public testnet on the journey, the litepaper and the download notice | +| Q59 | No team, custody or key-powers page (Reddit round 4 artefact 8); "0 admin keys in consensus" tile unchanged | `index.html:611`; no file | user-visible | 1 (plus the founder's policy call) | site owner | the key-powers table published; the tile links it | +| Q60 | Roadmap dates versus the testnet: the journey says "Public testnet, Aug to Oct 2027" and the litepaper "Pools and the public testnet are August 2027", while `docs/plans/testnet-go.md` has `igneum-testnet-1` seeds up and a go checklist dated 5 October 2026 | `index.html:679`, `litepaper.html:719`, `docs/plans/testnet-go.md:1-4` | the founder-visible | 0.5 (after the founder decides which is true) | site owner | one date for the public testnet on the journey, the litepaper and the download notice | | Q61 | No Content-Security-Policy header on the site (the relay has none either); inline scripts throughout, two pinned CDN modules on the homepage | `site/vercel.json:7`, `relay/vercel.json:31-43`, `verify/verify.js:5-6` | operator-visible (security) | 2 | site owner | a CSP with hashes or nonces for the inline scripts and `script-src` limited to self and cdn.jsdelivr; every page renders with no console violation | -| Q62 | Copy-law borderline headlines: "Mined by GPUs. Proven by fire." (the brand line, the project lead's call), "GPUs are back ยท for good", "Last hour's chip is already obsolete.", "Your coins. Final means final.", "Dates slip. Gates do not.", "Install. Start. The card mines and proves.", "Nothing is mined here." | `index.html:343, 344, 424`, `wallet.html` h1, `litepaper.html:678`, `miner.html` h1, `404.html` h1 | cosmetic | 1 | site owner (the project lead rules on the brand line) | each either kept by the project lead's word or reworded | +| Q62 | Copy-law borderline headlines: "Mined by GPUs. Proven by fire." (the brand line, the founder's call), "GPUs are back ยท for good", "Last hour's chip is already obsolete.", "Your coins. Final means final.", "Dates slip. Gates do not.", "Install. Start. The card mines and proves.", "Nothing is mined here." | `index.html:343, 344, 424`, `wallet.html` h1, `litepaper.html:678`, `miner.html` h1, `404.html` h1 | cosmetic | 1 | site owner (the founder rules on the brand line) | each either kept by the founder's word or reworded | **Reddit round 4, still open in the HTML.** Finding 6 (24 GB): half fixed (Q5). Finding 7 (eleven-card table): open (Q51). Finding 8 (admin keys): sentence added to the litepaper (`:659`), tile unchanged, no table (Q59). Finding 15 ("Monero's idea, finished for GPUs"): open (`index.html:444`, `litepaper.html:429`). Finding 16 ("devnet v0"): open (Q57). Finding 22: `/ledger` present, no nav item. Findings 2, 3, 5, 9, 10, 12, 13, 17, 19: fixed in the current files. "0% anyone else in the protocol" legend still live (`index.html:604`). @@ -266,8 +266,8 @@ The row for the ledger: **Q1 plus Q2, Q5, Q6, Q7, Q10**: every public and operat | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| | Q67 | Member transport is plaintext TCP against spec 9.3's TLS 1.3 with session binding; `binding` ignored | `server.rs:1-2, 115`, `protocol.rs:58-60` | user-visible (hijack risk) | 8 | consensus-engineer | rustls listener; a replayed `authorize` on a second connection is refused (O-9.7) | -| Q68 | Node down shows "difficulty 0, DAA 0" and a ramp-day-0 reward; no "node unreachable" state | `node.rs:350-368`, `web/index.html:163-164` | the project lead-visible (once deployed) | 2 | miner-community-lead | the page renders "Node unreachable since